summaryrefslogtreecommitdiff
path: root/src/logger.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/logger.rs')
-rw-r--r--src/logger.rs121
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));
+ }
+}