use std::fmt; use std::fs; use std::path::Path; use tracing_subscriber::EnvFilter; use tracing_subscriber::fmt::format::{FormatEvent, FormatFields}; use tracing_subscriber::registry::LookupSpan; /// Compact stderr formatter: `HH:MM:SS LEVEL message key=value` /// /// No span context, no full timestamps, no target — just the essentials. /// The JSON file log is unaffected (it uses its own layer). pub struct CompactHumanFormat; impl FormatEvent for CompactHumanFormat where S: tracing::Subscriber + for<'a> LookupSpan<'a>, N: for<'a> FormatFields<'a> + 'static, { fn format_event( &self, ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>, mut writer: tracing_subscriber::fmt::format::Writer<'_>, event: &tracing::Event<'_>, ) -> fmt::Result { let now = chrono::Local::now(); let time = now.format("%H:%M:%S"); let level = *event.metadata().level(); let styled = match level { tracing::Level::ERROR => console::style("ERROR").red().bold(), tracing::Level::WARN => console::style(" WARN").yellow(), tracing::Level::INFO => console::style(" INFO").green(), tracing::Level::DEBUG => console::style("DEBUG").dim(), tracing::Level::TRACE => console::style("TRACE").dim(), }; write!(writer, "{time} {styled} ")?; ctx.format_fields(writer.by_ref(), event)?; writeln!(writer) } } pub fn build_stderr_filter(verbose: u8, quiet: bool) -> EnvFilter { if std::env::var("RUST_LOG").is_ok() { return EnvFilter::from_default_env(); } if quiet { return EnvFilter::new("lore=warn,error"); } let directives = match verbose { 0 => "lore=warn", 1 => "lore=info,warn", 2 => "lore=debug,info", _ => "lore=trace,debug", }; EnvFilter::new(directives) } pub fn build_file_filter() -> EnvFilter { if std::env::var("RUST_LOG").is_ok() { return EnvFilter::from_default_env(); } EnvFilter::new("lore=debug,warn") } pub fn cleanup_old_logs(log_dir: &Path, retention_days: u32) -> usize { if retention_days == 0 || !log_dir.exists() { return 0; } let cutoff = chrono::Utc::now() - chrono::Duration::days(i64::from(retention_days)); let cutoff_date = cutoff.format("%Y-%m-%d").to_string(); let mut deleted = 0; let entries = match fs::read_dir(log_dir) { Ok(entries) => entries, Err(_) => return 0, }; for entry in entries.flatten() { let file_name = entry.file_name(); let name = file_name.to_string_lossy(); if let Some(date_str) = extract_log_date(&name) && date_str < cutoff_date && fs::remove_file(entry.path()).is_ok() { deleted += 1; } } deleted } fn extract_log_date(filename: &str) -> Option { let rest = filename.strip_prefix("lore.")?; let date_part = rest.get(..10)?; let parts: Vec<&str> = date_part.split('-').collect(); if parts.len() != 3 || parts[0].len() != 4 || parts[1].len() != 2 || parts[2].len() != 2 { return None; } if !parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit())) { return None; } let suffix = rest.get(10..)?; if suffix.is_empty() || suffix == ".log" { Some(date_part.to_string()) } else { None } } #[cfg(test)] mod tests { use super::*; use std::fs::File; use tempfile::TempDir; #[test] fn test_extract_log_date_with_extension() { assert_eq!( extract_log_date("lore.2026-02-04.log"), Some("2026-02-04".to_string()) ); } #[test] fn test_extract_log_date_without_extension() { assert_eq!( extract_log_date("lore.2026-02-04"), Some("2026-02-04".to_string()) ); } #[test] fn test_extract_log_date_rejects_non_log_files() { assert_eq!(extract_log_date("other.txt"), None); assert_eq!(extract_log_date("lore.config.json"), None); assert_eq!(extract_log_date("lore.db"), None); } #[test] fn test_extract_log_date_rejects_invalid_dates() { assert_eq!(extract_log_date("lore.not-a-date.log"), None); assert_eq!(extract_log_date("lore.20260204.log"), None); } #[test] fn test_cleanup_old_logs_deletes_old_files() { let dir = TempDir::new().unwrap(); File::create(dir.path().join("lore.2020-01-01.log")).unwrap(); File::create(dir.path().join("lore.2020-01-15.log")).unwrap(); let today = chrono::Utc::now().format("%Y-%m-%d").to_string(); let recent_name = format!("lore.{today}.log"); File::create(dir.path().join(&recent_name)).unwrap(); File::create(dir.path().join("other.txt")).unwrap(); let deleted = cleanup_old_logs(dir.path(), 7); assert_eq!(deleted, 2); assert!(!dir.path().join("lore.2020-01-01.log").exists()); assert!(!dir.path().join("lore.2020-01-15.log").exists()); assert!(dir.path().join(&recent_name).exists()); assert!(dir.path().join("other.txt").exists()); } #[test] fn test_cleanup_old_logs_zero_retention_is_noop() { let dir = TempDir::new().unwrap(); File::create(dir.path().join("lore.2020-01-01.log")).unwrap(); let deleted = cleanup_old_logs(dir.path(), 0); assert_eq!(deleted, 0); assert!(dir.path().join("lore.2020-01-01.log").exists()); } #[test] fn test_cleanup_old_logs_nonexistent_dir() { let deleted = cleanup_old_logs(Path::new("/nonexistent/dir"), 7); assert_eq!(deleted, 0); } #[test] fn test_build_stderr_filter_default() { let _filter = build_stderr_filter(0, false); } #[test] fn test_build_stderr_filter_verbose_levels() { let _f0 = build_stderr_filter(0, false); let _f1 = build_stderr_filter(1, false); let _f2 = build_stderr_filter(2, false); let _f3 = build_stderr_filter(3, false); } #[test] fn test_build_stderr_filter_quiet_overrides_verbose() { let _filter = build_stderr_filter(3, true); } #[test] fn test_build_file_filter() { let _filter = build_file_filter(); } }