Miscellaneous fixes across CLI and core modules: - Timeline: widen TAG_WIDTH from 10 to 11 to accommodate longer event type labels without truncation - render.rs: save and restore LORE_ICONS env var in glyph_mode test to prevent interference from the test environment leaking into or from other tests that set LORE_ICONS - logging.rs: adjust verbose=1 to info level (was debug), verbose=2 to debug — this reduces noise at -v while keeping -vv as the full debug experience - issues.rs, merge_requests.rs: use infodebug! macro consistently for ingestion summary logging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
218 lines
6.2 KiB
Rust
218 lines
6.2 KiB
Rust
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<S, N> FormatEvent<S, N> 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<String> {
|
|
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();
|
|
}
|
|
}
|