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}