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,
|
prefix: String,
|
||||||
},
|
},
|
||||||
Logger {
|
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,
|
log_path: String,
|
||||||
|
/// Name of the log instead of the command's basename
|
||||||
#[arg(long, short = 'n')]
|
#[arg(long, short = 'n')]
|
||||||
log_name: Option<String>,
|
log_name: Option<String>,
|
||||||
|
/// prefix log lines with time & stream
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
with_prefix: bool,
|
with_prefix: bool,
|
||||||
command: String,
|
command: String,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
},
|
},
|
||||||
Log {
|
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_path: String,
|
||||||
log_name: String,
|
log_name: String,
|
||||||
since: Option<String>,
|
#[command(subcommand)]
|
||||||
until: Option<String>,
|
op: LogOp,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,30 +88,100 @@ async fn main() -> Result<()> {
|
|||||||
C::Log {
|
C::Log {
|
||||||
log_path,
|
log_path,
|
||||||
log_name,
|
log_name,
|
||||||
since,
|
op,
|
||||||
until,
|
} => op.run(&log_path, &log_name).await,
|
||||||
} => {
|
}
|
||||||
let since = parse_ts_arg(since)?;
|
}
|
||||||
let until = parse_ts_arg(until)?;
|
|
||||||
|
|
||||||
let mut files = dkl::logger::log_files(&log_path, &log_name).await?;
|
async fn apply_config(config_file: &str, filters: &[glob::Pattern], chroot: &str) -> Result<()> {
|
||||||
files.sort();
|
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 {
|
dkl::apply::files(&files, chroot).await
|
||||||
if !since.is_none_or(|since| f.timestamp >= since) {
|
}
|
||||||
continue;
|
|
||||||
|
#[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)
|
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>> {
|
fn parse_globs(filters: &[String]) -> Result<Vec<glob::Pattern>> {
|
||||||
let mut errors = false;
|
let mut errors = false;
|
||||||
let filters = (filters.iter())
|
let filters = (filters.iter())
|
||||||
|
@ -35,7 +35,7 @@ impl<'t> Logger<'t> {
|
|||||||
let archives_read_dir = (fs::read_dir(archives_path).await)
|
let archives_read_dir = (fs::read_dir(archives_path).await)
|
||||||
.map_err(|e| format_err!("failed to list archives: {e}"))?;
|
.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?);
|
let mut current_log = BufWriter::new(self.open_log(prev_stamp).await?);
|
||||||
|
|
||||||
tokio::spawn(compress_archives(
|
tokio::spawn(compress_archives(
|
||||||
@ -94,7 +94,7 @@ impl<'t> Logger<'t> {
|
|||||||
prev_stamp: &mut Timestamp,
|
prev_stamp: &mut Timestamp,
|
||||||
out: &mut BufWriter<File>,
|
out: &mut BufWriter<File>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let trunc_ts = ts_trunc(log.ts);
|
let trunc_ts = trunc_ts(log.ts);
|
||||||
if *prev_stamp < trunc_ts {
|
if *prev_stamp < trunc_ts {
|
||||||
// switch log
|
// switch log
|
||||||
out.flush().await?;
|
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)
|
ts.duration_trunc(TRUNC_DELTA)
|
||||||
.expect("duration_trunc failed")
|
.expect("duration_trunc failed")
|
||||||
}
|
}
|
||||||
@ -331,12 +331,12 @@ impl PartialOrd for LogFile {
|
|||||||
|
|
||||||
impl LogFile {
|
impl LogFile {
|
||||||
pub async fn copy_to(&self, out: &mut (impl AsyncWrite + Unpin)) -> io::Result<u64> {
|
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 {
|
if self.compressed {
|
||||||
let mut out = ZstdDecoder::new(out);
|
let out = &mut ZstdDecoder::new(out);
|
||||||
tokio::io::copy(&mut input, &mut out).await
|
tokio::io::copy(input, out).await
|
||||||
} else {
|
} 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
|
dkl=target/debug/dkl
|
||||||
|
|
||||||
test=${1:-log}
|
test=${1:-log-clean}
|
||||||
|
|
||||||
export RUST_LOG=debug
|
export RUST_LOG=debug
|
||||||
|
|
||||||
@ -19,8 +19,11 @@ case $test in
|
|||||||
cat tmp/log/bash.log
|
cat tmp/log/bash.log
|
||||||
;;
|
;;
|
||||||
|
|
||||||
log)
|
log-ls) $dkl log --log-path tmp/log bash ls ;;
|
||||||
$dkl log --log-path tmp/log bash 20250720_12 20250720_16
|
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 ;;
|
*) echo 1>&2 "unknown test: $test"; exit 1 ;;
|
||||||
|
Reference in New Issue
Block a user