rust_loguru/handler/
console.rs

1use std::fmt;
2use std::io::{self, Write};
3use std::sync::Mutex;
4
5use crate::formatters::Formatter;
6use crate::level::LogLevel;
7use crate::record::Record;
8
9use super::{Handler, HandlerFilter};
10
11/// A wrapper around a writer that implements Debug
12pub struct DebugWrite {
13    writer: Mutex<Box<dyn Write + Send + Sync>>,
14}
15
16impl fmt::Debug for DebugWrite {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        f.debug_struct("DebugWrite")
19            .field("writer", &"<writer>")
20            .finish()
21    }
22}
23
24/// A handler that writes to the console
25pub struct ConsoleHandler {
26    /// The log level
27    level: LogLevel,
28    /// Whether the handler is enabled
29    enabled: bool,
30    /// The formatter to use
31    formatter: Formatter,
32    /// The output stream to write to
33    output: DebugWrite,
34    /// Optional filter closure
35    filter: Option<HandlerFilter>,
36}
37
38impl Clone for ConsoleHandler {
39    fn clone(&self) -> Self {
40        Self {
41            level: self.level,
42            enabled: self.enabled,
43            formatter: self.formatter.clone(),
44            output: DebugWrite {
45                writer: Mutex::new(Box::new(io::stdout())),
46            },
47            filter: self.filter.clone(),
48        }
49    }
50}
51
52impl ConsoleHandler {
53    /// Create a new console handler that writes to stdout
54    pub fn stdout(level: LogLevel) -> Self {
55        Self {
56            level,
57            enabled: true,
58            formatter: Formatter::text()
59                .with_pattern("{level} - {message}")
60                .with_colors(true),
61            output: DebugWrite {
62                writer: Mutex::new(Box::new(io::stdout())),
63            },
64            filter: None,
65        }
66    }
67
68    /// Create a new console handler that writes to stderr
69    pub fn stderr(level: LogLevel) -> Self {
70        Self {
71            level,
72            enabled: true,
73            formatter: Formatter::text()
74                .with_pattern("{level} - {message}")
75                .with_colors(true),
76            output: DebugWrite {
77                writer: Mutex::new(Box::new(io::stderr())),
78            },
79            filter: None,
80        }
81    }
82
83    /// Create a new console handler with a custom writer
84    pub fn with_writer(level: LogLevel, writer: Box<dyn Write + Send + Sync>) -> Self {
85        Self {
86            level,
87            enabled: true,
88            formatter: Formatter::text()
89                .with_pattern("{level} - {message}")
90                .with_colors(true),
91            output: DebugWrite {
92                writer: Mutex::new(writer),
93            },
94            filter: None,
95        }
96    }
97
98    /// Sets whether to use colors in the output.
99    pub fn with_colors(mut self, use_colors: bool) -> Self {
100        self.formatter = self.formatter.with_colors(use_colors);
101        self
102    }
103
104    /// Sets a custom format pattern.
105    pub fn with_pattern(self, pattern: impl Into<String>) -> Self {
106        let mut handler = self;
107        let formatter = handler.formatter.with_pattern(pattern);
108        handler.formatter = formatter;
109        handler
110    }
111
112    /// Sets a custom format function for the handler.
113    pub fn with_format<F>(mut self, format_fn: F) -> Self
114    where
115        F: Fn(&Record) -> String + Send + Sync + 'static,
116    {
117        self.formatter = self.formatter.with_format(format_fn);
118        self
119    }
120
121    pub fn with_formatter(mut self, formatter: Formatter) -> Self {
122        self.formatter = formatter;
123        self
124    }
125
126    pub fn with_filter(mut self, filter: HandlerFilter) -> Self {
127        self.filter = Some(filter);
128        self
129    }
130}
131
132impl Default for ConsoleHandler {
133    fn default() -> Self {
134        Self::stdout(LogLevel::Info)
135    }
136}
137
138impl Handler for ConsoleHandler {
139    fn handle(&self, record: &Record) -> Result<(), String> {
140        if !self.enabled || record.level() < self.level {
141            return Ok(());
142        }
143        if let Some(filter) = &self.filter {
144            if !(filter)(record) {
145                return Ok(());
146            }
147        }
148        let formatted = self.formatter.format(record);
149        let mut writer = self
150            .output
151            .writer
152            .lock()
153            .map_err(|e| format!("Failed to lock writer: {}", e))?;
154        write!(writer, "{}", formatted)
155            .map_err(|e| format!("Failed to write to console: {}", e))?;
156        writer
157            .flush()
158            .map_err(|e| format!("Failed to flush console: {}", e))?;
159        Ok(())
160    }
161
162    fn level(&self) -> LogLevel {
163        self.level
164    }
165
166    fn set_level(&mut self, level: LogLevel) {
167        self.level = level;
168    }
169
170    fn is_enabled(&self) -> bool {
171        self.enabled
172    }
173
174    fn set_enabled(&mut self, enabled: bool) {
175        self.enabled = enabled;
176    }
177
178    fn formatter(&self) -> &Formatter {
179        &self.formatter
180    }
181
182    fn set_formatter(&mut self, formatter: Formatter) {
183        self.formatter = formatter;
184    }
185
186    fn set_filter(&mut self, filter: Option<HandlerFilter>) {
187        self.filter = filter;
188    }
189
190    fn filter(&self) -> Option<&HandlerFilter> {
191        self.filter.as_ref()
192    }
193
194    fn handle_batch(&self, records: &[Record]) -> Result<(), String> {
195        for record in records {
196            self.handle(record)?;
197        }
198        Ok(())
199    }
200
201    fn init(&mut self) -> Result<(), String> {
202        Ok(())
203    }
204
205    fn flush(&self) -> Result<(), String> {
206        Ok(())
207    }
208
209    fn shutdown(&mut self) -> Result<(), String> {
210        Ok(())
211    }
212}
213
214impl fmt::Debug for ConsoleHandler {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        f.debug_struct("ConsoleHandler")
217            .field("level", &self.level)
218            .field("enabled", &self.enabled)
219            .field("formatter", &self.formatter)
220            .field("output", &self.output)
221            .finish()
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use std::sync::{Arc, Mutex};
229
230    struct TestOutput {
231        buffer: Arc<Mutex<Vec<u8>>>,
232    }
233
234    impl Clone for TestOutput {
235        fn clone(&self) -> Self {
236            Self {
237                buffer: self.buffer.clone(),
238            }
239        }
240    }
241
242    impl Write for TestOutput {
243        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
244            self.buffer.lock().unwrap().extend_from_slice(buf);
245            Ok(buf.len())
246        }
247
248        fn flush(&mut self) -> io::Result<()> {
249            Ok(())
250        }
251    }
252
253    impl TestOutput {
254        fn new() -> Self {
255            Self {
256                buffer: Arc::new(Mutex::new(Vec::new())),
257            }
258        }
259
260        fn contents(&self) -> String {
261            let buffer = self.buffer.lock().unwrap();
262            String::from_utf8_lossy(&buffer).to_string()
263        }
264    }
265
266    #[test]
267    fn test_console_handler_level_filtering() {
268        let output = TestOutput::new();
269        let mut handler = ConsoleHandler::with_writer(LogLevel::Warning, Box::new(output.clone()));
270        handler.set_level(LogLevel::Warning);
271
272        let info_record = Record::new(
273            LogLevel::Info,
274            "info message",
275            Some("test".to_string()),
276            Some("test.rs".to_string()),
277            Some(42),
278        );
279        let warning_record = Record::new(
280            LogLevel::Warning,
281            "warning message",
282            Some("test".to_string()),
283            Some("test.rs".to_string()),
284            Some(42),
285        );
286
287        assert!(handler.handle(&info_record).is_ok());
288        assert!(handler.handle(&warning_record).is_ok());
289        assert!(output.contents().contains("warning message"));
290    }
291
292    #[test]
293    fn test_console_handler_disabled() {
294        let output = TestOutput::new();
295        let mut handler = ConsoleHandler::with_writer(LogLevel::Warning, Box::new(output.clone()));
296        handler.set_enabled(false);
297
298        let record = Record::new(
299            LogLevel::Info,
300            "Test message",
301            Some("test".to_string()),
302            Some("test.rs".to_string()),
303            Some(42),
304        );
305
306        assert!(handler.handle(&record).is_ok());
307        assert!(output.contents().is_empty());
308    }
309
310    #[test]
311    fn test_console_handler_formatting() {
312        let output = TestOutput::new();
313        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()))
314            .with_pattern("{level} - {message}")
315            .with_colors(false);
316
317        let record = Record::new(
318            LogLevel::Info,
319            "Test message",
320            Some("test".to_string()),
321            Some("test.rs".to_string()),
322            Some(42),
323        );
324
325        assert!(handler.handle(&record).is_ok());
326        assert!(output.contents().contains("INFO - Test message"));
327    }
328
329    #[test]
330    fn test_console_handler_metadata() {
331        let output = TestOutput::new();
332        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()));
333
334        let mut record = Record::new(
335            LogLevel::Info,
336            "Test message",
337            Some("test".to_string()),
338            Some("test.rs".to_string()),
339            Some(42),
340        );
341        record = record.with_metadata("key1", "value1");
342        record = record.with_metadata("key2", "value2");
343
344        assert!(handler.handle(&record).is_ok());
345        let contents = output.contents();
346        assert!(contents.contains("key1=value1"));
347        assert!(contents.contains("key2=value2"));
348    }
349
350    #[test]
351    fn test_console_handler_structured_data() {
352        let output = TestOutput::new();
353        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()))
354            .with_pattern(r#"{{"level":"{level}","message":"{message}","module":"{module}"}}"#)
355            .with_colors(false);
356
357        let record = Record::new(
358            LogLevel::Info,
359            "Test message",
360            Some("test".to_string()),
361            Some("test.rs".to_string()),
362            Some(42),
363        );
364
365        assert!(handler.handle(&record).is_ok());
366        let output = output.contents();
367        assert!(output.contains(r#""level":"INFO""#));
368        assert!(output.contains(r#""message":"Test message""#));
369        assert!(output.contains(r#""module":"test""#));
370    }
371
372    #[test]
373    fn test_handle_uses_configured_writer() {
374        let output = TestOutput::new();
375        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()));
376        let record = Record::new(
377            LogLevel::Info,
378            "test message",
379            Some("test".to_string()),
380            Some("test.rs".to_string()),
381            Some(42),
382        );
383
384        assert!(handler.handle(&record).is_ok());
385        assert!(output.contents().contains("test message"));
386    }
387
388    #[test]
389    fn test_handle_respects_disabled() {
390        let output = TestOutput::new();
391        let mut handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()));
392        handler.set_enabled(false);
393        let record = Record::new(
394            LogLevel::Info,
395            "test message",
396            Some("test".to_string()),
397            Some("test.rs".to_string()),
398            Some(42),
399        );
400
401        assert!(handler.handle(&record).is_ok());
402        assert!(output.contents().is_empty());
403    }
404
405    #[test]
406    fn test_handle_respects_level() {
407        let output = TestOutput::new();
408        let mut handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()));
409        handler.set_level(LogLevel::Error);
410        let record = Record::new(
411            LogLevel::Info,
412            "test message",
413            Some("test".to_string()),
414            Some("test.rs".to_string()),
415            Some(42),
416        );
417
418        assert!(handler.handle(&record).is_ok());
419        assert!(output.contents().is_empty());
420    }
421
422    #[test]
423    fn test_console_handler_filtering() {
424        let output = TestOutput::new();
425        let filter = std::sync::Arc::new(|record: &Record| record.message().contains("pass"));
426        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()))
427            .with_filter(filter.clone());
428        let record1 = Record::new(
429            LogLevel::Info,
430            "should pass",
431            None::<String>,
432            None::<String>,
433            None,
434        );
435        let record2 = Record::new(
436            LogLevel::Info,
437            "should fail",
438            None::<String>,
439            None::<String>,
440            None,
441        );
442        assert!(handler.handle(&record1).is_ok());
443        assert!(handler.handle(&record2).is_ok());
444        let contents = output.contents();
445        assert!(contents.contains("should pass"));
446        assert!(!contents.contains("should fail"));
447    }
448
449    #[test]
450    fn test_console_handler_batch() {
451        let output = TestOutput::new();
452        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()));
453        let records = vec![
454            Record::new(LogLevel::Info, "msg1", None::<String>, None::<String>, None),
455            Record::new(LogLevel::Info, "msg2", None::<String>, None::<String>, None),
456        ];
457        assert!(handler.handle_batch(&records).is_ok());
458        let contents = output.contents();
459        assert!(contents.contains("msg1"));
460        assert!(contents.contains("msg2"));
461    }
462}