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