more log subcommands
This commit is contained in:
133
src/bin/dkl.rs
133
src/bin/dkl.rs
@ -24,21 +24,25 @@ enum Command {
|
||||
prefix: String,
|
||||
},
|
||||
Logger {
|
||||
#[arg(long, short = 'p', default_value = "/var/log")]
|
||||
/// Path where the logs are stored
|
||||
#[arg(long, short = 'p', default_value = "/var/log", env = "DKL_LOG_PATH")]
|
||||
log_path: String,
|
||||
/// Name of the log instead of the command's basename
|
||||
#[arg(long, short = 'n')]
|
||||
log_name: Option<String>,
|
||||
/// prefix log lines with time & stream
|
||||
#[arg(long)]
|
||||
with_prefix: bool,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
},
|
||||
Log {
|
||||
#[arg(long, short = 'p', default_value = "/var/log")]
|
||||
/// Path where the logs are stored
|
||||
#[arg(long, short = 'p', default_value = "/var/log", env = "DKL_LOG_PATH")]
|
||||
log_path: String,
|
||||
log_name: String,
|
||||
since: Option<String>,
|
||||
until: Option<String>,
|
||||
#[command(subcommand)]
|
||||
op: LogOp,
|
||||
},
|
||||
}
|
||||
|
||||
@ -84,30 +88,100 @@ async fn main() -> Result<()> {
|
||||
C::Log {
|
||||
log_path,
|
||||
log_name,
|
||||
since,
|
||||
until,
|
||||
} => {
|
||||
let since = parse_ts_arg(since)?;
|
||||
let until = parse_ts_arg(until)?;
|
||||
op,
|
||||
} => op.run(&log_path, &log_name).await,
|
||||
}
|
||||
}
|
||||
|
||||
let mut files = dkl::logger::log_files(&log_path, &log_name).await?;
|
||||
files.sort();
|
||||
async fn apply_config(config_file: &str, filters: &[glob::Pattern], chroot: &str) -> Result<()> {
|
||||
let config = fs::read_to_string(config_file).await?;
|
||||
let config: dkl::Config = serde_yaml::from_str(&config)?;
|
||||
|
||||
let mut out = tokio::io::stdout();
|
||||
let files = if filters.is_empty() {
|
||||
config.files
|
||||
} else {
|
||||
(config.files.into_iter())
|
||||
.filter(|f| filters.iter().any(|filter| filter.matches(&f.path)))
|
||||
.collect()
|
||||
};
|
||||
|
||||
for f in files {
|
||||
if !since.is_none_or(|since| f.timestamp >= since) {
|
||||
continue;
|
||||
dkl::apply::files(&files, chroot).await
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum LogOp {
|
||||
Ls {
|
||||
#[arg(short = 'l', long)]
|
||||
detail: bool,
|
||||
},
|
||||
Cleanup {
|
||||
/// days of log to keep
|
||||
days: u64,
|
||||
},
|
||||
Cat {
|
||||
/// print logs >= since
|
||||
since: Option<String>,
|
||||
/// print logs <= until
|
||||
until: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LogOp {
|
||||
async fn run(self, log_path: &str, log_name: &str) -> Result<()> {
|
||||
let mut files = dkl::logger::log_files(&log_path, &log_name).await?;
|
||||
files.sort();
|
||||
|
||||
use LogOp as Op;
|
||||
match self {
|
||||
Op::Ls { detail } => {
|
||||
for f in files {
|
||||
let path = f.path.to_string_lossy();
|
||||
if detail {
|
||||
println!("{ts} {path}", ts = f.timestamp);
|
||||
} else {
|
||||
println!("{path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Op::Cleanup { days } => {
|
||||
let deadline = chrono::Utc::now() - chrono::Days::new(days);
|
||||
let deadline = dkl::logger::trunc_ts(deadline);
|
||||
debug!("cleanup {log_name} logs < {deadline}");
|
||||
|
||||
for f in files {
|
||||
if f.timestamp < deadline {
|
||||
debug!("removing {}", f.path.to_string_lossy());
|
||||
fs::remove_file(f.path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Op::Cat { since, until } => {
|
||||
let since = parse_ts_arg(since)?;
|
||||
let until = parse_ts_arg(until)?;
|
||||
|
||||
let mut out = tokio::io::stdout();
|
||||
|
||||
for f in files {
|
||||
if !since.is_none_or(|since| f.timestamp >= since) {
|
||||
continue;
|
||||
}
|
||||
if !until.is_none_or(|until| f.timestamp <= until) {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!(
|
||||
"cat {path} (timestamp={ts}, compressed={comp})",
|
||||
path = f.path.to_string_lossy(),
|
||||
ts = f.timestamp.to_rfc3339(),
|
||||
comp = f.compressed
|
||||
);
|
||||
if let Err(e) = f.copy_to(&mut out).await {
|
||||
error!("{file}: {e}", file = f.path.to_string_lossy());
|
||||
}
|
||||
}
|
||||
if !until.is_none_or(|until| f.timestamp <= until) {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("{f:?}");
|
||||
f.copy_to(&mut out).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,21 +200,6 @@ fn basename(path: &str) -> &str {
|
||||
path.rsplit_once('/').map_or(path, |split| split.1)
|
||||
}
|
||||
|
||||
async fn apply_config(config_file: &str, filters: &[glob::Pattern], chroot: &str) -> Result<()> {
|
||||
let config = fs::read_to_string(config_file).await?;
|
||||
let config: dkl::Config = serde_yaml::from_str(&config)?;
|
||||
|
||||
let files = if filters.is_empty() {
|
||||
config.files
|
||||
} else {
|
||||
(config.files.into_iter())
|
||||
.filter(|f| filters.iter().any(|filter| filter.matches(&f.path)))
|
||||
.collect()
|
||||
};
|
||||
|
||||
dkl::apply::files(&files, chroot).await
|
||||
}
|
||||
|
||||
fn parse_globs(filters: &[String]) -> Result<Vec<glob::Pattern>> {
|
||||
let mut errors = false;
|
||||
let filters = (filters.iter())
|
||||
|
@ -35,7 +35,7 @@ impl<'t> Logger<'t> {
|
||||
let archives_read_dir = (fs::read_dir(archives_path).await)
|
||||
.map_err(|e| format_err!("failed to list archives: {e}"))?;
|
||||
|
||||
let mut prev_stamp = ts_trunc(Utc::now());
|
||||
let mut prev_stamp = trunc_ts(Utc::now());
|
||||
let mut current_log = BufWriter::new(self.open_log(prev_stamp).await?);
|
||||
|
||||
tokio::spawn(compress_archives(
|
||||
@ -94,7 +94,7 @@ impl<'t> Logger<'t> {
|
||||
prev_stamp: &mut Timestamp,
|
||||
out: &mut BufWriter<File>,
|
||||
) -> Result<()> {
|
||||
let trunc_ts = ts_trunc(log.ts);
|
||||
let trunc_ts = trunc_ts(log.ts);
|
||||
if *prev_stamp < trunc_ts {
|
||||
// switch log
|
||||
out.flush().await?;
|
||||
@ -193,7 +193,7 @@ async fn copy(
|
||||
}
|
||||
}
|
||||
|
||||
fn ts_trunc(ts: Timestamp) -> Timestamp {
|
||||
pub fn trunc_ts(ts: Timestamp) -> Timestamp {
|
||||
ts.duration_trunc(TRUNC_DELTA)
|
||||
.expect("duration_trunc failed")
|
||||
}
|
||||
@ -331,12 +331,12 @@ impl PartialOrd for LogFile {
|
||||
|
||||
impl LogFile {
|
||||
pub async fn copy_to(&self, out: &mut (impl AsyncWrite + Unpin)) -> io::Result<u64> {
|
||||
let mut input = File::open(&self.path).await?;
|
||||
let input = &mut File::open(&self.path).await?;
|
||||
if self.compressed {
|
||||
let mut out = ZstdDecoder::new(out);
|
||||
tokio::io::copy(&mut input, &mut out).await
|
||||
let out = &mut ZstdDecoder::new(out);
|
||||
tokio::io::copy(input, out).await
|
||||
} else {
|
||||
tokio::io::copy(&mut input, out).await
|
||||
tokio::io::copy(input, out).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
test-dkl
9
test-dkl
@ -3,7 +3,7 @@ set -ex
|
||||
|
||||
dkl=target/debug/dkl
|
||||
|
||||
test=${1:-log}
|
||||
test=${1:-log-clean}
|
||||
|
||||
export RUST_LOG=debug
|
||||
|
||||
@ -19,8 +19,11 @@ case $test in
|
||||
cat tmp/log/bash.log
|
||||
;;
|
||||
|
||||
log)
|
||||
$dkl log --log-path tmp/log bash 20250720_12 20250720_16
|
||||
log-ls) $dkl log --log-path tmp/log bash ls ;;
|
||||
log-cat) $dkl log --log-path tmp/log bash cat 20250720_12 20301231_23 ;;
|
||||
log-clean)
|
||||
$dkl log --log-path tmp/log bash ls -l
|
||||
$dkl log --log-path tmp/log bash cleanup 0
|
||||
;;
|
||||
|
||||
*) echo 1>&2 "unknown test: $test"; exit 1 ;;
|
||||
|
Reference in New Issue
Block a user