1use 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
79pub 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}