1use std::io::Write as _;
13use std::sync::OnceLock;
14
15use log::{Level, LevelFilter, Log, Metadata, Record};
16
17const 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 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
104pub 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#[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#[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}