gent/logging/
mod.rs

1//! Logging infrastructure for GENT
2//!
3//! Provides structured logging with levels, colored output, and timing support.
4
5use std::fmt;
6use std::io::{self, Write};
7use std::str::FromStr;
8use std::sync::Mutex;
9use std::time::Instant;
10
11/// Log levels in order of verbosity
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
13pub enum LogLevel {
14    Trace = 0,
15    Debug = 1,
16    Info = 2,
17    Warn = 3,
18    Error = 4,
19    Off = 5,
20}
21
22impl FromStr for LogLevel {
23    type Err = String;
24
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        match s.to_lowercase().as_str() {
27            "trace" => Ok(LogLevel::Trace),
28            "debug" => Ok(LogLevel::Debug),
29            "info" => Ok(LogLevel::Info),
30            "warn" | "warning" => Ok(LogLevel::Warn),
31            "error" => Ok(LogLevel::Error),
32            "off" | "none" => Ok(LogLevel::Off),
33            _ => Err(format!("Invalid log level: {}", s)),
34        }
35    }
36}
37
38impl LogLevel {
39    fn color_code(&self) -> &'static str {
40        match self {
41            LogLevel::Trace => "\x1b[90m", // Gray
42            LogLevel::Debug => "\x1b[36m", // Cyan
43            LogLevel::Info => "\x1b[32m",  // Green
44            LogLevel::Warn => "\x1b[33m",  // Yellow
45            LogLevel::Error => "\x1b[31m", // Red
46            LogLevel::Off => "",
47        }
48    }
49
50    fn label(&self) -> &'static str {
51        match self {
52            LogLevel::Trace => "TRACE",
53            LogLevel::Debug => "DEBUG",
54            LogLevel::Info => "INFO ",
55            LogLevel::Warn => "WARN ",
56            LogLevel::Error => "ERROR",
57            LogLevel::Off => "",
58        }
59    }
60}
61
62impl fmt::Display for LogLevel {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(f, "{}", self.label().trim())
65    }
66}
67
68/// Logger trait for abstracting log output
69pub trait Logger: Send + Sync {
70    fn log(&self, level: LogLevel, target: &str, message: &str);
71    fn log_with_duration(&self, level: LogLevel, target: &str, message: &str, duration_ms: u64);
72    fn is_enabled(&self, level: LogLevel) -> bool;
73    fn level(&self) -> LogLevel;
74}
75
76/// Main logger implementation with colored pretty output
77pub struct GentLogger {
78    level: LogLevel,
79    writer: Mutex<Box<dyn Write + Send>>,
80    use_colors: bool,
81}
82
83impl GentLogger {
84    /// Create a new logger with the given level, writing to stderr
85    pub fn new(level: LogLevel) -> Self {
86        Self {
87            level,
88            writer: Mutex::new(Box::new(io::stderr())),
89            use_colors: atty::is(atty::Stream::Stderr),
90        }
91    }
92
93    /// Create a logger that writes to a custom writer
94    pub fn with_writer(level: LogLevel, writer: Box<dyn Write + Send>) -> Self {
95        Self {
96            level,
97            writer: Mutex::new(writer),
98            use_colors: false,
99        }
100    }
101
102    fn format_message(
103        &self,
104        level: LogLevel,
105        target: &str,
106        message: &str,
107        duration_ms: Option<u64>,
108    ) -> String {
109        let reset = "\x1b[0m";
110        let dim = "\x1b[90m";
111
112        if self.use_colors {
113            let mut output = format!(
114                "{}{}{} {}{}{} {}",
115                level.color_code(),
116                level.label(),
117                reset,
118                dim,
119                target,
120                reset,
121                message,
122            );
123
124            if let Some(ms) = duration_ms {
125                output.push_str(&format!(" {}({}ms){}", dim, ms, reset));
126            }
127            output
128        } else {
129            let mut output = format!("{} {} {}", level.label(), target, message);
130            if let Some(ms) = duration_ms {
131                output.push_str(&format!(" ({}ms)", ms));
132            }
133            output
134        }
135    }
136}
137
138impl Logger for GentLogger {
139    fn log(&self, level: LogLevel, target: &str, message: &str) {
140        if level >= self.level {
141            let formatted = self.format_message(level, target, message, None);
142            if let Ok(mut writer) = self.writer.lock() {
143                let _ = writeln!(writer, "{}", formatted);
144            }
145        }
146    }
147
148    fn log_with_duration(&self, level: LogLevel, target: &str, message: &str, duration_ms: u64) {
149        if level >= self.level {
150            let formatted = self.format_message(level, target, message, Some(duration_ms));
151            if let Ok(mut writer) = self.writer.lock() {
152                let _ = writeln!(writer, "{}", formatted);
153            }
154        }
155    }
156
157    fn is_enabled(&self, level: LogLevel) -> bool {
158        level >= self.level
159    }
160
161    fn level(&self) -> LogLevel {
162        self.level
163    }
164}
165
166/// A no-op logger that discards all messages
167pub struct NullLogger;
168
169impl Logger for NullLogger {
170    fn log(&self, _level: LogLevel, _target: &str, _message: &str) {}
171    fn log_with_duration(
172        &self,
173        _level: LogLevel,
174        _target: &str,
175        _message: &str,
176        _duration_ms: u64,
177    ) {
178    }
179    fn is_enabled(&self, _level: LogLevel) -> bool {
180        false
181    }
182    fn level(&self) -> LogLevel {
183        LogLevel::Off
184    }
185}
186
187/// Timer for measuring operation duration
188/// Logs when dropped
189pub struct Timer<'a> {
190    start: Instant,
191    name: String,
192    target: String,
193    level: LogLevel,
194    logger: &'a dyn Logger,
195}
196
197impl<'a> Timer<'a> {
198    pub fn new(
199        name: impl Into<String>,
200        target: impl Into<String>,
201        level: LogLevel,
202        logger: &'a dyn Logger,
203    ) -> Self {
204        Self {
205            start: Instant::now(),
206            name: name.into(),
207            target: target.into(),
208            level,
209            logger,
210        }
211    }
212
213    pub fn elapsed_ms(&self) -> u64 {
214        self.start.elapsed().as_millis() as u64
215    }
216}
217
218impl<'a> Drop for Timer<'a> {
219    fn drop(&mut self) {
220        let ms = self.elapsed_ms();
221        self.logger.log_with_duration(
222            self.level,
223            &self.target,
224            &format!("{} completed", self.name),
225            ms,
226        );
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use std::sync::Arc;
234
235    #[test]
236    fn test_log_level_ordering() {
237        assert!(LogLevel::Trace < LogLevel::Debug);
238        assert!(LogLevel::Debug < LogLevel::Info);
239        assert!(LogLevel::Info < LogLevel::Warn);
240        assert!(LogLevel::Warn < LogLevel::Error);
241        assert!(LogLevel::Error < LogLevel::Off);
242    }
243
244    #[test]
245    fn test_log_level_from_str() {
246        assert_eq!("debug".parse::<LogLevel>(), Ok(LogLevel::Debug));
247        assert_eq!("DEBUG".parse::<LogLevel>(), Ok(LogLevel::Debug));
248        assert_eq!("info".parse::<LogLevel>(), Ok(LogLevel::Info));
249        assert_eq!("warn".parse::<LogLevel>(), Ok(LogLevel::Warn));
250        assert_eq!("warning".parse::<LogLevel>(), Ok(LogLevel::Warn));
251        assert!("invalid".parse::<LogLevel>().is_err());
252    }
253
254    #[test]
255    fn test_logger_is_enabled() {
256        let logger = GentLogger::new(LogLevel::Info);
257        assert!(!logger.is_enabled(LogLevel::Debug));
258        assert!(logger.is_enabled(LogLevel::Info));
259        assert!(logger.is_enabled(LogLevel::Warn));
260        assert!(logger.is_enabled(LogLevel::Error));
261    }
262
263    #[test]
264    fn test_null_logger() {
265        let logger = NullLogger;
266        assert!(!logger.is_enabled(LogLevel::Error));
267        // Should not panic
268        logger.log(LogLevel::Error, "test", "message");
269    }
270
271    #[test]
272    fn test_logger_captures_output() {
273        use std::sync::Arc;
274
275        let buffer = Arc::new(Mutex::new(Vec::new()));
276        let buffer_clone = buffer.clone();
277
278        let writer = Box::new(TestWriter {
279            buffer: buffer_clone,
280        });
281        let logger = GentLogger::with_writer(LogLevel::Debug, writer);
282
283        logger.log(LogLevel::Info, "test", "hello world");
284
285        let output = buffer.lock().unwrap();
286        let output_str = String::from_utf8_lossy(&output);
287        assert!(output_str.contains("INFO"));
288        assert!(output_str.contains("test"));
289        assert!(output_str.contains("hello world"));
290    }
291
292    struct TestWriter {
293        buffer: Arc<Mutex<Vec<u8>>>,
294    }
295
296    impl Write for TestWriter {
297        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
298            self.buffer.lock().unwrap().extend_from_slice(buf);
299            Ok(buf.len())
300        }
301
302        fn flush(&mut self) -> io::Result<()> {
303            Ok(())
304        }
305    }
306}