Skip to main content

bwx/
logger.rs

1//! Stderr logger gated by a single config boolean.
2//!
3//! When `logging` is `true` (the default), records from our crates at level
4//! `debug` and above (`debug`/`warn`/`error`) are written to stderr. When
5//! `false`, nothing is emitted. Output is restricted to our crates so
6//! third-party libraries don't pollute the terminal.
7//!
8//! `RUST_LOG` is still honored when set — useful for ad-hoc debugging — and
9//! is parsed as an `env_logger`-style spec with the configured baseline as
10//! the fallback default.
11
12use std::io::Write as _;
13use std::sync::OnceLock;
14
15use log::{Level, LevelFilter, Log, Metadata, Record};
16
17/// Crate-name prefixes whose log records should be emitted by default.
18/// Anything else only emits when `RUST_LOG` explicitly opts it in.
19const OUR_CRATES: &[&str] = &["bwx", "bwx_agent"];
20
21struct Logger {
22    default: LevelFilter,
23    modules: Vec<(String, LevelFilter)>,
24}
25
26impl Logger {
27    fn level_for(&self, target: &str) -> LevelFilter {
28        let mut best: Option<(usize, LevelFilter)> = None;
29        for (module, lvl) in &self.modules {
30            let matches = target == module
31                || (target.starts_with(module)
32                    && target.as_bytes().get(module.len()) == Some(&b':'));
33            if matches && best.is_none_or(|(len, _)| module.len() > len) {
34                best = Some((module.len(), *lvl));
35            }
36        }
37        best.map_or(self.default, |(_, l)| l)
38    }
39}
40
41impl Log for Logger {
42    fn enabled(&self, metadata: &Metadata) -> bool {
43        self.level_for(metadata.target()) >= metadata.level()
44    }
45
46    fn log(&self, record: &Record) {
47        if !self.enabled(record.metadata()) {
48            return;
49        }
50        let stderr = std::io::stderr();
51        let mut h = stderr.lock();
52        let _ = match record.level() {
53            Level::Error => writeln!(h, "error: {}", record.args()),
54            Level::Warn => writeln!(h, "warning: {}", record.args()),
55            // Info shouldn't normally fire (we don't ship info-level
56            // sources), but render it cleanly if some dependency does.
57            Level::Info => writeln!(h, "{}", record.args()),
58            Level::Debug => {
59                writeln!(h, "debug [{}] {}", record.target(), record.args())
60            }
61            Level::Trace => {
62                writeln!(h, "trace [{}] {}", record.target(), record.args())
63            }
64        };
65    }
66
67    fn flush(&self) {
68        let _ = std::io::stderr().flush();
69    }
70}
71
72static LOGGER: OnceLock<Logger> = OnceLock::new();
73
74fn parse_level(s: &str) -> Option<LevelFilter> {
75    match s.trim().to_ascii_lowercase().as_str() {
76        "off" => Some(LevelFilter::Off),
77        "error" => Some(LevelFilter::Error),
78        "warn" => Some(LevelFilter::Warn),
79        "info" => Some(LevelFilter::Info),
80        "debug" => Some(LevelFilter::Debug),
81        "trace" => Some(LevelFilter::Trace),
82        _ => None,
83    }
84}
85
86fn parse_spec(
87    spec: &str,
88    fallback: LevelFilter,
89) -> (LevelFilter, Vec<(String, LevelFilter)>) {
90    let mut default = fallback;
91    let mut modules = Vec::new();
92    for part in spec.split(',').map(str::trim).filter(|p| !p.is_empty()) {
93        if let Some((module, lvl)) = part.split_once('=') {
94            if let Some(lvl) = parse_level(lvl) {
95                modules.push((module.trim().to_string(), lvl));
96            }
97        } else if let Some(lvl) = parse_level(part) {
98            default = lvl;
99        }
100    }
101    (default, modules)
102}
103
104/// Initialize the global logger.
105///
106/// `enabled` is the configured baseline: `true` emits records at `debug`
107/// and above from our crates, `false` silences everything. `RUST_LOG`, if
108/// set, takes precedence and is parsed as an `env_logger`-style spec
109/// using the configured baseline as the default level.
110pub fn init(enabled: bool) {
111    let baseline = if enabled {
112        LevelFilter::Debug
113    } else {
114        LevelFilter::Off
115    };
116    let env = std::env::var("RUST_LOG").unwrap_or_default();
117    let (default, modules) = if env.trim().is_empty() {
118        let modules = OUR_CRATES
119            .iter()
120            .map(|c| ((*c).to_string(), baseline))
121            .collect();
122        (LevelFilter::Off, modules)
123    } else {
124        parse_spec(&env, baseline)
125    };
126
127    let max = modules
128        .iter()
129        .map(|(_, l)| *l)
130        .chain(std::iter::once(default))
131        .max()
132        .unwrap_or(LevelFilter::Off);
133
134    let logger = LOGGER.get_or_init(|| Logger { default, modules });
135
136    let _ = log::set_logger(logger);
137    log::set_max_level(max);
138}
139
140/// Format a `Duration` in a friendly form: `523ms`, `2.341s`, `1m 32s`, `5m`.
141#[must_use]
142pub fn format_duration(d: std::time::Duration) -> String {
143    let secs = d.as_secs();
144    let millis = d.subsec_millis();
145    if secs >= 60 {
146        let m = secs / 60;
147        let s = secs % 60;
148        if s == 0 {
149            format!("{m}m")
150        } else {
151            format!("{m}m {s}s")
152        }
153    } else if secs > 0 {
154        if millis == 0 {
155            format!("{secs}s")
156        } else {
157            format!("{secs}.{millis:03}s")
158        }
159    } else {
160        format!("{millis}ms")
161    }
162}
163
164/// Time `$body` and emit a debug record with the elapsed duration.
165///
166/// When debug is disabled the body runs without any timing instrumentation —
167/// no `Instant`, no string formatting — so this is safe on hot paths.
168#[macro_export]
169macro_rules! debug_time {
170    ($label:expr, $body:expr $(,)?) => {{
171        if ::log::log_enabled!(::log::Level::Debug) {
172            let __bwx_start = ::std::time::Instant::now();
173            let __bwx_result = $body;
174            ::log::debug!(
175                "{} ({})",
176                $label,
177                $crate::logger::format_duration(__bwx_start.elapsed()),
178            );
179            __bwx_result
180        } else {
181            $body
182        }
183    }};
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn bare_level() {
192        let (d, m) = parse_spec("debug", LevelFilter::Info);
193        assert_eq!(d, LevelFilter::Debug);
194        assert!(m.is_empty());
195    }
196
197    #[test]
198    fn default_and_modules() {
199        let (d, m) = parse_spec("info,bwx=debug", LevelFilter::Warn);
200        assert_eq!(d, LevelFilter::Info);
201        assert_eq!(m, vec![("bwx".to_string(), LevelFilter::Debug)]);
202    }
203
204    #[test]
205    fn trailing_default() {
206        let (d, m) = parse_spec("bwx_agent=trace,warn", LevelFilter::Info);
207        assert_eq!(d, LevelFilter::Warn);
208        assert_eq!(m, vec![("bwx_agent".to_string(), LevelFilter::Trace)]);
209    }
210
211    #[test]
212    fn empty_uses_fallback() {
213        let (d, m) = parse_spec("", LevelFilter::Info);
214        assert_eq!(d, LevelFilter::Info);
215        assert!(m.is_empty());
216    }
217
218    #[test]
219    fn level_for_module_prefix() {
220        let logger = Logger {
221            default: LevelFilter::Off,
222            modules: vec![("bwx".to_string(), LevelFilter::Debug)],
223        };
224        assert_eq!(logger.level_for("bwx"), LevelFilter::Debug);
225        assert_eq!(logger.level_for("bwx::config"), LevelFilter::Debug);
226        assert_eq!(logger.level_for("other"), LevelFilter::Off);
227        assert_eq!(logger.level_for("bwxx"), LevelFilter::Off);
228    }
229
230    #[test]
231    fn duration_formatting() {
232        use std::time::Duration;
233        assert_eq!(format_duration(Duration::from_millis(523)), "523ms");
234        assert_eq!(format_duration(Duration::from_millis(2341)), "2.341s");
235        assert_eq!(format_duration(Duration::from_secs(5)), "5s");
236        assert_eq!(format_duration(Duration::from_secs(92)), "1m 32s");
237        assert_eq!(format_duration(Duration::from_secs(300)), "5m");
238    }
239}