chie_core/
logging.rs

1//! Logging configuration with configurable verbosity.
2//!
3//! This module provides a flexible logging system with configurable verbosity levels
4//! and filtering capabilities.
5//!
6//! # Features
7//!
8//! - Multiple verbosity levels (Error, Warn, Info, Debug, Trace)
9//! - Module-level filtering
10//! - Structured logging support
11//! - Performance metrics logging
12//! - Conditional compilation for zero-cost when disabled
13//!
14//! # Example
15//!
16//! ```
17//! use chie_core::logging::{LogConfig, LogLevel, Logger};
18//!
19//! // Create a logger with Info level
20//! let config = LogConfig {
21//!     level: LogLevel::Info,
22//!     include_timestamps: true,
23//!     include_module_path: true,
24//!     include_line_numbers: false,
25//!     filter_modules: vec![],
26//! };
27//!
28//! let logger = Logger::new(config);
29//!
30//! // Log messages at different levels
31//! logger.info("chie_core::storage", "Storage initialized");
32//! logger.debug("chie_core::cache", "Cache size: 1024 entries");
33//! logger.error("chie_core::network", "Connection failed");
34//! ```
35
36use std::collections::HashSet;
37use std::fmt;
38use std::time::SystemTime;
39
40/// Log verbosity levels.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub enum LogLevel {
43    /// Critical errors only.
44    Error,
45    /// Warnings and errors.
46    Warn,
47    /// Informational messages, warnings, and errors.
48    Info,
49    /// Debug messages and above.
50    Debug,
51    /// All messages including trace.
52    Trace,
53}
54
55impl LogLevel {
56    /// Get the string representation of the log level.
57    #[must_use]
58    #[inline]
59    pub const fn as_str(&self) -> &'static str {
60        match self {
61            Self::Error => "ERROR",
62            Self::Warn => "WARN",
63            Self::Info => "INFO",
64            Self::Debug => "DEBUG",
65            Self::Trace => "TRACE",
66        }
67    }
68
69    /// Get a colored version of the log level (ANSI codes).
70    #[must_use]
71    #[inline]
72    pub const fn colored(&self) -> &'static str {
73        match self {
74            Self::Error => "\x1b[31mERROR\x1b[0m", // Red
75            Self::Warn => "\x1b[33mWARN\x1b[0m",   // Yellow
76            Self::Info => "\x1b[32mINFO\x1b[0m",   // Green
77            Self::Debug => "\x1b[36mDEBUG\x1b[0m", // Cyan
78            Self::Trace => "\x1b[90mTRACE\x1b[0m", // Gray
79        }
80    }
81
82    /// Check if this level should be logged given the configured level.
83    #[must_use]
84    #[inline]
85    pub const fn should_log(&self, configured_level: &Self) -> bool {
86        (*self as u8) <= (*configured_level as u8)
87    }
88}
89
90impl fmt::Display for LogLevel {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "{}", self.as_str())
93    }
94}
95
96/// Logging configuration.
97#[derive(Debug, Clone)]
98pub struct LogConfig {
99    /// Minimum log level to display.
100    pub level: LogLevel,
101    /// Include timestamps in log messages.
102    pub include_timestamps: bool,
103    /// Include module path in log messages.
104    pub include_module_path: bool,
105    /// Include line numbers in log messages.
106    pub include_line_numbers: bool,
107    /// Modules to filter (only log these modules if non-empty).
108    pub filter_modules: Vec<String>,
109}
110
111impl Default for LogConfig {
112    fn default() -> Self {
113        Self {
114            level: LogLevel::Info,
115            include_timestamps: true,
116            include_module_path: true,
117            include_line_numbers: false,
118            filter_modules: Vec::new(),
119        }
120    }
121}
122
123impl LogConfig {
124    /// Create a new configuration with the specified level.
125    #[must_use]
126    #[inline]
127    pub const fn new(level: LogLevel) -> Self {
128        Self {
129            level,
130            include_timestamps: true,
131            include_module_path: true,
132            include_line_numbers: false,
133            filter_modules: Vec::new(),
134        }
135    }
136
137    /// Create a minimal configuration (no timestamps, no module paths).
138    #[must_use]
139    #[inline]
140    pub const fn minimal(level: LogLevel) -> Self {
141        Self {
142            level,
143            include_timestamps: false,
144            include_module_path: false,
145            include_line_numbers: false,
146            filter_modules: Vec::new(),
147        }
148    }
149
150    /// Create a verbose configuration (all metadata included).
151    #[must_use]
152    #[inline]
153    pub const fn verbose(level: LogLevel) -> Self {
154        Self {
155            level,
156            include_timestamps: true,
157            include_module_path: true,
158            include_line_numbers: true,
159            filter_modules: Vec::new(),
160        }
161    }
162
163    /// Add a module filter.
164    #[must_use]
165    pub fn with_module_filter(mut self, module: String) -> Self {
166        self.filter_modules.push(module);
167        self
168    }
169}
170
171/// Logger instance with configuration.
172pub struct Logger {
173    config: LogConfig,
174    filter_set: HashSet<String>,
175    use_color: bool,
176}
177
178impl Logger {
179    /// Create a new logger with the given configuration.
180    #[must_use]
181    pub fn new(config: LogConfig) -> Self {
182        let filter_set: HashSet<String> = config.filter_modules.iter().cloned().collect();
183
184        Self {
185            config,
186            filter_set,
187            use_color: is_terminal(),
188        }
189    }
190
191    /// Create a logger with default configuration.
192    #[must_use]
193    #[inline]
194    pub fn default_config() -> Self {
195        Self::new(LogConfig::default())
196    }
197
198    /// Check if a module should be logged.
199    #[must_use]
200    #[inline]
201    fn should_log_module(&self, module: &str) -> bool {
202        if self.filter_set.is_empty() {
203            return true;
204        }
205
206        self.filter_set
207            .iter()
208            .any(|filter| module.starts_with(filter) || filter.starts_with(module))
209    }
210
211    /// Log a message at the specified level.
212    pub fn log(&self, level: LogLevel, module: &str, message: &str, line: Option<u32>) {
213        if !level.should_log(&self.config.level) {
214            return;
215        }
216
217        if !self.should_log_module(module) {
218            return;
219        }
220
221        let mut parts = Vec::new();
222
223        // Timestamp
224        if self.config.include_timestamps {
225            let timestamp = format_timestamp();
226            parts.push(timestamp);
227        }
228
229        // Level
230        let level_str = if self.use_color {
231            level.colored().to_string()
232        } else {
233            level.as_str().to_string()
234        };
235        parts.push(format!("[{}]", level_str));
236
237        // Module path
238        if self.config.include_module_path {
239            parts.push(format!("[{}]", module));
240        }
241
242        // Line number
243        if self.config.include_line_numbers {
244            if let Some(line_num) = line {
245                parts.push(format!("[L{}]", line_num));
246            }
247        }
248
249        // Message
250        parts.push(message.to_string());
251
252        println!("{}", parts.join(" "));
253    }
254
255    /// Log an error message.
256    #[inline]
257    pub fn error(&self, module: &str, message: &str) {
258        self.log(LogLevel::Error, module, message, None);
259    }
260
261    /// Log a warning message.
262    #[inline]
263    pub fn warn(&self, module: &str, message: &str) {
264        self.log(LogLevel::Warn, module, message, None);
265    }
266
267    /// Log an info message.
268    #[inline]
269    pub fn info(&self, module: &str, message: &str) {
270        self.log(LogLevel::Info, module, message, None);
271    }
272
273    /// Log a debug message.
274    #[inline]
275    pub fn debug(&self, module: &str, message: &str) {
276        self.log(LogLevel::Debug, module, message, None);
277    }
278
279    /// Log a trace message.
280    #[inline]
281    pub fn trace(&self, module: &str, message: &str) {
282        self.log(LogLevel::Trace, module, message, None);
283    }
284
285    /// Log an error message with line number.
286    #[inline]
287    pub fn error_at(&self, module: &str, message: &str, line: u32) {
288        self.log(LogLevel::Error, module, message, Some(line));
289    }
290
291    /// Log a warning message with line number.
292    #[inline]
293    pub fn warn_at(&self, module: &str, message: &str, line: u32) {
294        self.log(LogLevel::Warn, module, message, Some(line));
295    }
296
297    /// Log a structured message with key-value pairs.
298    pub fn structured(
299        &self,
300        level: LogLevel,
301        module: &str,
302        message: &str,
303        fields: &[(&str, &str)],
304    ) {
305        if !level.should_log(&self.config.level) {
306            return;
307        }
308
309        if !self.should_log_module(module) {
310            return;
311        }
312
313        let fields_str: Vec<String> = fields.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
314
315        let full_message = if fields_str.is_empty() {
316            message.to_string()
317        } else {
318            format!("{} {}", message, fields_str.join(" "))
319        };
320
321        self.log(level, module, &full_message, None);
322    }
323
324    /// Log performance metrics.
325    #[inline]
326    pub fn perf(&self, module: &str, operation: &str, duration_ms: u64) {
327        let message = format!("{} completed in {}ms", operation, duration_ms);
328        self.structured(
329            LogLevel::Debug,
330            module,
331            &message,
332            &[
333                ("operation", operation),
334                ("duration_ms", &duration_ms.to_string()),
335            ],
336        );
337    }
338
339    /// Get the current log level.
340    #[must_use]
341    #[inline]
342    pub const fn level(&self) -> LogLevel {
343        self.config.level
344    }
345
346    /// Set the log level.
347    pub fn set_level(&mut self, level: LogLevel) {
348        self.config.level = level;
349    }
350
351    /// Enable or disable colored output.
352    #[inline]
353    pub fn set_color(&mut self, use_color: bool) {
354        self.use_color = use_color;
355    }
356}
357
358/// Format a timestamp for logging.
359#[must_use]
360fn format_timestamp() -> String {
361    let now = SystemTime::now()
362        .duration_since(SystemTime::UNIX_EPOCH)
363        .unwrap_or_default();
364
365    let secs = now.as_secs();
366    let millis = now.subsec_millis();
367
368    // Simple ISO-like format: HH:MM:SS.mmm
369    let hours = (secs / 3600) % 24;
370    let minutes = (secs / 60) % 60;
371    let seconds = secs % 60;
372
373    format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
374}
375
376/// Check if stdout is a terminal (for color support).
377#[must_use]
378#[inline]
379fn is_terminal() -> bool {
380    // Simple heuristic: check if TERM environment variable is set
381    std::env::var("TERM").is_ok()
382}
383
384/// Macro for logging with automatic module path.
385#[macro_export]
386macro_rules! log_error {
387    ($logger:expr, $($arg:tt)*) => {
388        $logger.error(module_path!(), &format!($($arg)*))
389    };
390}
391
392/// Macro for logging warnings with automatic module path.
393#[macro_export]
394macro_rules! log_warn {
395    ($logger:expr, $($arg:tt)*) => {
396        $logger.warn(module_path!(), &format!($($arg)*))
397    };
398}
399
400/// Macro for logging info with automatic module path.
401#[macro_export]
402macro_rules! log_info {
403    ($logger:expr, $($arg:tt)*) => {
404        $logger.info(module_path!(), &format!($($arg)*))
405    };
406}
407
408/// Macro for logging debug with automatic module path.
409#[macro_export]
410macro_rules! log_debug {
411    ($logger:expr, $($arg:tt)*) => {
412        $logger.debug(module_path!(), &format!($($arg)*))
413    };
414}
415
416/// Macro for logging trace with automatic module path.
417#[macro_export]
418macro_rules! log_trace {
419    ($logger:expr, $($arg:tt)*) => {
420        $logger.trace(module_path!(), &format!($($arg)*))
421    };
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn test_log_level_ordering() {
430        assert!(LogLevel::Error < LogLevel::Warn);
431        assert!(LogLevel::Warn < LogLevel::Info);
432        assert!(LogLevel::Info < LogLevel::Debug);
433        assert!(LogLevel::Debug < LogLevel::Trace);
434    }
435
436    #[test]
437    fn test_should_log() {
438        let configured_level = LogLevel::Info;
439
440        assert!(LogLevel::Error.should_log(&configured_level));
441        assert!(LogLevel::Warn.should_log(&configured_level));
442        assert!(LogLevel::Info.should_log(&configured_level));
443        assert!(!LogLevel::Debug.should_log(&configured_level));
444        assert!(!LogLevel::Trace.should_log(&configured_level));
445    }
446
447    #[test]
448    fn test_log_config_default() {
449        let config = LogConfig::default();
450        assert_eq!(config.level, LogLevel::Info);
451        assert!(config.include_timestamps);
452        assert!(config.include_module_path);
453        assert!(!config.include_line_numbers);
454    }
455
456    #[test]
457    fn test_log_config_minimal() {
458        let config = LogConfig::minimal(LogLevel::Debug);
459        assert_eq!(config.level, LogLevel::Debug);
460        assert!(!config.include_timestamps);
461        assert!(!config.include_module_path);
462        assert!(!config.include_line_numbers);
463    }
464
465    #[test]
466    fn test_log_config_verbose() {
467        let config = LogConfig::verbose(LogLevel::Trace);
468        assert_eq!(config.level, LogLevel::Trace);
469        assert!(config.include_timestamps);
470        assert!(config.include_module_path);
471        assert!(config.include_line_numbers);
472    }
473
474    #[test]
475    fn test_logger_creation() {
476        let config = LogConfig::default();
477        let logger = Logger::new(config);
478        assert_eq!(logger.level(), LogLevel::Info);
479    }
480
481    #[test]
482    fn test_module_filtering() {
483        let config = LogConfig::default().with_module_filter("chie_core::storage".to_string());
484        let logger = Logger::new(config);
485
486        assert!(logger.should_log_module("chie_core::storage"));
487        assert!(logger.should_log_module("chie_core::storage::chunk"));
488        assert!(!logger.should_log_module("chie_core::network"));
489    }
490
491    #[test]
492    fn test_logger_level_change() {
493        let mut logger = Logger::default_config();
494        assert_eq!(logger.level(), LogLevel::Info);
495
496        logger.set_level(LogLevel::Debug);
497        assert_eq!(logger.level(), LogLevel::Debug);
498    }
499
500    #[test]
501    fn test_log_level_display() {
502        assert_eq!(LogLevel::Error.to_string(), "ERROR");
503        assert_eq!(LogLevel::Warn.to_string(), "WARN");
504        assert_eq!(LogLevel::Info.to_string(), "INFO");
505        assert_eq!(LogLevel::Debug.to_string(), "DEBUG");
506        assert_eq!(LogLevel::Trace.to_string(), "TRACE");
507    }
508}