From 555663bd3dfc4543d1d75bfd380acffd896cc560 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:32:59 -0600 Subject: [PATCH] refactor: split logger into focused modules --- src/logger/global.rs | 71 ++++++++++ src/logger/level.rs | 33 +++++ src/logger/macros.rs | 83 +++++++++++ src/logger/mod.rs | 321 +++++-------------------------------------- src/logger/record.rs | 45 ++++++ src/logger/sink.rs | 60 ++++++++ src/logger/tests.rs | 19 +++ 7 files changed, 342 insertions(+), 290 deletions(-) create mode 100644 src/logger/global.rs create mode 100644 src/logger/level.rs create mode 100644 src/logger/macros.rs create mode 100644 src/logger/record.rs create mode 100644 src/logger/sink.rs create mode 100644 src/logger/tests.rs diff --git a/src/logger/global.rs b/src/logger/global.rs new file mode 100644 index 0000000..3324101 --- /dev/null +++ b/src/logger/global.rs @@ -0,0 +1,71 @@ +use core::cell::UnsafeCell; + +use super::sink::NullLogger; +use crate::logger::{LogLevel, Logger, Record}; + +struct LoggerCell(UnsafeCell<&'static dyn Logger>); + +impl LoggerCell { + const fn new(logger: &'static dyn Logger) -> Self { + Self(UnsafeCell::new(logger)) + } + + fn set(&self, logger: &'static dyn Logger) { + // Rationale: the logger is installed during single-threaded startup. + // Keeping the unsafety inside this tiny cell is easier to audit than + // exposing `static mut` references throughout the module. + unsafe { + *self.0.get() = logger; + } + } + + fn get(&self) -> &'static dyn Logger { + // Rationale: after startup the stored reference is treated as immutable, + // so reading the copied trait object reference is safe under the module + // contract documented on `set_logger`. + unsafe { *self.0.get() } + } +} + +// SAFETY: access is funneled through the startup-only `set` contract and +// read-only `get` path above. `Logger: Sync` ensures sharing the sink is valid. +unsafe impl Sync for LoggerCell {} + +static GLOBAL_LOGGER: LoggerCell = LoggerCell::new(&NullLogger); + +/// Installs the global logger used by the logging macros. +/// +/// Call this once during startup, before any concurrent execution begins. +/// Replacing the logger later would require external synchronization and is not +/// supported by this module's contract. +/// +/// # Examples +/// +/// ```rust,no_run +/// use unshell::logger::{Logger, Record, set_logger}; +/// +/// struct MyLogger; +/// +/// impl Logger for MyLogger { +/// fn log(&self, _record: &Record<'_>) {} +/// } +/// +/// static LOGGER: MyLogger = MyLogger; +/// set_logger(&LOGGER); +/// ``` +pub fn set_logger(logger: &'static dyn Logger) { + GLOBAL_LOGGER.set(logger); +} + +/// Returns the currently installed global logger. +#[must_use] +pub fn global_logger() -> &'static dyn Logger { + GLOBAL_LOGGER.get() +} + +/// Sends a single record through the installed global logger. +/// +/// Most code should prefer the exported logging macros. +pub fn log(level: LogLevel, message: &str, file: Option<&'static str>, line: Option) { + global_logger().log(&Record::new(level, message, file, line)); +} diff --git a/src/logger/level.rs b/src/logger/level.rs new file mode 100644 index 0000000..2368d42 --- /dev/null +++ b/src/logger/level.rs @@ -0,0 +1,33 @@ +/// Severity level carried by a [`crate::logger::Record`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum LogLevel { + /// Verbose diagnostic information. + Debug, + /// Normal operational messages. + Info, + /// Something unexpected happened but execution can continue. + Warn, + /// A serious error occurred. + Error, +} + +impl LogLevel { + /// Returns a short uppercase label suitable for log prefixes. + /// + /// # Examples + /// + /// ```rust + /// use unshell::logger::LogLevel; + /// + /// assert_eq!(LogLevel::Info.as_str(), "INFO"); + /// ``` + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Debug => "DEBUG", + Self::Info => "INFO", + Self::Warn => "WARN", + Self::Error => "ERROR", + } + } +} diff --git a/src/logger/macros.rs b/src/logger/macros.rs new file mode 100644 index 0000000..30b0b40 --- /dev/null +++ b/src/logger/macros.rs @@ -0,0 +1,83 @@ +/// Logs a message at [`crate::logger::LogLevel::Debug`]. +/// +/// # Examples +/// +/// ```rust +/// use unshell::debug; +/// +/// debug!("loop iteration {}", 42); +/// ``` +#[macro_export] +macro_rules! debug { + ($($arg:tt)*) => { + $crate::logger::log( + $crate::logger::LogLevel::Debug, + &format!($($arg)*), + Some(file!()), + Some(line!()), + ) + }; +} + +/// Logs a message at [`crate::logger::LogLevel::Info`]. +/// +/// # Examples +/// +/// ```rust +/// use unshell::info; +/// +/// info!("server started on port {}", 9000); +/// ``` +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => { + $crate::logger::log( + $crate::logger::LogLevel::Info, + &format!($($arg)*), + Some(file!()), + Some(line!()), + ) + }; +} + +/// Logs a message at [`crate::logger::LogLevel::Warn`]. +/// +/// # Examples +/// +/// ```rust +/// use unshell::warn; +/// +/// warn!("unexpected path: {}", "/unknown"); +/// ``` +#[macro_export] +macro_rules! warn { + ($($arg:tt)*) => { + $crate::logger::log( + $crate::logger::LogLevel::Warn, + &format!($($arg)*), + Some(file!()), + Some(line!()), + ) + }; +} + +/// Logs a message at [`crate::logger::LogLevel::Error`]. +/// +/// # Examples +/// +/// ```rust +/// use unshell::error; +/// +/// error!("connection failed: {}", "timeout"); +/// ``` +#[macro_export] +macro_rules! error { + ($($arg:tt)*) => { + $crate::logger::log( + $crate::logger::LogLevel::Error, + &format!($($arg)*), + Some(file!()), + Some(line!()), + ) + }; +} diff --git a/src/logger/mod.rs b/src/logger/mod.rs index 43cc909..18aa580 100644 --- a/src/logger/mod.rs +++ b/src/logger/mod.rs @@ -1,309 +1,50 @@ //! # Logger Module //! -//! A lightweight global logging system for core-only environments. +//! Lightweight logging primitives for `no_std` environments. //! -//! ## Usage +//! The logger stays intentionally small: +//! - call sites use exported `debug!`, `info!`, `warn!`, and `error!` macros +//! - sinks implement [`Logger`] +//! - startup code installs a single global logger with [`set_logger`] +//! +//! ## Quick start //! //! ```rust -//! use unshell::{info, warn, error}; -//! use unshell::logger::{Logger, Record}; +//! use unshell::{error, info, warn}; +//! use unshell::logger::{Logger, Record, set_logger}; //! //! struct Sink; +//! //! impl Logger for Sink { -//! fn log(&self, _record: &Record<'_>) {} -//! } -//! -//! static LOGGER: Sink = Sink; -//! unshell::logger::set_logger(&LOGGER); -//! -//! info!("Starting up"); -//! warn!("Something is off"); -//! error!("Critical failure"); -//! ``` -//! -//! ## Installing a logger -//! -//! Call [`set_logger`] with any type that implements [`Logger`]: -//! -//! ```rust -//! use unshell::logger::{Logger, LogLevel, Record, set_logger}; -//! -//! struct MemoryLogger { -//! min_level: LogLevel, -//! } -//! -//! impl Logger for MemoryLogger { //! fn log(&self, record: &Record<'_>) { -//! if record.level < self.min_level { -//! return; -//! } //! let _ = record; //! } //! } //! -//! static MY_LOGGER: MemoryLogger = MemoryLogger { -//! min_level: LogLevel::Info, -//! }; -//! set_logger(&MY_LOGGER); +//! static LOGGER: Sink = Sink; +//! set_logger(&LOGGER); +//! +//! info!("starting up"); +//! warn!("slow path engaged"); +//! error!("critical failure"); //! ``` //! -//! ## Thread safety +//! ## Design notes //! -//! The global logger pointer is set **once at startup**, before any threads -//! are spawned. After that, it is only read (never written). This is safe -//! because: -//! -//! 1. The payload is single-threaded. -//! 2. Integrators install the logger before concurrent execution begins. -//! -//! If you need to change the logger after threads start, synchronise access -//! with a `Mutex` or an atomic pointer in your logger implementation. +//! The global sink is installed once at startup and then treated as immutable. +//! That contract lets the module stay `no_std` while still providing a simple +//! global logging API. -// --------------------------------------------------------------------------- -// Log levels -// --------------------------------------------------------------------------- +mod global; +mod level; +mod macros; +mod record; +mod sink; -/// The severity level of a log record. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum LogLevel { - /// Verbose diagnostic information. - Debug, - /// Normal operational messages. - Info, - /// Something unexpected happened but execution can continue. - Warn, - /// A serious error occurred. - Error, -} +#[cfg(test)] +mod tests; -impl LogLevel { - /// Short uppercase label, suitable for log line prefixes. - /// - /// # Example - /// - /// ```rust - /// use unshell::logger::LogLevel; - /// assert_eq!(LogLevel::Info.as_str(), "INFO"); - /// ``` - #[must_use] - pub fn as_str(self) -> &'static str { - match self { - Self::Debug => "DEBUG", - Self::Info => "INFO", - Self::Warn => "WARN", - Self::Error => "ERROR", - } - } -} - -// --------------------------------------------------------------------------- -// Log record -// --------------------------------------------------------------------------- - -/// A single log entry passed to a [`Logger`]. -/// -/// Borrows from the call site to avoid heap allocation on the hot path. -pub struct Record<'a> { - /// Severity level. - pub level: LogLevel, - /// The log message. - pub message: &'a str, - /// Source file, if available (e.g. `file!()`). - pub file: Option<&'static str>, - /// Source line number, if available (e.g. `line!()`). - pub line: Option, -} - -// --------------------------------------------------------------------------- -// Logger trait -// --------------------------------------------------------------------------- - -/// A sink for log records. -/// -/// Implement this to direct log output wherever you want, such as a device -/// sink, a ring buffer, or a test collector. -pub trait Logger: Sync { - /// Receive and process a log record. - fn log(&self, record: &Record<'_>); -} - -// --------------------------------------------------------------------------- -// Global logger state -// --------------------------------------------------------------------------- - -/// The no-op logger used before any logger is installed. -struct NullLogger; -impl Logger for NullLogger { - fn log(&self, _record: &Record<'_>) {} -} - -/// The global logger pointer. -/// -/// Written once at startup via [`set_logger`], then only read. -/// # Safety -/// This is `static mut` to avoid a dependency on synchronisation primitives -/// in a core-only context. It is safe as long as `set_logger` is called before -/// any threads are spawned (see module-level docs). -static mut GLOBAL_LOGGER: &dyn Logger = &NullLogger; - -/// Install a new global logger. -/// -/// Must be called **before** spawning any threads. After this call, all -/// `info!`, `warn!`, `error!`, and `debug!` macros route to this logger. -/// -/// # Safety -/// -/// This function writes to a `static mut`. It is safe when called exactly -/// once at program startup before any other threads exist. -/// -/// # Example -/// -/// ```rust,no_run -/// use unshell::logger::{Logger, Record, set_logger}; -/// -/// static MY_LOGGER: MyLogger = MyLogger; -/// set_logger(&MY_LOGGER); -/// -/// # struct MyLogger; -/// # impl Logger for MyLogger { fn log(&self, _: &Record<'_>) {} } -/// ``` -pub fn set_logger(logger: &'static dyn Logger) { - // SAFETY: called once at startup before any threads are spawned. - #[allow(static_mut_refs)] - unsafe { - GLOBAL_LOGGER = logger; - } -} - -/// Return a reference to the currently installed logger. -/// -/// Used internally by the logging macros. -#[must_use] -pub fn global_logger() -> &'static dyn Logger { - // SAFETY: GLOBAL_LOGGER is only written once (at startup) and is - // read-only thereafter. No data race is possible. - #[allow(static_mut_refs)] - unsafe { - GLOBAL_LOGGER - } -} - -/// Log a record through the global logger. -/// -/// This is the low-level function called by the macros. Prefer using the -/// `info!`, `warn!`, `error!`, and `debug!` macros directly. -pub fn log(level: LogLevel, message: &str, file: Option<&'static str>, line: Option) { - global_logger().log(&Record { - level, - message, - file, - line, - }); -} - -// --------------------------------------------------------------------------- -// A minimal compatibility logger -// --------------------------------------------------------------------------- - -/// A simple filter-only logger. -/// -/// This provides a small compatibility surface for installations that want a -/// concrete logger type without defining their own sink yet. -pub struct CompatibilityLogger { - /// Minimum level to accept. Records below this level are discarded. - min_level: LogLevel, -} - -impl CompatibilityLogger { - /// Create a new `CompatibilityLogger` that accepts records at `min_level` - /// and above. - #[must_use] - pub const fn new(min_level: LogLevel) -> Self { - Self { min_level } - } -} - -impl Logger for CompatibilityLogger { - fn log(&self, record: &Record<'_>) { - if record.level < self.min_level { - return; - } - let _ = record; - } -} - -// --------------------------------------------------------------------------- -// Logging macros -// --------------------------------------------------------------------------- - -/// Log at [`LogLevel::Debug`] level. -/// -/// ```rust -/// use unshell::debug; -/// debug!("loop iteration {}", 42); -/// ``` -#[macro_export] -macro_rules! debug { - ($($arg:tt)*) => { - $crate::logger::log( - $crate::logger::LogLevel::Debug, - &format!($($arg)*), - Some(file!()), - Some(line!()), - ) - }; -} - -/// Log at [`LogLevel::Info`] level. -/// -/// ```rust -/// use unshell::info; -/// info!("server started on port {}", 9000); -/// ``` -#[macro_export] -macro_rules! info { - ($($arg:tt)*) => { - $crate::logger::log( - $crate::logger::LogLevel::Info, - &format!($($arg)*), - Some(file!()), - Some(line!()), - ) - }; -} - -/// Log at [`LogLevel::Warn`] level. -/// -/// ```rust -/// use unshell::warn; -/// warn!("unexpected path: {}", "/unknown"); -/// ``` -#[macro_export] -macro_rules! warn { - ($($arg:tt)*) => { - $crate::logger::log( - $crate::logger::LogLevel::Warn, - &format!($($arg)*), - Some(file!()), - Some(line!()), - ) - }; -} - -/// Log at [`LogLevel::Error`] level. -/// -/// ```rust -/// use unshell::error; -/// error!("connection failed: {}", "timeout"); -/// ``` -#[macro_export] -macro_rules! error { - ($($arg:tt)*) => { - $crate::logger::log( - $crate::logger::LogLevel::Error, - &format!($($arg)*), - Some(file!()), - Some(line!()), - ) - }; -} +pub use global::{global_logger, log, set_logger}; +pub use level::LogLevel; +pub use record::Record; +pub use sink::{CompatibilityLogger, Logger}; diff --git a/src/logger/record.rs b/src/logger/record.rs new file mode 100644 index 0000000..29752c8 --- /dev/null +++ b/src/logger/record.rs @@ -0,0 +1,45 @@ +use crate::logger::LogLevel; + +/// A single log entry delivered to a [`crate::logger::Logger`]. +/// +/// The record borrows the formatted message from the logging call site so the +/// sink can inspect source context without owning additional state. +pub struct Record<'a> { + /// Severity level for the entry. + pub level: LogLevel, + /// Human-readable message body. + pub message: &'a str, + /// Source file reported by `file!()` when available. + pub file: Option<&'static str>, + /// Source line reported by `line!()` when available. + pub line: Option, +} + +impl<'a> Record<'a> { + /// Creates a new record from explicit parts. + /// + /// # Examples + /// + /// ```rust + /// use unshell::logger::{LogLevel, Record}; + /// + /// let record = Record::new(LogLevel::Warn, "unexpected route", Some("router.rs"), Some(12)); + /// + /// assert_eq!(record.level, LogLevel::Warn); + /// assert_eq!(record.message, "unexpected route"); + /// ``` + #[must_use] + pub const fn new( + level: LogLevel, + message: &'a str, + file: Option<&'static str>, + line: Option, + ) -> Self { + Self { + level, + message, + file, + line, + } + } +} diff --git a/src/logger/sink.rs b/src/logger/sink.rs new file mode 100644 index 0000000..7fb3614 --- /dev/null +++ b/src/logger/sink.rs @@ -0,0 +1,60 @@ +use crate::logger::{LogLevel, Record}; + +/// Destination for log records. +/// +/// Implement this trait to forward logs into a serial console, buffer, test +/// collector, or host integration. +pub trait Logger: Sync { + /// Receives a single log record. + fn log(&self, record: &Record<'_>); +} + +/// Small filter-only logger for integrations that want a concrete type early. +/// +/// This logger intentionally performs no output. It only exposes the same +/// filtering decision a real sink would make, which is useful while wiring up a +/// platform-specific backend later. +pub struct CompatibilityLogger { + min_level: LogLevel, +} + +impl CompatibilityLogger { + /// Creates a logger that accepts `min_level` and anything more severe. + #[must_use] + pub const fn new(min_level: LogLevel) -> Self { + Self { min_level } + } + + /// Returns whether a record at `level` would be accepted. + /// + /// # Examples + /// + /// ```rust + /// use unshell::logger::{CompatibilityLogger, LogLevel}; + /// + /// let logger = CompatibilityLogger::new(LogLevel::Warn); + /// + /// assert!(!logger.accepts(LogLevel::Info)); + /// assert!(logger.accepts(LogLevel::Error)); + /// ``` + #[must_use] + pub fn accepts(&self, level: LogLevel) -> bool { + level >= self.min_level + } +} + +impl Logger for CompatibilityLogger { + fn log(&self, record: &Record<'_>) { + if !self.accepts(record.level) { + return; + } + + let _ = record; + } +} + +pub(crate) struct NullLogger; + +impl Logger for NullLogger { + fn log(&self, _record: &Record<'_>) {} +} diff --git a/src/logger/tests.rs b/src/logger/tests.rs new file mode 100644 index 0000000..a85f79f --- /dev/null +++ b/src/logger/tests.rs @@ -0,0 +1,19 @@ +use crate::logger::{CompatibilityLogger, LogLevel}; + +#[test] +fn level_labels_match_expected_output() { + assert_eq!(LogLevel::Debug.as_str(), "DEBUG"); + assert_eq!(LogLevel::Info.as_str(), "INFO"); + assert_eq!(LogLevel::Warn.as_str(), "WARN"); + assert_eq!(LogLevel::Error.as_str(), "ERROR"); +} + +#[test] +fn compatibility_logger_filters_lower_levels() { + let logger = CompatibilityLogger::new(LogLevel::Warn); + + assert!(!logger.accepts(LogLevel::Debug)); + assert!(!logger.accepts(LogLevel::Info)); + assert!(logger.accepts(LogLevel::Warn)); + assert!(logger.accepts(LogLevel::Error)); +}