Skip to main content

commons/
logging.rs

1//! Structured logging and telemetry utilities.
2
3use std::fmt;
4
5/// Log levels for structured logging
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub enum LogLevel {
8    /// Trace level - very verbose debugging
9    Trace = 0,
10    /// Debug level - debugging information
11    Debug = 1,
12    /// Info level - general information
13    Info = 2,
14    /// Warn level - warning messages
15    Warn = 3,
16    /// Error level - error messages
17    Error = 4,
18}
19
20impl fmt::Display for LogLevel {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::Trace => write!(f, "TRACE"),
24            Self::Debug => write!(f, "DEBUG"),
25            Self::Info => write!(f, "INFO"),
26            Self::Warn => write!(f, "WARN"),
27            Self::Error => write!(f, "ERROR"),
28        }
29    }
30}
31
32/// Simple structured logger.
33///
34/// Lightweight wrapper that prints timestamped, level-filtered messages to
35/// stdout. Each `Logger` owns its module name as a `String` — creating one
36/// allocates, so prefer storing it rather than constructing per-call.
37///
38/// For high-throughput or production logging, consider pairing this with the
39/// [`log`](https://crates.io/crates/log) crate facade.
40#[derive(Debug)]
41pub struct Logger {
42    level: LogLevel,
43    module: String,
44}
45
46impl Logger {
47    /// Create a new logger for a module
48    #[must_use]
49    pub fn new(module: &str) -> Self {
50        Self {
51            level: LogLevel::Info,
52            module: module.to_string(),
53        }
54    }
55
56    /// Set the minimum log level
57    pub const fn set_level(&mut self, level: LogLevel) {
58        self.level = level;
59    }
60
61    /// Log a message at the given level
62    pub fn log(&self, level: LogLevel, message: &str) {
63        if level >= self.level {
64            let timestamp = Self::timestamp();
65            println!("[{timestamp}] {level} [{}] {message}", self.module);
66        }
67    }
68
69    /// Get the current Unix timestamp in seconds.
70    ///
71    /// Uses `crate::time::unix_timestamp()` when the `time` feature is
72    /// enabled (which the `logging` feature implies). Falls back to raw
73    /// `SystemTime` arithmetic otherwise, so logging still compiles even
74    /// if the dependency chain is manually overridden.
75    #[cfg(feature = "time")]
76    fn timestamp() -> u64 {
77        crate::time::unix_timestamp()
78    }
79
80    /// Fallback timestamp when the `time` feature is absent.
81    #[cfg(not(feature = "time"))]
82    fn timestamp() -> u64 {
83        use std::time::{SystemTime, UNIX_EPOCH};
84        SystemTime::now()
85            .duration_since(UNIX_EPOCH)
86            .unwrap_or_default()
87            .as_secs()
88    }
89
90    /// Log a trace message
91    pub fn trace(&self, message: &str) {
92        self.log(LogLevel::Trace, message);
93    }
94
95    /// Log a debug message
96    pub fn debug(&self, message: &str) {
97        self.log(LogLevel::Debug, message);
98    }
99
100    /// Log an info message
101    pub fn info(&self, message: &str) {
102        self.log(LogLevel::Info, message);
103    }
104
105    /// Log a warning message
106    pub fn warn(&self, message: &str) {
107        self.log(LogLevel::Warn, message);
108    }
109
110    /// Log an error message
111    pub fn error(&self, message: &str) {
112        self.log(LogLevel::Error, message);
113    }
114}
115
116/// Create a logger for the current module.
117///
118/// Returns a [`Logger`] whose module name is set to the caller's
119/// [`module_path!()`]. This is the recommended way to obtain a logger
120/// without hard-coding module strings.
121#[cfg(feature = "logging")]
122#[macro_export]
123macro_rules! logger {
124    () => {
125        $crate::logging::Logger::new(module_path!())
126    };
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_logger_basic() {
135        let logger = Logger::new("test_module");
136        // Exercises the crate::time::unix_timestamp() path — will panic
137        // if the `time` feature is not correctly pulled in by `logging`.
138        logger.info("basic log test");
139    }
140
141    #[test]
142    fn test_logger_level_filtering() {
143        let mut logger = Logger::new("filter_test");
144        logger.set_level(LogLevel::Warn);
145
146        // These should not panic — they are simply filtered out.
147        logger.trace("should be filtered");
148        logger.debug("should be filtered");
149        logger.info("should be filtered");
150
151        // These should print (level >= Warn).
152        logger.warn("visible warning");
153        logger.error("visible error");
154    }
155}