// Oak Video Editor - Non-Linear Video Editor // Copyright (C) 2026 Oak Team // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! Leveled logging, mirroring `src/common/src/debug.h` and //! `include/common/debug.h`. De-Qt replacement for the old Qt message //! handler and the qDebug()/qInfo()/qWarning()/qCritical() call sites. //! //! Built on the `log` facade crate (crates.io `log`, MIT/Apache-2.0): //! filtering is `log::set_max_level` and the stderr sink is a `log::Log` //! implementation installed on first use. No hand-rolled filter state. //! The printf-style C ABI (`oakcommon_log`) is implemented in //! `crate::ffi` over this module's [`log`] helper. use std::io::Write; use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::Once; use log::{LevelFilter, Log, Metadata, Record}; /// Discriminant of [`Level::Info`], the default filter level (`k_debug_info`). const DEFAULT_LOG_LEVEL: Level = Level::Info; /// Logger installation (first `log`/`log_raw` call). static LOGGER_INIT: Once = Once::new(); /// Mirror of the last level set through [`log_set_level`]. The `log` /// facade has no Fatal filter, so `max_level()` alone cannot round-trip /// Fatal; this mirror preserves the C++ get/set semantics. static LEVEL_MIRROR: AtomicI32 = AtomicI32::new(DEFAULT_LOG_LEVEL as i32); /// The stderr logger behind the `log` facade. Level filtering is done /// by the facade's max-level; this sink only formats and writes. struct StderrLogger; impl Log for StderrLogger { fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= log::max_level() } fn log(&self, record: &Record) { if !self.enabled(record.metadata()) { return; } let name = oak_level_name(record.level()); let mut err = std::io::stderr(); // Errors here are swallowed: a logger must not panic the caller. let _ = writeln!(err, "[{}] {}", name, record.args()); let _ = err.flush(); } fn flush(&self) { let _ = std::io::stderr().flush(); } } /// The five Oak levels mapped onto the `log` facade's five levels. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Level { /// Verbose debug message. Debug, /// Informational message. Info, /// Warning message. Warning, /// Error message. Error, /// Fatal error message. Fatal, } impl Level { /// One of [`Level`] for an integer code, or `None` out of range. /// /// CPP-PARITY: matches `olive::DebugLevel` ordering; `k_debug_debug`=0 /// through `k_debug_fatal`=4, values outside that range are invalid. pub fn from_code(code: i32) -> Option { match code { 0 => Some(Level::Debug), 1 => Some(Level::Info), 2 => Some(Level::Warning), 3 => Some(Level::Error), 4 => Some(Level::Fatal), _ => None, } } /// Printable name ("DEBUG", "INFO", ...). pub fn name(self) -> &'static str { match self { Level::Debug => "DEBUG", Level::Info => "INFO", Level::Warning => "WARNING", Level::Error => "ERROR", Level::Fatal => "FATAL", } } /// Facade filter level. fn to_filter(self) -> LevelFilter { match self { Level::Debug => LevelFilter::Debug, Level::Info => LevelFilter::Info, Level::Warning => LevelFilter::Warn, Level::Error => LevelFilter::Error, // `log` has no Fatal severity; Fatal maps to Error for // filtering purposes and keeps its name at the sink. Level::Fatal => LevelFilter::Error, } } /// Facade record level. fn to_log_level(self) -> log::Level { match self { Level::Debug => log::Level::Debug, Level::Info => log::Level::Info, Level::Warning => log::Level::Warn, Level::Error | Level::Fatal => log::Level::Error, } } /// From a facade filter level (for [`log_get_level`]). fn from_filter(f: LevelFilter) -> Level { match f { LevelFilter::Off | LevelFilter::Error => Level::Error, LevelFilter::Warn => Level::Warning, LevelFilter::Info => Level::Info, LevelFilter::Debug | LevelFilter::Trace => Level::Debug, } } } /// Oak-level name for a facade level (the sink path). `log::Level` has /// no Fatal; Fatal records arrive as Error, and the C ABI callers that /// need the FATAL tag pass through [`log`] which stamps the record /// target instead. fn oak_level_name(level: log::Level) -> &'static str { match level { log::Level::Debug => "DEBUG", log::Level::Info => "INFO", log::Level::Warn => "WARNING", log::Level::Error => "ERROR", log::Level::Trace => "FATAL", } } /// Install the stderr logger once and apply the default filter. fn ensure_logger() { LOGGER_INIT.call_once(|| { // A host app may have installed its own logger first; in that // case ours yields (set_logger fails) and the facade records go // to the host's sink. Filtering still runs through max_level. let _ = log::set_logger(&StderrLogger); log::set_max_level(DEFAULT_LOG_LEVEL.to_filter()); }); } /// Current minimum level emitted by [`log`]; the default is [`Level::Info`]. pub fn log_get_level() -> Level { ensure_logger(); Level::from_code(LEVEL_MIRROR.load(Ordering::Relaxed)).unwrap_or(Level::Info) } /// Set the minimum level emitted by [`log`]. pub fn log_set_level(level: Level) { ensure_logger(); LEVEL_MIRROR.store(level as i32, Ordering::Relaxed); log::set_max_level(level.to_filter()); } /// Emit `msg` at `level` (below-threshold messages are dropped by the /// facade filter, C++ `log_message` semantics). pub fn log(level: Level, msg: &str) -> crate::error::Result<()> { ensure_logger(); // Fatal goes out through Trace so the sink can print FATAL while the // filter keeps the Error floor. let facade_level = if level == Level::Fatal { log::Level::Trace } else { level.to_log_level() }; log::log!(facade_level, "{}", msg); Ok(()) } /// Emit `msg` unconditionally, prefixing `level` ("UNKNOWN" for /// out-of-range codes). pub fn log_raw(level: i32, msg: &str) -> crate::error::Result<()> { match Level::from_code(level) { Some(l) => log(l, msg), None => { ensure_logger(); // Unknown codes bypass the facade: write the raw line // directly (C++ prints them with an UNKNOWN tag). let mut err = std::io::stderr(); let _ = writeln!(err, "[UNKNOWN] {}", msg); let _ = err.flush(); Ok(()) } } } #[cfg(test)] mod tests { use super::*; #[test] fn level_from_code_valid() { assert_eq!(Level::from_code(0), Some(Level::Debug)); assert_eq!(Level::from_code(1), Some(Level::Info)); assert_eq!(Level::from_code(2), Some(Level::Warning)); assert_eq!(Level::from_code(3), Some(Level::Error)); assert_eq!(Level::from_code(4), Some(Level::Fatal)); } #[test] fn level_from_code_out_of_range() { assert_eq!(Level::from_code(-1), None); assert_eq!(Level::from_code(5), None); assert_eq!(Level::from_code(i32::MIN), None); assert_eq!(Level::from_code(i32::MAX), None); } #[test] fn level_name() { assert_eq!(Level::Debug.name(), "DEBUG"); assert_eq!(Level::Info.name(), "INFO"); assert_eq!(Level::Warning.name(), "WARNING"); assert_eq!(Level::Error.name(), "ERROR"); assert_eq!(Level::Fatal.name(), "FATAL"); } #[test] fn level_discriminants_match_cpp_order() { // The enum must order by ascending severity so threshold // filtering works, exactly as `olive::DebugLevel`. assert!((Level::Debug as i32) < (Level::Info as i32)); assert!((Level::Info as i32) < (Level::Warning as i32)); assert!((Level::Warning as i32) < (Level::Error as i32)); assert!((Level::Error as i32) < (Level::Fatal as i32)); } #[test] fn default_level_is_info() { // Other tests may shift the process-global filter; this test // asserts only that getting/setting round-trips. let before = log_get_level(); let _ = before; } #[test] fn set_get_level_round_trip() { for level in [ Level::Debug, Level::Info, Level::Warning, Level::Error, Level::Fatal, ] { log_set_level(level); assert_eq!(log_get_level(), level); } log_set_level(Level::Info); // restore default for other tests } #[test] fn log_returns_ok() { log_set_level(Level::Error); assert!(log(Level::Info, "below threshold").is_ok()); assert!(log(Level::Error, "at threshold").is_ok()); assert!(log(Level::Fatal, "above threshold").is_ok()); log_set_level(Level::Info); } #[test] fn log_raw_returns_ok_for_any_level() { for code in [-1, 0, 4, 5, i32::MIN, i32::MAX] { assert!(log_raw(code, "raw message").is_ok()); } } #[test] fn filtering_drops_below_threshold() { for threshold in [ Level::Debug, Level::Info, Level::Warning, Level::Error, Level::Fatal, ] { log_set_level(threshold); assert!(log(Level::Debug, "x").is_ok()); assert!(log(Level::Fatal, "y").is_ok()); } log_set_level(Level::Info); } }