nonblocking_logger/structs/
logger.rs

1use crate::enums::log_level::LogLevel;
2use std::collections::HashMap;
3use std::io::{self, Write};
4use std::sync::RwLock;
5
6/// A simple, flexible logger that supports multiple targets and custom formats
7pub struct Logger {
8    level: LogLevel,
9    time_format: String,
10    format_strings: HashMap<LogLevel, String>,
11    targets: Vec<RwLock<Box<dyn Write + Send + Sync>>>,
12}
13
14impl Logger {
15    /// Create a new logger with default settings
16    pub fn new() -> Self {
17        Self {
18            level: LogLevel::Info,
19            time_format: "%Y-%m-%d %H:%M:%S".to_string(),
20            format_strings: Self::default_format_strings(),
21            targets: vec![RwLock::new(Box::new(io::stdout()))],
22        }
23    }
24
25    /// Create a logger with a specific log level
26    pub fn with_level(level: LogLevel) -> Self {
27        Self {
28            level,
29            time_format: "%Y-%m-%d %H:%M:%S".to_string(),
30            format_strings: Self::default_format_strings(),
31            targets: vec![RwLock::new(Box::new(io::stdout()))],
32        }
33    }
34
35    /// Create a logger configured from environment variables
36    ///
37    /// This method checks for log level configuration in the following order:
38    /// 1. RUST_LOG environment variable (Rust convention)
39    /// 2. LOG_LEVEL environment variable (fallback)
40    ///
41    /// If neither is found or both are invalid, defaults to Info level.
42    pub fn from_env() -> Self {
43        use crate::utils::log_util::parse_log_level_from_env;
44        let level = parse_log_level_from_env();
45        Self::with_level(level)
46    }
47
48    /// Set the time format using chrono format string
49    ///
50    /// Common formats:
51    /// - `%Y-%m-%d %H:%M:%S` - "2025-09-14 16:57:00" (default)
52    /// - `%H:%M:%S` - "16:57:00"
53    /// - `%Y-%m-%d %H:%M:%S%.3f` - "2025-09-14 16:57:00.123"
54    /// - `%Y-%m-%d` - "2025-09-14"
55    pub fn time_format(mut self, format: &str) -> Self {
56        self.time_format = format.to_string();
57        self
58    }
59
60    /// Disable time prefix
61    ///
62    /// This sets the time format to empty string, effectively removing timestamps.
63    pub fn no_time_prefix(mut self) -> Self {
64        self.time_format = String::new();
65        self
66    }
67
68    /// Set custom format string for a specific log level
69    pub fn format_for_level(mut self, level: LogLevel, format: &str) -> Self {
70        self.format_strings.insert(level, format.to_string());
71        self
72    }
73
74    /// Set a target to write logs to (stdout) - replaces all existing targets
75    pub fn stdout(mut self) -> Self {
76        self.targets = vec![RwLock::new(Box::new(io::stdout()))];
77        self
78    }
79
80    /// Set a target to write logs to (stderr) - replaces all existing targets
81    pub fn stderr(mut self) -> Self {
82        self.targets = vec![RwLock::new(Box::new(io::stderr()))];
83        self
84    }
85
86    /// Add a file as a target - replaces all existing targets
87    pub fn file(mut self, path: &str) -> io::Result<Self> {
88        let file = std::fs::OpenOptions::new()
89            .create(true)
90            .append(true)
91            .open(path)?;
92        self.targets = vec![RwLock::new(Box::new(file))];
93        Ok(self)
94    }
95
96    /// Set a custom Write target - replaces all existing targets
97    ///
98    /// This allows you to set any type that implements `Write + Send + Sync` as the only logging target.
99    /// Useful for custom writers, network streams, or any other Write implementor.
100    ///
101    /// # Examples
102    ///
103    /// ```rust
104    /// use nonblocking_logger::Logger;
105    /// use std::io::Write;
106    ///
107    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
108    ///     let mut buffer = Vec::new();
109    ///     let logger = Logger::new().custom(buffer);
110    ///     
111    ///     logger.info("This will be written to the custom target only")?;
112    ///     Ok(())
113    /// }
114    /// ```
115    pub fn custom<W>(mut self, target: W) -> Self 
116    where 
117        W: Write + Send + Sync + 'static 
118    {
119        self.targets = vec![RwLock::new(Box::new(target))];
120        self
121    }
122
123    /// Add a stdout target to existing targets
124    pub fn add_stdout(mut self) -> Self {
125        self.targets.push(RwLock::new(Box::new(io::stdout())));
126        self
127    }
128
129    /// Add a stderr target to existing targets
130    pub fn add_stderr(mut self) -> Self {
131        self.targets.push(RwLock::new(Box::new(io::stderr())));
132        self
133    }
134
135    /// Add a file target to existing targets
136    pub fn add_file(mut self, path: &str) -> io::Result<Self> {
137        let file = std::fs::OpenOptions::new()
138            .create(true)
139            .append(true)
140            .open(path)?;
141        self.targets.push(RwLock::new(Box::new(file)));
142        Ok(self)
143    }
144
145    /// Add a custom Write target to existing targets
146    ///
147    /// This allows you to add any type that implements `Write + Send + Sync` as a logging target.
148    /// Useful for custom writers, network streams, or any other Write implementor.
149    ///
150    /// # Examples
151    ///
152    /// ```rust
153    /// use nonblocking_logger::Logger;
154    /// use std::io::Write;
155    ///
156    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
157    ///     let mut buffer = Vec::new();
158    ///     let logger = Logger::new().add_target(buffer);
159    ///     
160    ///     logger.info("This will be written to the custom target")?;
161    ///     Ok(())
162    /// }
163    /// ```
164    pub fn add_target<W>(mut self, target: W) -> Self 
165    where 
166        W: Write + Send + Sync + 'static 
167    {
168        self.targets.push(RwLock::new(Box::new(target)));
169        self
170    }
171
172
173    /// Log a message (always outputs, no level filtering)
174    pub fn log(&self, message: &str) -> io::Result<()> {
175        let formatted = self.format_message_simple(message);
176
177        for target in &self.targets {
178            let mut target = target.write().unwrap();
179            writeln!(target, "{}", formatted)?;
180            target.flush()?;
181        }
182
183        Ok(())
184    }
185
186    /// Log a message with lazy evaluation (always outputs, no level filtering)
187    ///
188    /// This is more efficient when the message requires expensive computation, as the closure
189    /// will only be executed if the log level allows the message to be output.
190    ///
191    /// # Examples
192    ///
193    /// ```rust
194    /// use nonblocking_logger::Logger;
195    ///
196    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
197    ///     let logger = Logger::new();
198    ///     
199    ///     // This expensive computation will always run and output
200    ///     logger.log_lazy(|| {
201    ///         format!("Expensive computation result: {}", "some_expensive_result")
202    ///     })?;
203    ///     
204    ///     Ok(())
205    /// }
206    /// ```
207    pub fn log_lazy<F>(&self, message_fn: F) -> io::Result<()>
208    where
209        F: FnOnce() -> String,
210    {
211        let message = message_fn();
212        self.log(&message)
213    }
214
215
216    /// Log a message with lazy evaluation and specific level (with filtering)
217    pub(crate) fn log_lazy_with_level<F>(&self, level: LogLevel, message_fn: F) -> io::Result<()>
218    where
219        F: FnOnce() -> String,
220    {
221        if level < self.level {
222            return Ok(());
223        }
224
225        let message = message_fn();
226        self.log_with_level(level, &message)
227    }
228
229    /// Log a message with a specific level (with filtering)
230    pub(crate) fn log_with_level(&self, level: LogLevel, message: &str) -> io::Result<()> {
231        if level < self.level {
232            return Ok(());
233        }
234
235        let formatted = self.format_message(level, message);
236
237        for target in &self.targets {
238            let mut target = target.write().unwrap();
239            writeln!(target, "{}", formatted)?;
240            target.flush()?;
241        }
242
243        Ok(())
244    }
245
246    /// Convenience methods for each log level
247    pub fn error(&self, message: &str) -> io::Result<()> {
248        self.log_with_level(LogLevel::Error, message)
249    }
250
251    pub fn warning(&self, message: &str) -> io::Result<()> {
252        self.log_with_level(LogLevel::Warning, message)
253    }
254
255    pub fn info(&self, message: &str) -> io::Result<()> {
256        self.log_with_level(LogLevel::Info, message)
257    }
258
259    pub fn debug(&self, message: &str) -> io::Result<()> {
260        self.log_with_level(LogLevel::Debug, message)
261    }
262
263    pub fn trace(&self, message: &str) -> io::Result<()> {
264        self.log_with_level(LogLevel::Trace, message)
265    }
266
267    /// Convenience methods for each log level with lazy evaluation
268    ///
269    /// These methods only execute the closure if the log level is sufficient,
270    /// making them more efficient for expensive message computations.
271    pub fn error_lazy<F>(&self, message_fn: F) -> io::Result<()>
272    where
273        F: FnOnce() -> String,
274    {
275        self.log_lazy_with_level(LogLevel::Error, message_fn)
276    }
277
278    pub fn warning_lazy<F>(&self, message_fn: F) -> io::Result<()>
279    where
280        F: FnOnce() -> String,
281    {
282        self.log_lazy_with_level(LogLevel::Warning, message_fn)
283    }
284
285    pub fn info_lazy<F>(&self, message_fn: F) -> io::Result<()>
286    where
287        F: FnOnce() -> String,
288    {
289        self.log_lazy_with_level(LogLevel::Info, message_fn)
290    }
291
292    pub fn debug_lazy<F>(&self, message_fn: F) -> io::Result<()>
293    where
294        F: FnOnce() -> String,
295    {
296        self.log_lazy_with_level(LogLevel::Debug, message_fn)
297    }
298
299    pub fn trace_lazy<F>(&self, message_fn: F) -> io::Result<()>
300    where
301        F: FnOnce() -> String,
302    {
303        self.log_lazy_with_level(LogLevel::Trace, message_fn)
304    }
305
306    /// Set the log level
307    pub fn set_level(&mut self, level: LogLevel) {
308        self.level = level;
309    }
310
311    /// Set the time format using chrono format string
312    ///
313    /// Common formats:
314    /// - `%Y-%m-%d %H:%M:%S` - "2025-09-14 16:57:00" (default)
315    /// - `%H:%M:%S` - "16:57:00"
316    /// - `%Y-%m-%d %H:%M:%S%.3f` - "2025-09-14 16:57:00.123"
317    /// - `%Y-%m-%d` - "2025-09-14"
318    pub fn set_time_format(&mut self, format: &str) {
319        self.time_format = format.to_string();
320    }
321
322    /// Disable time prefix
323    ///
324    /// This sets the time format to empty string, effectively removing timestamps.
325    pub fn disable_time_prefix(&mut self) {
326        self.time_format = String::new();
327    }
328
329    /// Set custom format string for a specific log level
330    pub fn set_format_for_level(&mut self, level: LogLevel, format: &str) {
331        self.format_strings.insert(level, format.to_string());
332    }
333
334    /// Get the current log level
335    pub fn level(&self) -> LogLevel {
336        self.level
337    }
338
339    /// Get the current log level (alias for level for compatibility)
340    pub fn get_level(&self) -> LogLevel {
341        self.level
342    }
343
344    /// Clear all targets
345    pub fn clear_targets(mut self) -> Self {
346        self.targets.clear();
347        self
348    }
349
350    fn default_format_strings() -> HashMap<LogLevel, String> {
351        let mut formats = HashMap::new();
352        formats.insert(LogLevel::Error, "{time} [{level}] {message}".to_string());
353        formats.insert(LogLevel::Warning, "{time} [{level}] {message}".to_string());
354        formats.insert(LogLevel::Info, "{time} [{level}] {message}".to_string());
355        formats.insert(LogLevel::Debug, "{time} [{level}] {message}".to_string());
356        formats.insert(LogLevel::Trace, "{time} [{level}] {message}".to_string());
357        formats
358    }
359
360    fn format_message(&self, level: LogLevel, message: &str) -> String {
361        let format_string = self
362            .format_strings
363            .get(&level)
364            .unwrap_or(&self.format_strings[&LogLevel::Info])
365            .clone();
366
367        let time_str = if self.time_format.is_empty() {
368            String::new()
369        } else {
370            use simple_datetime_rs::{DateTime, Format};
371            DateTime::now()
372                .format(&self.time_format)
373                .unwrap_or_default()
374        };
375
376        format_string
377            .replace("{time}", &time_str)
378            .replace("{level}", &level.to_string())
379            .replace("{message}", message)
380    }
381
382    fn format_message_simple(&self, message: &str) -> String {
383        if self.time_format.is_empty() {
384            message.to_string()
385        } else {
386            use simple_datetime_rs::{DateTime, Format};
387            let time_str = DateTime::now()
388                .format(&self.time_format)
389                .unwrap_or_default();
390            format!("{} {}", time_str, message)
391        }
392    }
393}
394
395impl Default for Logger {
396    fn default() -> Self {
397        Self::new()
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use std::io::Cursor;
405
406    /// Create a test logger that writes to a Cursor<Vec<u8>> instead of stdout
407    /// This allows us to verify the actual log output in tests
408    fn create_test_logger() -> (Logger, std::io::Cursor<Vec<u8>>) {
409        let cursor = Cursor::new(Vec::<u8>::new());
410        let cursor_clone = Cursor::new(Vec::<u8>::new());
411
412        let logger = Logger {
413            level: LogLevel::Info,
414            time_format: String::new(), // No time prefix for cleaner tests
415            format_strings: Logger::default_format_strings(),
416            targets: vec![RwLock::new(Box::new(cursor_clone))],
417        };
418
419        (logger, cursor)
420    }
421
422    /// Create a test logger with a specific log level
423    fn create_test_logger_with_level(level: LogLevel) -> (Logger, std::io::Cursor<Vec<u8>>) {
424        let (mut logger, cursor) = create_test_logger();
425        logger.level = level;
426        (logger, cursor)
427    }
428
429    /// Create a test logger that allows us to capture and verify the actual log output
430    /// This is useful for testing the actual formatted output
431    fn create_capturable_test_logger() -> (Logger, std::sync::Arc<std::sync::Mutex<Vec<u8>>>) {
432        use std::io::Write;
433
434        let buffer = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
435        let buffer_clone = buffer.clone();
436
437        struct CapturingWriter {
438            buffer: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
439        }
440
441        impl Write for CapturingWriter {
442            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
443                self.buffer.lock().unwrap().extend_from_slice(buf);
444                Ok(buf.len())
445            }
446
447            fn flush(&mut self) -> std::io::Result<()> {
448                Ok(())
449            }
450        }
451
452        let logger = Logger {
453            level: LogLevel::Info,
454            time_format: String::new(), // No time prefix for cleaner tests
455            format_strings: Logger::default_format_strings(),
456            targets: vec![RwLock::new(Box::new(CapturingWriter {
457                buffer: buffer_clone,
458            }))],
459        };
460
461        (logger, buffer)
462    }
463
464    #[test]
465    fn test_simple_logging() -> io::Result<()> {
466        let (logger, _cursor) = create_test_logger();
467
468        assert_eq!(logger.level(), LogLevel::Info);
469
470        logger.info("Hello, world!")?;
471        logger.warning("This is a warning")?;
472        logger.error("This is an error")?;
473
474        assert_eq!(logger.level(), LogLevel::Info);
475
476        Ok(())
477    }
478
479    #[test]
480    fn test_time_format() -> io::Result<()> {
481        let (mut logger, _cursor) = create_test_logger();
482        logger.time_format = "%Y-%m-%d %H:%M:%S".to_string();
483
484        assert_eq!(logger.level(), LogLevel::Info);
485
486        logger.info("Test message")?;
487
488        logger.warning("Another test message")?;
489
490        Ok(())
491    }
492
493    #[test]
494    fn test_custom_format() -> io::Result<()> {
495        let (mut logger, _cursor) = create_test_logger();
496        logger
497            .format_strings
498            .insert(LogLevel::Error, "ERROR: {message}".to_string());
499
500        assert_eq!(logger.level(), LogLevel::Info);
501
502        logger.error("Something went wrong")?;
503
504        logger.info("This should use default format")?;
505        logger.warning("This should also use default format")?;
506
507        Ok(())
508    }
509
510    #[test]
511    fn test_log_level_filtering() -> io::Result<()> {
512        let (logger, _cursor) = create_test_logger_with_level(LogLevel::Warning);
513
514        assert_eq!(logger.level(), LogLevel::Warning);
515
516        logger.info("This should not appear")?;
517        logger.debug("This should not appear")?;
518        logger.trace("This should not appear")?;
519
520        logger.warning("This should appear")?;
521        logger.error("This should also appear")?;
522
523        assert_eq!(logger.level(), LogLevel::Warning);
524
525        Ok(())
526    }
527
528    #[test]
529    fn test_multiple_loggers() -> io::Result<()> {
530        let (logger1, _cursor1) = create_test_logger();
531        let (logger2, _cursor2) = create_test_logger_with_level(LogLevel::Warning);
532
533        assert_eq!(logger1.level(), LogLevel::Info);
534        assert_eq!(logger2.level(), LogLevel::Warning);
535
536        logger1.info("Message from logger 1")?;
537        logger2.warning("Message from logger 2")?;
538
539        logger1.info("Another message from logger 1")?;
540
541        logger2.info("This should be filtered by logger 2")?;
542
543        assert_eq!(logger1.level(), LogLevel::Info);
544        assert_eq!(logger2.level(), LogLevel::Warning);
545
546        Ok(())
547    }
548
549    #[test]
550    fn test_lazy_logging_execution() -> io::Result<()> {
551        let (logger, _cursor) = create_test_logger_with_level(LogLevel::Info);
552
553        let mut expensive_called = false;
554
555        logger.debug_lazy(|| {
556            expensive_called = true;
557            "This should not be computed".to_string()
558        })?;
559
560        assert!(
561            !expensive_called,
562            "Expensive computation should not have been called"
563        );
564
565        expensive_called = false;
566
567        logger.info_lazy(|| {
568            expensive_called = true;
569            "This should be computed".to_string()
570        })?;
571
572        assert!(
573            expensive_called,
574            "Expensive computation should have been called"
575        );
576
577        Ok(())
578    }
579
580    #[test]
581    fn test_lazy_logging_with_expensive_computation() -> io::Result<()> {
582        let (logger, _cursor) = create_test_logger_with_level(LogLevel::Warning);
583
584        use std::cell::RefCell;
585        let computation_count = RefCell::new(0);
586
587        logger.trace_lazy(|| {
588            *computation_count.borrow_mut() += 1;
589            "Trace message".to_string()
590        })?;
591        logger.debug_lazy(|| {
592            *computation_count.borrow_mut() += 1;
593            "Debug message".to_string()
594        })?;
595        logger.info_lazy(|| {
596            *computation_count.borrow_mut() += 1;
597            "Info message".to_string()
598        })?;
599
600        assert_eq!(
601            *computation_count.borrow(),
602            0,
603            "No expensive computations should have been executed"
604        );
605
606        logger.warning_lazy(|| {
607            *computation_count.borrow_mut() += 1;
608            "Warning message".to_string()
609        })?;
610        assert_eq!(
611            *computation_count.borrow(),
612            1,
613            "One expensive computation should have been executed"
614        );
615
616        logger.error_lazy(|| {
617            *computation_count.borrow_mut() += 1;
618            "Error message".to_string()
619        })?;
620        assert_eq!(
621            *computation_count.borrow(),
622            2,
623            "Two expensive computations should have been executed"
624        );
625
626        Ok(())
627    }
628
629    #[test]
630    fn test_lazy_logging_vs_regular_logging() -> io::Result<()> {
631        let (logger, _cursor) = create_test_logger_with_level(LogLevel::Warning);
632        let mut lazy_called = false;
633
634        logger.warning_lazy(|| {
635            lazy_called = true;
636            "Lazy warning".to_string()
637        })?;
638
639        logger.warning("Regular warning")?;
640        assert!(lazy_called, "Lazy closure should have been called");
641
642        Ok(())
643    }
644
645    #[test]
646    fn test_multi_target_logging() -> io::Result<()> {
647        let (logger, _cursor) = create_test_logger();
648
649        logger.info("Test message for multi-target logging")?;
650
651        Ok(())
652    }
653
654    #[test]
655    fn test_multi_target_lazy_logging() -> io::Result<()> {
656        let (logger, _cursor) = create_test_logger();
657
658        let mut call_count = 0;
659
660        logger.info_lazy(|| {
661            call_count += 1;
662            "Lazy message for multiple targets".to_string()
663        })?;
664
665        assert_eq!(call_count, 1, "Lazy closure should be called only once");
666
667        Ok(())
668    }
669
670    #[test]
671    fn test_log_output_verification() -> io::Result<()> {
672        let (logger, buffer) = create_capturable_test_logger();
673
674        logger.info("Test message")?;
675
676        std::thread::sleep(std::time::Duration::from_millis(10));
677
678        let captured = buffer.lock().unwrap();
679        let output = String::from_utf8_lossy(&captured);
680
681        assert!(
682            output.contains("Test message"),
683            "Output should contain the log message"
684        );
685        assert!(
686            output.contains("[INFO]"),
687            "Output should contain the log level"
688        );
689
690        Ok(())
691    }
692
693    #[test]
694    fn test_custom_format_output_verification() -> io::Result<()> {
695        let (mut logger, buffer) = create_capturable_test_logger();
696
697        logger
698            .format_strings
699            .insert(LogLevel::Error, "ERROR: {message}".to_string());
700
701        logger.error("Something went wrong")?;
702
703        std::thread::sleep(std::time::Duration::from_millis(10));
704
705        let captured = buffer.lock().unwrap();
706        let output = String::from_utf8_lossy(&captured);
707
708        assert!(
709            output.contains("ERROR: Something went wrong"),
710            "Output should contain the custom formatted message"
711        );
712
713        Ok(())
714    }
715}