//! `log` records on stderr for the native desktop app and CLI (#168). The web build installs //! eframe's `WebLogger` instead. //! //! Warnings and errors are shown by default. `LIGHTCRAFT_LOG` or `RUST_LOG` picks another level //! (`off`, `error`, `warn`, `info`, `debug`, `trace`); `LIGHTCRAFT_LOG` wins when both are set. //! `RUST_LOG` also accepts per-target directives (`lightcraft=debug,wgpu=warn`): a bare level //! applies to everything, and the most verbose `lightcraft…=level` directive sets LightCraft's. //! Below warnings only LightCraft's own records are shown, so `info` and `debug` aren't drowned //! out by the GPU and windowing libraries. use log::{Level, LevelFilter, Metadata, Record}; /// Records shown at a level, written to stderr as `: LEVEL target: message`. pub struct StderrLog { prefix: &'static str, level: LevelFilter, } impl StderrLog { pub fn new(prefix: &'static str, level: LevelFilter) -> Self { Self { prefix, level } } } impl log::Log for StderrLog { fn enabled(&self, m: &Metadata) -> bool { m.level() <= self.level && (m.level() <= Level::Warn || m.target().starts_with("lightcraft")) } fn log(&self, r: &Record) { if self.enabled(r.metadata()) { eprintln!("{}: {} {}: {}", self.prefix, r.level(), r.target(), r.args()); } } fn flush(&self) {} } fn parse_level(s: &str) -> Option { match s.trim().to_ascii_lowercase().as_str() { "off" | "none" => Some(LevelFilter::Off), "error" => Some(LevelFilter::Error), "warn" | "warning" => Some(LevelFilter::Warn), "info" => Some(LevelFilter::Info), "debug" => Some(LevelFilter::Debug), "trace" => Some(LevelFilter::Trace), _ => None, } } /// The level that `LIGHTCRAFT_LOG` / `RUST_LOG` ask for; warnings when neither names one. pub fn level_from(lightcraft_log: Option<&str>, rust_log: Option<&str>) -> LevelFilter { if let Some(l) = lightcraft_log.and_then(parse_level) { return l; } let Some(rust_log) = rust_log else { return LevelFilter::Warn }; let mut bare = None; let mut ours: Option = None; for directive in rust_log.split(',') { match directive.split_once('=') { None => bare = parse_level(directive).or(bare), Some((target, level)) if target.trim().starts_with("lightcraft") => { if let Some(l) = parse_level(level) { ours = Some(ours.map_or(l, |o| o.max(l))); } } Some(_) => {} } } ours.or(bare).unwrap_or(LevelFilter::Warn) } /// Install the stderr logger at the level the environment asks for. Does nothing when a logger is /// already installed. pub fn install(prefix: &'static str) { let level = level_from(std::env::var("LIGHTCRAFT_LOG").ok().as_deref(), std::env::var("RUST_LOG").ok().as_deref()); static LOGGER: std::sync::OnceLock = std::sync::OnceLock::new(); if log::set_logger(LOGGER.get_or_init(|| StderrLog::new(prefix, level))).is_ok() { log::set_max_level(level); } } #[cfg(test)] mod tests { use super::*; use log::Log; #[test] fn warnings_by_default_and_either_variable_picks_the_level() { assert_eq!(level_from(None, None), LevelFilter::Warn); assert_eq!(level_from(None, Some("")), LevelFilter::Warn); assert_eq!(level_from(None, Some("warn")), LevelFilter::Warn); assert_eq!(level_from(None, Some("TRACE")), LevelFilter::Trace); assert_eq!(level_from(None, Some("off")), LevelFilter::Off); assert_eq!(level_from(Some("info"), None), LevelFilter::Info); assert_eq!(level_from(Some("error"), Some("debug")), LevelFilter::Error, "LIGHTCRAFT_LOG wins"); assert_eq!(level_from(Some("nonsense"), Some("debug")), LevelFilter::Debug, "an unknown LIGHTCRAFT_LOG is ignored"); } #[test] fn rust_log_directives_set_lightcraft_s_level() { assert_eq!(level_from(None, Some("lightcraft=debug,wgpu=warn")), LevelFilter::Debug); assert_eq!(level_from(None, Some("wgpu_core=trace")), LevelFilter::Warn, "other crates' directives don't apply"); assert_eq!(level_from(None, Some("error,lightcraft_gpu=info")), LevelFilter::Info); assert_eq!(level_from(None, Some("lightcraft_gpu=info,lightcraft_engine=trace")), LevelFilter::Trace); assert_eq!(level_from(None, Some("info,naga=off")), LevelFilter::Info); } #[test] fn only_lightcraft_records_below_warnings() { let log = StderrLog::new("test", LevelFilter::Debug); let meta = |level, target| Metadata::builder().level(level).target(target).build(); assert!(log.enabled(&meta(Level::Warn, "wgpu_core::device"))); assert!(log.enabled(&meta(Level::Debug, "lightcraft_gpu::ctx"))); assert!(!log.enabled(&meta(Level::Info, "wgpu_core::device"))); assert!(!log.enabled(&meta(Level::Trace, "lightcraft_gpu::ctx"))); let quiet = StderrLog::new("test", LevelFilter::Off); assert!(!quiet.enabled(&meta(Level::Error, "lightcraft_engine"))); } }