brep_app/logger.rs
1//! The native binary's `log` sink: `RUST_LOG`-filtered lines to stderr.
2//!
3//! This exists so the warnings eframe, winit and wgpu emit through the `log`
4//! facade reach the terminal — the app itself logs three lines. It replaces
5//! `env_logger`, which cost twelve crates for that: a full `regex`, `jiff` +
6//! `jiff-core` (a date library, for timestamps we never print) and the
7//! `anstream`/`anstyle`/`colorchoice` colour stack.
8//!
9//! What it keeps of `env_logger`'s behaviour: the `RUST_LOG` variable, its
10//! comma-separated `level` / `target=level` directives (longest matching target
11//! prefix wins, exactly as `env_logger` resolves them), the `Error` default
12//! when `RUST_LOG` is unset or unparseable, and the `[LEVEL target] message`
13//! line shape. What it drops: colour, timestamps, and regex message filters
14//! (`RUST_LOG=…/pattern`) — none of which this binary ever asked for. A `/`
15//! suffix is parsed off and ignored rather than misread as part of a target.
16//!
17//! Native only: the wasm build logs through the browser console.
18
19use std::io::Write as _;
20
21/// One `target=level` directive, or a bare `level` when `target` is empty.
22struct Directive {
23 target: String,
24 level: log::LevelFilter,
25}
26
27struct StderrLogger {
28 /// Longest-prefix-first, so `resolve` can take the first match.
29 directives: Vec<Directive>,
30 /// The loosest level any directive allows — what `enabled` answers with
31 /// before a target is known, and the ceiling `log::set_max_level` gets.
32 max: log::LevelFilter,
33}
34
35impl StderrLogger {
36 /// The level that applies to `target`: the longest directive prefix that
37 /// matches it, else the bare-level directive, else `Off`.
38 fn resolve(&self, target: &str) -> log::LevelFilter {
39 for d in &self.directives {
40 if d.target.is_empty() || target.starts_with(&d.target) {
41 return d.level;
42 }
43 }
44 log::LevelFilter::Off
45 }
46}
47
48impl log::Log for StderrLogger {
49 fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
50 // Cheap reject before the per-target scan: nothing above the loosest
51 // directive can pass, whatever its target.
52 metadata.level() <= self.max && metadata.level() <= self.resolve(metadata.target())
53 }
54
55 fn log(&self, record: &log::Record<'_>) {
56 if !self.enabled(record.metadata()) {
57 return;
58 }
59 // Ignore a closed/broken stderr: a logger must never take the app down.
60 let _ = writeln!(
61 std::io::stderr(),
62 "[{} {}] {}",
63 record.level(),
64 record.target(),
65 record.args()
66 );
67 }
68
69 fn flush(&self) {
70 let _ = std::io::stderr().flush();
71 }
72}
73
74/// `RUST_LOG` as directives, longest target prefix first. An empty or
75/// all-unparseable spec yields the `Error` default `env_logger` also uses.
76fn parse(spec: &str) -> Vec<Directive> {
77 // `env_logger` splits a regex message filter off at the first `/`.
78 let spec = spec.split('/').next().unwrap_or("");
79 let mut directives: Vec<Directive> = Vec::new();
80 for part in spec.split(',').map(str::trim).filter(|p| !p.is_empty()) {
81 let (target, level) = match part.split_once('=') {
82 // `target=level`; a bare `target=` means "everything from target".
83 // A bare `=` names nothing, so it is dropped rather than read as a
84 // global `trace` — which is what `env_logger` does with it too.
85 Some((t, "")) if t.trim().is_empty() => continue,
86 Some((t, "")) => (t.trim(), log::LevelFilter::Trace),
87 Some((t, l)) => match l.trim().parse() {
88 Ok(level) => (t.trim(), level),
89 Err(_) => continue,
90 },
91 // A bare word is a level if it parses as one, a target otherwise.
92 None => match part.parse() {
93 Ok(level) => ("", level),
94 Err(_) => (part, log::LevelFilter::Trace),
95 },
96 };
97 directives.push(Directive { target: target.to_owned(), level });
98 }
99 if directives.is_empty() {
100 directives.push(Directive { target: String::new(), level: log::LevelFilter::Error });
101 }
102 // Longest target first so the most specific directive wins; the bare-level
103 // directive (empty target) sorts last and acts as the fallback.
104 directives.sort_by(|a, b| b.target.len().cmp(&a.target.len()));
105 directives
106}
107
108/// Install the logger. Mirrors `env_logger::try_init`: `Err` if some other
109/// logger is already installed, and the caller is free to ignore that.
110pub fn try_init() -> Result<(), log::SetLoggerError> {
111 let directives = parse(&std::env::var("RUST_LOG").unwrap_or_default());
112 let max = directives.iter().map(|d| d.level).max().unwrap_or(log::LevelFilter::Error);
113 log::set_boxed_logger(Box::new(StderrLogger { directives, max }))?;
114 log::set_max_level(max);
115 Ok(())
116}
117
118// BREP private tests: a8e719b8f199c66b