From 52c23653ac81f3e24a8d7079b2348fe592334571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Cluseau?= Date: Sun, 20 Jul 2025 22:30:10 +0200 Subject: [PATCH] more log subcommands --- src/bin/dkl.rs | 133 +++++++++++++++++++++++++++++++++++-------------- src/logger.rs | 14 +++--- test-dkl | 9 ++-- 3 files changed, 109 insertions(+), 47 deletions(-) diff --git a/src/bin/dkl.rs b/src/bin/dkl.rs index 2a099ae..5330960 100644 --- a/src/bin/dkl.rs +++ b/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, + /// prefix log lines with time & stream #[arg(long)] with_prefix: bool, command: String, args: Vec, }, 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, - until: Option, + #[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, + /// print logs <= until + until: Option, + }, +} + +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> { let mut errors = false; let filters = (filters.iter()) diff --git a/src/logger.rs b/src/logger.rs index 9378791..c0d4870 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -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, ) -> 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 { - 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 } } } diff --git a/test-dkl b/test-dkl index 67cf7f1..2bd2705 100755 --- a/test-dkl +++ b/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 ;;