Skip to main content

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