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 { 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)); } }