diff options
Diffstat (limited to 'src/logger.rs')
| -rw-r--r-- | src/logger.rs | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..8d9611b --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,121 @@ +use log::{LevelFilter, Log, Metadata, Record}; +use std::io::Write as _; + +pub struct Logger { + show_timestamp: bool, +} + +impl Logger { + /// Initialize the global logger. + /// + /// `level` is the configured log level (from config or CLI). + /// `RUST_LOG` env var overrides it if set and valid. + /// Timestamps are omitted when `JOURNAL_STREAM` is set (systemd/journald). + /// + /// # Panics + /// + /// Panics if called more than once (the global logger can only be set once). + pub fn init(level: LevelFilter) { + let effective_level = std::env::var("RUST_LOG") + .ok() + .and_then(|s| parse_level(&s)) + .unwrap_or(level); + + let show_timestamp = std::env::var_os("JOURNAL_STREAM").is_none(); + + let logger = Self { show_timestamp }; + log::set_boxed_logger(Box::new(logger)).expect("logger already initialized"); + log::set_max_level(effective_level); + } +} + +fn parse_level(s: &str) -> Option<LevelFilter> { + match s.trim().to_lowercase().as_str() { + "trace" => Some(LevelFilter::Trace), + "debug" => Some(LevelFilter::Debug), + "info" => Some(LevelFilter::Info), + "warn" => Some(LevelFilter::Warn), + "error" => Some(LevelFilter::Error), + "off" => Some(LevelFilter::Off), + _ => None, + } +} + +impl Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + let mut stderr = std::io::stderr().lock(); + if self.show_timestamp { + let now = crate::time::format_log_timestamp(std::time::SystemTime::now()); + let _ = writeln!(stderr, "{now} {:>5} {}", record.level(), record.args()); + } else { + let _ = writeln!(stderr, "{:>5} {}", record.level(), record.args()); + } + } + + fn flush(&self) { + let _ = std::io::stderr().flush(); + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + + #[test] + fn parse_level_valid_levels() { + assert_eq!(parse_level("trace"), Some(LevelFilter::Trace)); + assert_eq!(parse_level("debug"), Some(LevelFilter::Debug)); + assert_eq!(parse_level("info"), Some(LevelFilter::Info)); + assert_eq!(parse_level("warn"), Some(LevelFilter::Warn)); + assert_eq!(parse_level("error"), Some(LevelFilter::Error)); + } + + #[test] + fn parse_level_case_insensitive() { + assert_eq!(parse_level("DEBUG"), Some(LevelFilter::Debug)); + assert_eq!(parse_level("Debug"), Some(LevelFilter::Debug)); + assert_eq!(parse_level("dEbUg"), Some(LevelFilter::Debug)); + } + + #[test] + fn parse_level_invalid_returns_none() { + assert_eq!(parse_level("invalid"), None); + assert_eq!(parse_level(""), None); + assert_eq!(parse_level("verbose"), None); + } + + #[test] + fn parse_level_off() { + assert_eq!(parse_level("off"), Some(LevelFilter::Off)); + } + + #[test] + fn parse_level_trimmed() { + assert_eq!(parse_level(" info "), Some(LevelFilter::Info)); + assert_eq!(parse_level("\tdebug\n"), Some(LevelFilter::Debug)); + } + + #[test] + fn enabled_respects_max_level() { + let logger = Logger { + show_timestamp: true, + }; + + // Set max level to Warn for this test + log::set_max_level(LevelFilter::Warn); + + let warn_meta = log::MetadataBuilder::new().level(log::Level::Warn).build(); + let debug_meta = log::MetadataBuilder::new().level(log::Level::Debug).build(); + + assert!(logger.enabled(&warn_meta)); + assert!(!logger.enabled(&debug_meta)); + } +} |
