more log subcommands

This commit is contained in:
Mikaël Cluseau
2025-07-20 22:30:10 +02:00
parent b1bf8f3fb8
commit 52c23653ac
3 changed files with 109 additions and 47 deletions

View File

@ -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())

View File

@ -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
}
}
}

View File

@ -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 ;;