cnc_rs/
logger.rs

1use env_logger::{Builder, Env};
2use std::{
3    fs::File,
4    io::{self, Write},
5};
6
7pub fn init_logger(verbose: bool) {
8    let env = Env::default()
9        .filter_or("MY_LOG_LEVEL", "info")
10        .write_style("MY_LOG_STYLE");
11
12    let mut builder = Builder::from_env(env);
13
14    builder.format(move |buf, record| {
15        let timestamp = buf.timestamp();
16        writeln!(
17            buf,
18            "[{timestamp}] {level}: {message}",
19            level = record.level(),
20            message = record.args()
21        )
22    });
23
24    if verbose {
25        // Redirect logs to both stdout and a file
26        let log_file = File::create("output.log").expect("Unable to create log file");
27        let dual_writer = DualWriter {
28            stdout: io::stdout(),
29            log_file,
30        };
31
32        builder.target(env_logger::Target::Pipe(Box::new(dual_writer)));
33    }
34
35    builder.init();
36}
37
38/// A writer that duplicates output to stdout and a log file.
39pub struct DualWriter {
40    stdout: io::Stdout,
41    log_file: File,
42}
43
44impl Write for DualWriter {
45    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
46        self.stdout.lock().write_all(buf)?;
47        self.log_file.write(buf)
48    }
49
50    fn flush(&mut self) -> io::Result<()> {
51        self.stdout.lock().flush()?;
52        self.log_file.flush()
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use env_logger::{Builder, Env};
59    use log::{debug, error, info, warn};
60    use std::{
61        fs::{self, File},
62        io::{self, Write},
63        thread,
64    };
65
66    use super::DualWriter;
67
68    fn init_logger(verbose: bool) {
69        let env = Env::default()
70            .filter_or("MY_LOG_LEVEL", "info")
71            .write_style("MY_LOG_STYLE");
72
73        let mut builder = Builder::from_env(env);
74        builder.format(move |buf, record| {
75            let timestamp = buf.timestamp();
76            writeln!(
77                buf,
78                "[{timestamp}] {level}: {message}",
79                level = record.level(),
80                message = record.args()
81            )
82        });
83        if verbose {
84            // Redirect logs to both stdout and a file
85            let log_file = File::create("output.log").expect("Unable to create log file");
86            let dual_writer = DualWriter {
87                stdout: io::stdout(),
88                log_file,
89            };
90
91            builder.target(env_logger::Target::Pipe(Box::new(dual_writer)));
92        }
93
94        builder.is_test(true).try_init().unwrap();
95    }
96
97    #[test]
98    fn test_logger_behavior() {
99        init_logger(true);
100
101        // Test logging functionality
102        let a = 1;
103        let b = 2;
104
105        debug!("checking whether {} + {} = 3", a, b);
106        assert_eq!(3, a + b);
107
108        // Test log formatting
109        let msg = "Test message";
110        info!("{}", msg);
111        let log_contents = fs::read_to_string("output.log").expect("Unable to read log file");
112        assert!(log_contents.contains("Test message"));
113
114        // Test different log levels
115        info!("This is an info message");
116        warn!("This is a warning message");
117        error!("This is an error message");
118        let log_contents = fs::read_to_string("output.log").expect("Unable to read log file");
119        assert!(log_contents.contains("This is an info message"));
120        assert!(log_contents.contains("This is a warning message"));
121        assert!(log_contents.contains("This is an error message"));
122
123        // Test dual output
124        info!("Logging to both stdout and file");
125        let log_contents = fs::read_to_string("output.log").expect("Unable to read log file");
126        assert!(log_contents.contains("Logging to both stdout and file"));
127
128        // Test concurrent logging
129        let handles: Vec<_> = (0..10)
130            .map(|i| {
131                thread::spawn(move || {
132                    for _ in 0..10 {
133                        info!("Thread {} logging", i);
134                    }
135                })
136            })
137            .collect();
138
139        for handle in handles {
140            handle.join().unwrap();
141        }
142
143        let log_contents = fs::read_to_string("output.log").expect("Unable to read log file");
144        for i in 0..10 {
145            assert!(log_contents.contains(&format!("Thread {} logging", i)));
146        }
147
148        fs::remove_file("output.log").unwrap();
149    }
150}