tracing_web/
console_writer.rs

1use std::io::Write;
2
3use tracing_core::Level;
4use tracing_subscriber::fmt::MakeWriter;
5use wasm_bindgen::JsValue;
6use web_sys::console;
7
8/// **Discouraged** A [`MakeWriter`] emitting the written text to the [`console`].
9///
10/// The used log method is sensitive to the level the event is emitted with.
11///
12/// | Level     | Method           |
13/// |-----------|------------------|
14/// | TRACE     | console.debug    |
15/// | DEBUG     | console.debug    |
16/// | INFO      | console.info     |
17/// | WARN      | console.warn     |
18/// | ERROR     | console.error    |
19/// | other     | console.log      |
20///
21/// ### Note
22///
23/// Since version `0.1.3`, you should prefer the alternative, more powerful [`MakeWebConsoleWriter`].
24// For now, I have decided against deprecating this. While I do intend to deprecate or even remove it in 0.2, a warning is probably too picky on downstream developers.
25// #[deprecated(
26//     since = "0.1.3",
27//     note = "use `MakeWebConsoleWriter` instead, which provides a more future proof API"
28// )]
29pub struct MakeConsoleWriter;
30
31/// A [`MakeWriter`] emitting the written text to the [`console`].
32///
33/// The used log method is sensitive to the level the event is emitted with.
34///
35/// | Level     | Method           |
36/// |-----------|------------------|
37/// | TRACE     | console.debug    |
38/// | DEBUG     | console.debug    |
39/// | INFO      | console.info     |
40/// | WARN      | console.warn     |
41/// | ERROR     | console.error    |
42/// | other     | console.log      |
43pub struct MakeWebConsoleWriter {
44    use_pretty_label: bool,
45}
46
47impl Default for MakeWebConsoleWriter {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl MakeWebConsoleWriter {
54    /// Create a default console writer, i.e. no level annotation is shown when logging a message.
55    pub fn new() -> Self {
56        Self {
57            use_pretty_label: false,
58        }
59    }
60    /// Enables an additional label for the log level to be shown.
61    ///
62    /// It is recommended that you also use [`Layer::with_level(false)`] if you use this option, to avoid the event level being shown twice.
63    ///
64    /// [`Layer::with_level(false)`]: tracing_subscriber::fmt::Layer::with_level
65    pub fn with_pretty_level(mut self) -> Self {
66        self.use_pretty_label = true;
67        self
68    }
69}
70
71type LogDispatcher = fn(Level, &str);
72
73/// Concrete [`std::io::Write`] implementation returned by [`MakeConsoleWriter`] and [`MakeWebConsoleWriter`].
74pub struct ConsoleWriter {
75    buffer: Vec<u8>,
76    level: Level,
77    log: LogDispatcher,
78}
79
80impl Write for ConsoleWriter {
81    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
82        self.buffer.write(buf)
83    }
84
85    fn flush(&mut self) -> std::io::Result<()> {
86        // Nothing to-do here, we instead flush on drop
87        Ok(())
88    }
89}
90
91impl Drop for ConsoleWriter {
92    fn drop(&mut self) {
93        // TODO: it's rather pointless to decoded to utf-8 here,
94        //  just to re-encode as utf-16 when crossing wasm-bindgen boundaries
95        // we could use TextDecoder directly to produce a
96        let message = String::from_utf8_lossy(&self.buffer);
97        (self.log)(self.level, message.as_ref())
98    }
99}
100
101// Now, for the implementation details. For each supported log level, we have a dummy type with a trait impl providing
102// the (1) "simple" logging via the console.* methods, just forwarding the message and (2) "pretty" logging which passes
103// additional CSS along. The trait makes it convenient to instantiate a generic parameter below to obtain the needed
104// fn pointers for the applicable dispatcher.
105
106trait LogImpl {
107    fn log_simple(level: Level, msg: &str);
108    fn log_pretty(level: Level, msg: &str);
109}
110
111const MESSAGE_STYLE: &str = "background: inherit; color: inherit;";
112macro_rules! make_log_impl {
113    ($T:ident {
114        simple: $s:expr,
115        pretty: {
116            log: $p:expr, fmt: $f:expr, label_style: $l:expr $(,)?
117        } $(,)?
118    }) => {
119        struct $T;
120        impl LogImpl for $T {
121            #[inline(always)]
122            fn log_simple(_level: Level, msg: &str) {
123                $s(&JsValue::from(msg));
124            }
125            #[inline(always)]
126            fn log_pretty(_level: Level, msg: &str) {
127                let fmt = JsValue::from(wasm_bindgen::intern($f));
128                let label_style = JsValue::from(wasm_bindgen::intern($l));
129                let msg_style = JsValue::from(wasm_bindgen::intern(MESSAGE_STYLE));
130                $p(&fmt, &label_style, &msg_style, &JsValue::from(msg));
131            }
132        }
133    };
134}
135
136// Even though console.trace exists and generates stack traces, it logs with level: info, so leads to verbose logs, so log with debug
137make_log_impl!(LogLevelTrace { simple: console::debug_1, pretty: { log: console::debug_4, fmt: "%cTRACE%c %s", label_style: "color: white; font-weight: bold; padding: 0 5px; background: #75507B;" } });
138make_log_impl!(LogLevelDebug { simple: console::debug_1, pretty: { log: console::debug_4, fmt: "%cDEBUG%c %s", label_style: "color: white; font-weight: bold; padding: 0 5px; background: #3465A4;" } });
139make_log_impl!(LogLevelInfo  { simple: console::info_1,  pretty: { log: console::info_4,  fmt: "%c INFO%c %s", label_style: "color: white; font-weight: bold; padding: 0 5px; background: #4E9A06;" } });
140make_log_impl!(LogLevelWarn  { simple: console::warn_1,  pretty: { log: console::warn_4,  fmt: "%c WARN%c %s", label_style: "color: white; font-weight: bold; padding: 0 5px; background: #C4A000;" } });
141make_log_impl!(LogLevelError { simple: console::error_1, pretty: { log: console::error_4, fmt: "%cERROR%c %s", label_style: "color: white; font-weight: bold; padding: 0 5px; background: #CC0000;" } });
142
143// This impl serves as a fallback for potential additions to tracing's levels that I can't forsee. It should not be reachable in code as of the time of writing, but might be in future additions to tracing.
144struct LogLevelFallback;
145impl LogImpl for LogLevelFallback {
146    #[inline(always)]
147    fn log_simple(_level: Level, msg: &str) {
148        console::log_1(&JsValue::from(msg))
149    }
150
151    #[inline(always)]
152    fn log_pretty(level: Level, msg: &str) {
153        let fmt = JsValue::from(wasm_bindgen::intern("%c%s%c %s"));
154        let label_level = JsValue::from(format!("{}", level));
155        // Note: `text-transform` might not have perfect browser support, but is available in at least Firefox and Chrome at the time of writing
156        let label_style = JsValue::from(wasm_bindgen::intern(
157            "color: white; font-weight: bold; padding: 0 5px; background: #424242; text-transform: uppercase;",
158        ));
159        let msg_style = JsValue::from(wasm_bindgen::intern(MESSAGE_STYLE));
160        let msg = JsValue::from(msg);
161        console::log_5(&fmt, &label_style, &label_level, &msg_style, &msg)
162    }
163}
164
165// An additional trait (implemented again by dummy types) makes it convenient to select the correct
166// logging implementation. We can then generalize in `select_dispatcher`.
167
168trait LogImplStyle {
169    fn get_dispatch<L: LogImpl>(&self) -> LogDispatcher;
170}
171struct SimpleStyle;
172impl LogImplStyle for SimpleStyle {
173    #[inline(always)]
174    fn get_dispatch<L: LogImpl>(&self) -> LogDispatcher {
175        L::log_simple
176    }
177}
178struct PrettyStyle;
179impl LogImplStyle for PrettyStyle {
180    #[inline(always)]
181    fn get_dispatch<L: LogImpl>(&self) -> LogDispatcher {
182        L::log_pretty
183    }
184}
185
186fn select_dispatcher(style: impl LogImplStyle, level: Level) -> LogDispatcher {
187    if level == Level::TRACE {
188        style.get_dispatch::<LogLevelTrace>()
189    } else if level == Level::DEBUG {
190        style.get_dispatch::<LogLevelDebug>()
191    } else if level == Level::INFO {
192        style.get_dispatch::<LogLevelInfo>()
193    } else if level == Level::WARN {
194        style.get_dispatch::<LogLevelWarn>()
195    } else if level == Level::ERROR {
196        style.get_dispatch::<LogLevelError>()
197    } else {
198        style.get_dispatch::<LogLevelFallback>()
199    }
200}
201
202impl MakeConsoleWriter {
203    // "upgrade" to a MakeWebConsoleWriter, mainly to unify code paths.
204    fn upgrade(&self) -> MakeWebConsoleWriter {
205        MakeWebConsoleWriter {
206            use_pretty_label: false,
207        }
208    }
209}
210impl<'a> MakeWriter<'a> for MakeConsoleWriter {
211    type Writer = ConsoleWriter;
212
213    fn make_writer(&'a self) -> Self::Writer {
214        self.upgrade().make_writer()
215    }
216
217    fn make_writer_for(&'a self, meta: &tracing_core::Metadata<'_>) -> Self::Writer {
218        self.upgrade().make_writer_for(meta)
219    }
220}
221
222impl<'a> MakeWriter<'a> for MakeWebConsoleWriter {
223    type Writer = ConsoleWriter;
224
225    fn make_writer(&'a self) -> Self::Writer {
226        ConsoleWriter {
227            buffer: vec![],
228            level: Level::TRACE, // if no level is known, assume the most detailed
229            log: if self.use_pretty_label {
230                PrettyStyle.get_dispatch::<LogLevelFallback>()
231            } else {
232                SimpleStyle.get_dispatch::<LogLevelFallback>()
233            },
234        }
235    }
236
237    fn make_writer_for(&'a self, meta: &tracing_core::Metadata<'_>) -> Self::Writer {
238        let level = *meta.level();
239        let log_fn = if self.use_pretty_label {
240            select_dispatcher(PrettyStyle, level)
241        } else {
242            select_dispatcher(SimpleStyle, level)
243        };
244        ConsoleWriter {
245            buffer: vec![],
246            level,
247            log: log_fn,
248        }
249    }
250}