whiskers.git / backend / worker / src / logging.rs
1//! The Worker's logger: the `log` facade (which `whiskers-service` and `whiskers-core` already use)
2//! written to the runtime's console, at the console method that carries its level, so `wrangler dev`,
3//! Cloudflare's log stream and `celld dev --logs` all show the level the code chose.
4//!
5//! The level is the `WHISKERS_LOG` setting, spelled as everywhere else in Whiskers (`error`, `warn`,
6//! `info` (default), `debug`, `trace`, `off`; `parse_level` below is a copy of the core's, which is
7//! compiled out on wasm, and a native test holds the two together). Nothing here knows what a
8//! record contains: the convention that no record carries content is the callers', and
9//! `line` is the one place a record becomes text, so it is the one place to look.
10
11use log::{Level, LevelFilter, Log, Metadata, Record};
12
13/// What a record looks like in the log: its level, where it came from and its message.
14pub fn line(level: Level, target: &str, message: &std::fmt::Arguments<'_>) -> String {
15    format!("{level} {target}: {message}")
16}
17
18/// Reads a level name as `WHISKERS_LOG` spells it. Unknown names are `None`.
19fn parse_level(s: &str) -> Option<LevelFilter> {
20    match s.trim().to_ascii_lowercase().as_str() {
21        "off" | "none" => Some(LevelFilter::Off),
22        "error" => Some(LevelFilter::Error),
23        "warn" | "warning" => Some(LevelFilter::Warn),
24        "info" => Some(LevelFilter::Info),
25        "debug" => Some(LevelFilter::Debug),
26        "trace" => Some(LevelFilter::Trace),
27        _ => None,
28    }
29}
30
31/// The level named by the setting, or `info` for none and for a name that is not a level (an
32/// operator's typo must not silence the log).
33pub fn level_of(setting: Option<&str>) -> LevelFilter {
34    setting.and_then(parse_level).unwrap_or(LevelFilter::Info)
35}
36
37struct Console;
38
39impl Log for Console {
40    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
41        metadata.level() <= log::max_level()
42    }
43
44    fn log(&self, record: &Record<'_>) {
45        if !self.enabled(record.metadata()) {
46            return;
47        }
48        emit(record.level(), &line(record.level(), record.target(), record.args()));
49    }
50
51    fn flush(&self) {}
52}
53
54#[cfg(target_arch = "wasm32")]
55fn emit(level: Level, text: &str) {
56    match level {
57        Level::Error => worker::console_error!("{text}"),
58        Level::Warn => worker::console_warn!("{text}"),
59        Level::Info => worker::console_log!("{text}"),
60        Level::Debug | Level::Trace => worker::console_debug!("{text}"),
61    }
62}
63
64/// A native build (the tests) has no console.
65#[cfg(not(target_arch = "wasm32"))]
66fn emit(_level: Level, _text: &str) {}
67
68static CONSOLE: Console = Console;
69
70/// Installs the logger once per isolate; later calls change nothing.
71pub fn install(env: &worker::Env) {
72    let level = level_of(env.var("WHISKERS_LOG").ok().map(|v| v.to_string()).as_deref());
73    if log::set_logger(&CONSOLE).is_ok() {
74        log::set_max_level(level);
75        log::debug!("logging installed at level {level}");
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn the_setting_picks_the_level_and_a_typo_does_not_silence_the_log() {
85        assert_eq!(level_of(None), LevelFilter::Info);
86        assert_eq!(level_of(Some("debug")), LevelFilter::Debug);
87        assert_eq!(level_of(Some("OFF")), LevelFilter::Off);
88        assert_eq!(level_of(Some("verbose")), LevelFilter::Info);
89    }
90
91    #[test]
92    fn the_spelling_is_the_cores() {
93        for name in ["off", "none", "error", "warn", "warning", "info", "debug", "trace", " Debug ", "loud", ""] {
94            assert_eq!(parse_level(name), whiskers_core::logging::parse_level(name), "{name:?}");
95        }
96    }
97
98    #[test]
99    fn a_line_is_a_level_a_place_and_a_message() {
100        assert_eq!(line(Level::Warn, "whiskers_service", &format_args!("{} facts", 3)), "WARN whiskers_service: 3 facts");
101    }
102}