mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-09 06:47:59 -06:00
refactor: split logger into focused modules
This commit is contained in:
@@ -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<u32>) {
|
||||||
|
global_logger().log(&Record::new(level, message, file, line));
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
+31
-290
@@ -1,309 +1,50 @@
|
|||||||
//! # Logger Module
|
//! # 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
|
//! ```rust
|
||||||
//! use unshell::{info, warn, error};
|
//! use unshell::{error, info, warn};
|
||||||
//! use unshell::logger::{Logger, Record};
|
//! use unshell::logger::{Logger, Record, set_logger};
|
||||||
//!
|
//!
|
||||||
//! struct Sink;
|
//! struct Sink;
|
||||||
|
//!
|
||||||
//! impl Logger for 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<'_>) {
|
//! fn log(&self, record: &Record<'_>) {
|
||||||
//! if record.level < self.min_level {
|
|
||||||
//! return;
|
|
||||||
//! }
|
|
||||||
//! let _ = record;
|
//! let _ = record;
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! static MY_LOGGER: MemoryLogger = MemoryLogger {
|
//! static LOGGER: Sink = Sink;
|
||||||
//! min_level: LogLevel::Info,
|
//! set_logger(&LOGGER);
|
||||||
//! };
|
//!
|
||||||
//! set_logger(&MY_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
|
//! The global sink is installed once at startup and then treated as immutable.
|
||||||
//! are spawned. After that, it is only read (never written). This is safe
|
//! That contract lets the module stay `no_std` while still providing a simple
|
||||||
//! because:
|
//! global logging API.
|
||||||
//!
|
|
||||||
//! 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.
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
mod global;
|
||||||
// Log levels
|
mod level;
|
||||||
// ---------------------------------------------------------------------------
|
mod macros;
|
||||||
|
mod record;
|
||||||
|
mod sink;
|
||||||
|
|
||||||
/// The severity level of a log record.
|
#[cfg(test)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
mod tests;
|
||||||
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 {
|
pub use global::{global_logger, log, set_logger};
|
||||||
/// Short uppercase label, suitable for log line prefixes.
|
pub use level::LogLevel;
|
||||||
///
|
pub use record::Record;
|
||||||
/// # Example
|
pub use sink::{CompatibilityLogger, Logger};
|
||||||
///
|
|
||||||
/// ```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<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 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<u32>) {
|
|
||||||
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!()),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<u32>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
file,
|
||||||
|
line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<'_>) {}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user