Skip to main content

bwx/
logger.rs

1//! Minimal RUST_LOG-compatible logger writing `LEVEL: message` to stderr.
2
3use std::io::Write as _;
4use std::sync::OnceLock;
5
6use log::{LevelFilter, Log, Metadata, Record};
7
8struct Logger {
9    default: LevelFilter,
10    modules: Vec<(String, LevelFilter)>,
11}
12
13impl Logger {
14    fn level_for(&self, target: &str) -> LevelFilter {
15        let mut best: Option<(usize, LevelFilter)> = None;
16        for (module, lvl) in &self.modules {
17            let matches = target == module
18                || (target.starts_with(module)
19                    && target.as_bytes().get(module.len()) == Some(&b':'));
20            if matches && best.is_none_or(|(len, _)| module.len() > len) {
21                best = Some((module.len(), *lvl));
22            }
23        }
24        best.map_or(self.default, |(_, l)| l)
25    }
26}
27
28impl Log for Logger {
29    fn enabled(&self, metadata: &Metadata) -> bool {
30        self.level_for(metadata.target()) >= metadata.level()
31    }
32
33    fn log(&self, record: &Record) {
34        if !self.enabled(record.metadata()) {
35            return;
36        }
37        let stderr = std::io::stderr();
38        let mut h = stderr.lock();
39        let _ = writeln!(h, "{}: {}", record.level(), record.args());
40    }
41
42    fn flush(&self) {
43        let _ = std::io::stderr().flush();
44    }
45}
46
47static LOGGER: OnceLock<Logger> = OnceLock::new();
48
49fn parse_level(s: &str) -> Option<LevelFilter> {
50    match s.trim().to_ascii_lowercase().as_str() {
51        "off" => Some(LevelFilter::Off),
52        "error" => Some(LevelFilter::Error),
53        "warn" => Some(LevelFilter::Warn),
54        "info" => Some(LevelFilter::Info),
55        "debug" => Some(LevelFilter::Debug),
56        "trace" => Some(LevelFilter::Trace),
57        _ => None,
58    }
59}
60
61fn parse_spec(
62    spec: &str,
63    fallback: LevelFilter,
64) -> (LevelFilter, Vec<(String, LevelFilter)>) {
65    let mut default = fallback;
66    let mut modules = Vec::new();
67    for part in spec.split(',').map(str::trim).filter(|p| !p.is_empty()) {
68        if let Some((module, lvl)) = part.split_once('=') {
69            if let Some(lvl) = parse_level(lvl) {
70                modules.push((module.trim().to_string(), lvl));
71            }
72        } else if let Some(lvl) = parse_level(part) {
73            default = lvl;
74        }
75    }
76    (default, modules)
77}
78
79/// Initialize the global logger from `RUST_LOG`, using `default_level` if unset.
80pub fn init(default_level: &str) {
81    let fallback = parse_level(default_level).unwrap_or(LevelFilter::Info);
82    let spec = std::env::var("RUST_LOG").unwrap_or_default();
83    let (default, modules) = parse_spec(&spec, fallback);
84
85    let max = modules
86        .iter()
87        .map(|(_, l)| *l)
88        .chain(std::iter::once(default))
89        .max()
90        .unwrap_or(LevelFilter::Off);
91
92    let logger = LOGGER.get_or_init(|| Logger { default, modules });
93
94    let _ = log::set_logger(logger);
95    log::set_max_level(max);
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn bare_level() {
104        let (d, m) = parse_spec("debug", LevelFilter::Info);
105        assert_eq!(d, LevelFilter::Debug);
106        assert!(m.is_empty());
107    }
108
109    #[test]
110    fn default_and_modules() {
111        let (d, m) = parse_spec("info,bwx=debug", LevelFilter::Warn);
112        assert_eq!(d, LevelFilter::Info);
113        assert_eq!(m, vec![("bwx".to_string(), LevelFilter::Debug)]);
114    }
115
116    #[test]
117    fn trailing_default() {
118        let (d, m) = parse_spec("bwx_agent=trace,warn", LevelFilter::Info);
119        assert_eq!(d, LevelFilter::Warn);
120        assert_eq!(m, vec![("bwx_agent".to_string(), LevelFilter::Trace)]);
121    }
122
123    #[test]
124    fn empty_uses_fallback() {
125        let (d, m) = parse_spec("", LevelFilter::Info);
126        assert_eq!(d, LevelFilter::Info);
127        assert!(m.is_empty());
128    }
129
130    #[test]
131    fn level_for_module_prefix() {
132        let logger = Logger {
133            default: LevelFilter::Warn,
134            modules: vec![("bwx".to_string(), LevelFilter::Debug)],
135        };
136        assert_eq!(logger.level_for("bwx"), LevelFilter::Debug);
137        assert_eq!(logger.level_for("bwx::config"), LevelFilter::Debug);
138        assert_eq!(logger.level_for("other"), LevelFilter::Warn);
139        assert_eq!(logger.level_for("bwxx"), LevelFilter::Warn);
140    }
141}