Skip to main content

eframe/web/
web_logger.rs

1/// Implements [`log::Log`] to log messages to `console.log`, `console.warn`, etc.
2pub struct WebLogger {
3    filter: log::LevelFilter,
4}
5
6impl WebLogger {
7    /// Install a new `WebLogger`, piping all [`log`] events to the web console.
8    pub fn init(filter: log::LevelFilter) -> Result<(), log::SetLoggerError> {
9        log::set_max_level(filter);
10        log::set_boxed_logger(Box::new(Self::new(filter)))
11    }
12
13    /// Create a new [`WebLogger`] with the given filter,
14    /// but don't install it.
15    pub fn new(filter: log::LevelFilter) -> Self {
16        Self { filter }
17    }
18}
19
20impl log::Log for WebLogger {
21    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
22        /// Never log anything less serious than a `INFO` from these crates.
23        const CRATES_AT_INFO_LEVEL: &[&str] = &[
24            // wgpu crates spam a lot on debug level, which is really annoying
25            "naga",
26            "wgpu_core",
27            "wgpu_hal",
28        ];
29
30        if CRATES_AT_INFO_LEVEL
31            .iter()
32            .any(|crate_name| metadata.target().starts_with(crate_name))
33        {
34            return metadata.level() <= log::LevelFilter::Info;
35        }
36
37        metadata.level() <= self.filter
38    }
39
40    fn log(&self, record: &log::Record<'_>) {
41        #![expect(clippy::match_same_arms)]
42
43        if !self.enabled(record.metadata()) {
44            return;
45        }
46
47        let msg = if let (Some(file), Some(line)) = (record.file(), record.line()) {
48            let file = shorten_file_path(file);
49            format!("[{}] {file}:{line}: {}", record.target(), record.args())
50        } else {
51            format!("[{}] {}", record.target(), record.args())
52        };
53
54        match record.level() {
55            // NOTE: the `console::trace` includes a stack trace, which is super-noisy.
56            log::Level::Trace => console::debug(&msg),
57
58            log::Level::Debug => console::debug(&msg),
59            log::Level::Info => console::info(&msg),
60            log::Level::Warn => console::warn(&msg),
61
62            // Using console.error causes crashes for unknown reason
63            // https://github.com/emilk/egui/pull/2961
64            // log::Level::Error => console::error(&msg),
65            log::Level::Error => console::warn(&format!("ERROR: {msg}")),
66        }
67    }
68
69    fn flush(&self) {}
70}
71
72/// js-bindings for console.log, console.warn, etc
73mod console {
74    use wasm_bindgen::prelude::*;
75
76    #[wasm_bindgen]
77    extern "C" {
78        /// `console.trace`
79        #[wasm_bindgen(js_namespace = console)]
80        pub fn trace(s: &str);
81
82        /// `console.debug`
83        #[wasm_bindgen(js_namespace = console)]
84        pub fn debug(s: &str);
85
86        /// `console.info`
87        #[wasm_bindgen(js_namespace = console)]
88        pub fn info(s: &str);
89
90        /// `console.warn`
91        #[wasm_bindgen(js_namespace = console)]
92        pub fn warn(s: &str);
93
94        // Using console.error causes crashes for unknown reason
95        // https://github.com/emilk/egui/pull/2961
96        // /// `console.error`
97        // #[wasm_bindgen(js_namespace = console)]
98        // pub fn error(s: &str);
99    }
100}
101
102/// Shorten a path to a Rust source file.
103///
104/// Example input:
105/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs`
106/// * `crates/rerun/src/main.rs`
107/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs`
108///
109/// Example output:
110/// * `tokio-1.24.1/src/runtime/runtime.rs`
111/// * `rerun/src/main.rs`
112/// * `core/src/ops/function.rs`
113#[allow(clippy::allow_attributes, dead_code)] // only used on web and in tests
114fn shorten_file_path(file_path: &str) -> &str {
115    if let Some(i) = file_path.rfind("/src/") {
116        if let Some(prev_slash) = file_path[..i].rfind('/') {
117            &file_path[prev_slash + 1..]
118        } else {
119            file_path
120        }
121    } else {
122        file_path
123    }
124}
125
126#[test]
127fn test_shorten_file_path() {
128    for (before, after) in [
129        (
130            "/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
131            "tokio-1.24.1/src/runtime/runtime.rs",
132        ),
133        ("crates/rerun/src/main.rs", "rerun/src/main.rs"),
134        (
135            "/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
136            "core/src/ops/function.rs",
137        ),
138        ("/weird/path/file.rs", "/weird/path/file.rs"),
139    ] {
140        assert_eq!(shorten_file_path(before), after);
141    }
142}