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, HandlerError, 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(mut self, pattern: impl Into<String>) -> Self {
106        self.formatter = Formatter::template(pattern);
107        self
108    }
109
110    /// Sets a custom format function for the handler.
111    pub fn with_format<F>(mut self, format_fn: F) -> Self
112    where
113        F: Fn(&Record) -> String + Send + Sync + 'static,
114    {
115        self.formatter = self.formatter.with_format(format_fn);
116        self
117    }
118
119    pub fn with_formatter(mut self, formatter: Formatter) -> Self {
120        self.formatter = formatter;
121        self
122    }
123
124    pub fn with_filter(mut self, filter: HandlerFilter) -> Self {
125        self.filter = Some(filter);
126        self
127    }
128}
129
130impl Default for ConsoleHandler {
131    fn default() -> Self {
132        Self::stdout(LogLevel::Info)
133    }
134}
135
136impl Handler for ConsoleHandler {
137    fn handle(&self, record: &Record) -> Result<(), HandlerError> {
138        if !self.enabled || record.level() < self.level {
139            return Ok(());
140        }
141        if let Some(filter) = &self.filter {
142            if !(filter)(record) {
143                return Ok(());
144            }
145        }
146        let formatted = self.formatter.format(record);
147        let mut writer = self
148            .output
149            .writer
150            .lock()
151            .map_err(|e| HandlerError::Custom(format!("Failed to lock writer: {}", e)))?;
152        write!(writer, "{}", formatted).map_err(HandlerError::IoError)?;
153        writer.flush().map_err(HandlerError::IoError)?;
154        Ok(())
155    }
156
157    fn level(&self) -> LogLevel {
158        self.level
159    }
160
161    fn set_level(&mut self, level: LogLevel) {
162        self.level = level;
163    }
164
165    fn is_enabled(&self) -> bool {
166        self.enabled
167    }
168
169    fn set_enabled(&mut self, enabled: bool) {
170        self.enabled = enabled;
171    }
172
173    fn formatter(&self) -> &Formatter {
174        &self.formatter
175    }
176
177    fn set_formatter(&mut self, formatter: Formatter) {
178        self.formatter = formatter;
179    }
180
181    fn set_filter(&mut self, filter: Option<HandlerFilter>) {
182        self.filter = filter;
183    }
184
185    fn filter(&self) -> Option<&HandlerFilter> {
186        self.filter.as_ref()
187    }
188
189    fn handle_batch(&self, records: &[Record]) -> Result<(), HandlerError> {
190        for record in records {
191            self.handle(record)?;
192        }
193        Ok(())
194    }
195
196    fn init(&mut self) -> Result<(), HandlerError> {
197        Ok(())
198    }
199
200    fn flush(&self) -> Result<(), HandlerError> {
201        Ok(())
202    }
203
204    fn shutdown(&mut self) -> Result<(), HandlerError> {
205        Ok(())
206    }
207}
208
209impl fmt::Debug for ConsoleHandler {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        f.debug_struct("ConsoleHandler")
212            .field("level", &self.level)
213            .field("enabled", &self.enabled)
214            .field("formatter", &self.formatter)
215            .field("output", &self.output)
216            .finish()
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use std::sync::{Arc, Mutex};
224
225    struct TestOutput {
226        buffer: Arc<Mutex<Vec<u8>>>,
227    }
228
229    impl Clone for TestOutput {
230        fn clone(&self) -> Self {
231            Self {
232                buffer: self.buffer.clone(),
233            }
234        }
235    }
236
237    impl Write for TestOutput {
238        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
239            self.buffer.lock().unwrap().extend_from_slice(buf);
240            Ok(buf.len())
241        }
242
243        fn flush(&mut self) -> io::Result<()> {
244            Ok(())
245        }
246    }
247
248    impl TestOutput {
249        fn new() -> Self {
250            Self {
251                buffer: Arc::new(Mutex::new(Vec::new())),
252            }
253        }
254
255        fn contents(&self) -> String {
256            let buffer = self.buffer.lock().unwrap();
257            String::from_utf8_lossy(&buffer).to_string()
258        }
259    }
260
261    #[test]
262    fn test_console_handler_level_filtering() {
263        let output = TestOutput::new();
264        let mut handler = ConsoleHandler::with_writer(LogLevel::Warning, Box::new(output.clone()));
265        handler.set_level(LogLevel::Warning);
266
267        let info_record = Record::new(
268            LogLevel::Info,
269            "info message",
270            Some("test".to_string()),
271            Some("test.rs".to_string()),
272            Some(42),
273        );
274        let warning_record = Record::new(
275            LogLevel::Warning,
276            "warning message",
277            Some("test".to_string()),
278            Some("test.rs".to_string()),
279            Some(42),
280        );
281
282        assert!(handler.handle(&info_record).is_ok());
283        assert!(handler.handle(&warning_record).is_ok());
284        assert!(output.contents().contains("warning message"));
285    }
286
287    #[test]
288    fn test_console_handler_disabled() {
289        let output = TestOutput::new();
290        let mut handler = ConsoleHandler::with_writer(LogLevel::Warning, Box::new(output.clone()));
291        handler.set_enabled(false);
292
293        let record = Record::new(
294            LogLevel::Info,
295            "Test message",
296            Some("test".to_string()),
297            Some("test.rs".to_string()),
298            Some(42),
299        );
300
301        assert!(handler.handle(&record).is_ok());
302        assert!(output.contents().is_empty());
303    }
304
305    #[test]
306    fn test_console_handler_formatting() {
307        let output = TestOutput::new();
308        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()))
309            .with_pattern("{level} - {message}")
310            .with_colors(false);
311
312        let record = Record::new(
313            LogLevel::Info,
314            "Test message",
315            Some("test".to_string()),
316            Some("test.rs".to_string()),
317            Some(42),
318        );
319
320        assert!(handler.handle(&record).is_ok());
321        assert!(output.contents().contains("INFO - Test message"));
322    }
323
324    #[test]
325    fn test_console_handler_metadata() {
326        let output = TestOutput::new();
327        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()))
328            .with_pattern("{level} - {message} {metadata}");
329
330        let mut record = Record::new(
331            LogLevel::Info,
332            "Test message",
333            Some("test".to_string()),
334            Some("test.rs".to_string()),
335            Some(42),
336        );
337        record = record.with_metadata("key1", "value1");
338        record = record.with_metadata("key2", "value2");
339
340        assert!(handler.handle(&record).is_ok());
341        let contents = output.contents();
342        assert!(contents.contains("key1=value1"));
343        assert!(contents.contains("key2=value2"));
344    }
345
346    #[test]
347    fn test_console_handler_structured_data() {
348        let output = TestOutput::new();
349        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()))
350            .with_formatter(Formatter::json());
351
352        let record = Record::new(
353            LogLevel::Info,
354            "Test message",
355            Some("test".to_string()),
356            Some("test.rs".to_string()),
357            Some(42),
358        );
359
360        assert!(handler.handle(&record).is_ok());
361        let output = output.contents();
362        assert!(output.contains(r#""level":"INFO""#));
363        assert!(output.contains(r#""message":"Test message""#));
364        assert!(output.contains(r#""module":"test""#));
365    }
366
367    #[test]
368    fn test_handle_uses_configured_writer() {
369        let output = TestOutput::new();
370        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()));
371        let record = Record::new(
372            LogLevel::Info,
373            "test message",
374            Some("test".to_string()),
375            Some("test.rs".to_string()),
376            Some(42),
377        );
378
379        assert!(handler.handle(&record).is_ok());
380        assert!(output.contents().contains("test message"));
381    }
382
383    #[test]
384    fn test_handle_respects_disabled() {
385        let output = TestOutput::new();
386        let mut handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()));
387        handler.set_enabled(false);
388        let record = Record::new(
389            LogLevel::Info,
390            "test message",
391            Some("test".to_string()),
392            Some("test.rs".to_string()),
393            Some(42),
394        );
395
396        assert!(handler.handle(&record).is_ok());
397        assert!(output.contents().is_empty());
398    }
399
400    #[test]
401    fn test_handle_respects_level() {
402        let output = TestOutput::new();
403        let mut handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()));
404        handler.set_level(LogLevel::Error);
405        let record = Record::new(
406            LogLevel::Info,
407            "test message",
408            Some("test".to_string()),
409            Some("test.rs".to_string()),
410            Some(42),
411        );
412
413        assert!(handler.handle(&record).is_ok());
414        assert!(output.contents().is_empty());
415    }
416
417    #[test]
418    fn test_console_handler_filtering() {
419        let output = TestOutput::new();
420        let filter = std::sync::Arc::new(|record: &Record| record.message().contains("pass"));
421        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()))
422            .with_filter(filter.clone());
423        let record1 = Record::new(
424            LogLevel::Info,
425            "should pass",
426            None::<String>,
427            None::<String>,
428            None,
429        );
430        let record2 = Record::new(
431            LogLevel::Info,
432            "should fail",
433            None::<String>,
434            None::<String>,
435            None,
436        );
437        assert!(handler.handle(&record1).is_ok());
438        assert!(handler.handle(&record2).is_ok());
439        let contents = output.contents();
440        assert!(contents.contains("should pass"));
441        assert!(!contents.contains("should fail"));
442    }
443
444    #[test]
445    fn test_console_handler_batch() {
446        let output = TestOutput::new();
447        let handler = ConsoleHandler::with_writer(LogLevel::Info, Box::new(output.clone()));
448        let records = vec![
449            Record::new(LogLevel::Info, "msg1", None::<String>, None::<String>, None),
450            Record::new(LogLevel::Info, "msg2", None::<String>, None::<String>, None),
451        ];
452        assert!(handler.handle_batch(&records).is_ok());
453        let contents = output.contents();
454        assert!(contents.contains("msg1"));
455        assert!(contents.contains("msg2"));
456    }
457}