Skip to main content

daily_logger/
logger.rs

1// logger.rs
2
3use chrono::{Datelike, Local};
4use log::{kv::Key, Level, Log, Metadata, Record};
5use once_cell::sync::Lazy;
6
7use std::{
8    collections::{HashMap, VecDeque},
9    fs::{File, OpenOptions},
10    io::{BufWriter, Write},
11    path::PathBuf,
12    sync::Mutex,
13};
14
15const MAX_CACHE_SIZE: usize = 32;
16static LOGGER: Lazy<DailyLogger> = Lazy::new(DailyLogger::new);
17static FILE_CACHE: Lazy<Mutex<FileCache>> =
18    Lazy::new(|| Mutex::new(FileCache::new(MAX_CACHE_SIZE)));
19
20pub fn init_logger(stdout_level: log::LevelFilter, file_level: log::LevelFilter, base_path: impl Into<PathBuf>) {
21    LOGGER.set_base_path(base_path.into());
22    LOGGER.set_levels(stdout_level, file_level);
23    log::set_logger(&*LOGGER).unwrap();
24    log::set_max_level(stdout_level.max(file_level));
25}
26
27pub struct DailyLogger {
28    base_path: Mutex<Option<PathBuf>>,
29    stdout_level: Mutex<log::LevelFilter>,
30    file_level: Mutex<log::LevelFilter>,
31}
32
33impl DailyLogger {
34    fn new() -> Self {
35        Self {
36            base_path: Mutex::new(None),
37            stdout_level: Mutex::new(log::LevelFilter::Info),
38            file_level: Mutex::new(log::LevelFilter::Info),
39        }
40    }
41
42    fn set_base_path(&self, path: PathBuf) {
43        let mut base = self.base_path.lock().unwrap();
44        *base = Some(path);
45    }
46
47    fn get_base_path(&self) -> Option<PathBuf> {
48        self.base_path.lock().unwrap().clone()
49    }
50
51    fn set_levels(&self, stdout_level: log::LevelFilter, file_level: log::LevelFilter) {
52        *self.stdout_level.lock().unwrap() = stdout_level;
53        *self.file_level.lock().unwrap() = file_level;
54    }
55}
56
57impl Log for DailyLogger {
58    fn enabled(&self, metadata: &Metadata) -> bool {
59        metadata.level() <= log::max_level()
60    }
61
62    fn log(&self, record: &Record) {
63        if !self.enabled(record.metadata()) {
64            return;
65        }
66
67        let stdout_level = *self.stdout_level.lock().unwrap();
68        let file_level = *self.file_level.lock().unwrap();
69
70        let now = Local::now();
71        let mut log_entry: String = format!(
72            "{}-{}|[{}]: {}",
73            now.to_rfc3339(),
74            record.level(),
75            record.target(),
76            record.args()
77        );
78
79
80        
81        let key_values = record.key_values();
82        if let Some(uuid) = key_values.get(Key::from("uuid")) {
83            let file_name = format!("order_{uuid}.log");
84
85            log_entry = format!(
86                "{}-{}|[{}]<{}>:{}",
87                now.to_rfc3339(),
88                record.level(),
89                record.target(),
90                uuid,
91                record.args()
92            );
93
94            if record.level() <= file_level {
95                write_to_file(&file_name, &log_entry, self.get_base_path());
96            }
97        }
98
99        if record.level() <= stdout_level {
100            let colored_entry = match record.level() {
101                Level::Error => format!("\x1b[31m{log_entry}\x1b[0m"),
102                Level::Warn => format!("\x1b[33m{log_entry}\x1b[0m"),
103                Level::Info => format!("\x1b[32m{log_entry}\x1b[0m"),
104                Level::Debug => format!("\x1b[37m{log_entry}\x1b[0m"),
105                Level::Trace => format!("\x1b[90m{log_entry}\x1b[0m"),
106            };
107            println!("{colored_entry}");
108        }
109
110        if record.level() <= file_level {
111            let date_log_name = format!("log_{}_{}_{}.log", now.year(), now.month(), now.day());
112            write_to_file(&date_log_name, &log_entry, self.get_base_path());
113        }
114    }
115
116    fn flush(&self) {}
117}
118
119fn write_to_file(file_name: &str, log_entry: &str, base_path: Option<PathBuf>) {
120    let mut cache = FILE_CACHE.lock().unwrap();
121    let full_path = base_path.map(|base| base.join(file_name)).unwrap_or_else(|| PathBuf::from(file_name));
122    let writer = cache.get_or_open(full_path);
123    let _ = writeln!(writer, "{log_entry}");
124    let _ = writer.flush();
125}
126
127struct FileCache {
128    max_size: usize,
129    files: HashMap<PathBuf, BufWriter<File>>,
130    order: VecDeque<PathBuf>,
131}
132
133impl FileCache {
134    fn new(max_size: usize) -> Self {
135        Self {
136            max_size,
137            files: HashMap::new(),
138            order: VecDeque::new(),
139        }
140    }
141
142    fn get_or_open(&mut self, path: PathBuf) -> &mut BufWriter<File> {
143        if self.files.contains_key(&path) {
144            self.order.retain(|f| f != &path);
145            self.order.push_back(path.clone());
146        } else {
147            if self.files.len() >= self.max_size {
148                if let Some(oldest) = self.order.pop_front() {
149                    self.files.remove(&oldest);
150                }
151            }
152
153            if let Some(parent) = path.parent() {
154                let _ = std::fs::create_dir_all(parent);
155            }
156
157            let file = OpenOptions::new()
158                .create(true)
159                .append(true)
160                .open(&path)
161                .expect("Failed to open log file");
162
163            let writer = BufWriter::with_capacity(1024, file);
164            self.files.insert(path.clone(), writer);
165            self.order.push_back(path.clone());
166        }
167
168        self.files.get_mut(&path).unwrap()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use log::{debug, error, info, trace, warn};
176    use std::fs;
177    use std::path::Path;
178    use std::sync::{Mutex, Once};
179    use std::thread;
180    use std::time::Duration;
181
182    static INIT_LOGGER: Once = Once::new();
183    static TEST_MUTEX: Mutex<()> = Mutex::new(());
184
185    fn cleanup_test_dir(test_dir: &Path) {
186        if test_dir.exists() {
187            let _ = fs::remove_dir_all(test_dir);
188        }
189    }
190
191    fn init_test_logger() {
192        INIT_LOGGER.call_once(|| {
193            init_logger(log::LevelFilter::Off, log::LevelFilter::Trace, "test_logs");
194        });
195    }
196
197    fn setup_test_dir(test_name: &str) -> (PathBuf, std::sync::MutexGuard<'static, ()>) {
198        let _guard = TEST_MUTEX.lock().unwrap();
199        init_test_logger();
200        let test_base = PathBuf::from(format!("test_logs_{}", test_name));
201        cleanup_test_dir(&test_base);
202        
203        LOGGER.set_base_path(test_base.clone());
204        thread::sleep(Duration::from_millis(50));
205        (test_base, _guard)
206    }
207
208    fn wait_for_file_operations() {
209        log::logger().flush();
210        thread::sleep(Duration::from_millis(100));
211    }
212
213    #[test]
214    fn test_daily_log_file_generation() {
215        let (test_base, _guard) = setup_test_dir("daily");
216        
217        info!(target: "daily_test", "Daily log message 1");
218        warn!(target: "daily_test", "Daily warning message");
219        error!(target: "daily_test", "Daily error message");
220        
221        wait_for_file_operations();
222        
223        let now = Local::now();
224        let daily_log = test_base.join(format!("log_{}_{}_{}.log", now.year(), now.month(), now.day()));
225        assert!(daily_log.exists(), "Daily log file should exist: {:?}", daily_log);
226        
227        let daily_content = fs::read_to_string(&daily_log).expect("Should read daily log");
228        assert!(daily_content.contains("Daily log message 1"));
229        assert!(daily_content.contains("Daily warning message"));
230        assert!(daily_content.contains("Daily error message"));
231        
232        cleanup_test_dir(&test_base);
233    }
234
235    #[test]
236    fn test_uuid_specific_order_logs() {
237        let (test_base, _guard) = setup_test_dir("uuid");
238        
239        let uuid1 = "test-order-123";
240        let uuid2 = "test-order-456";
241        
242        info!(target: "vending", uuid = uuid1; "Order {} started", uuid1);
243        debug!(target: "vending", uuid = uuid1; "Processing order {}", uuid1);
244        error!(target: "vending", uuid = uuid1; "Order {} failed", uuid1);
245        
246        info!(target: "payment", uuid = uuid2; "Payment for order {} initiated", uuid2);
247        warn!(target: "payment", uuid = uuid2; "Payment warning for {}", uuid2);
248        
249        wait_for_file_operations();
250        
251        let uuid1_file = test_base.join(format!("order_{}.log", uuid1));
252        let uuid2_file = test_base.join(format!("order_{}.log", uuid2));
253        
254        assert!(uuid1_file.exists(), "UUID1 log file should exist");
255        assert!(uuid2_file.exists(), "UUID2 log file should exist");
256        
257        let uuid1_content = fs::read_to_string(&uuid1_file).expect("Should read UUID1 log");
258        assert!(uuid1_content.contains(&format!("<{}>", uuid1)));
259        assert!(uuid1_content.contains("Order test-order-123 started"));
260        assert!(uuid1_content.contains("Processing order"));
261        assert!(uuid1_content.contains("Order test-order-123 failed"));
262        
263        let uuid2_content = fs::read_to_string(&uuid2_file).expect("Should read UUID2 log");
264        assert!(uuid2_content.contains(&format!("<{}>", uuid2)));
265        assert!(uuid2_content.contains("Payment for order test-order-456"));
266        assert!(uuid2_content.contains("Payment warning"));
267        
268        assert!(!uuid1_content.contains("Payment"));
269        assert!(!uuid2_content.contains("Order test-order-123"));
270        
271        cleanup_test_dir(&test_base);
272    }
273
274    #[test]
275    fn test_file_cache_functionality() {
276        let (test_base, _guard) = setup_test_dir("cache");
277        
278        for i in 0..35 {
279            let uuid = format!("cache-test-{:03}", i);
280            info!(target: "cache_test", uuid = uuid.as_str(); "Cache test message {}", i);
281        }
282        
283        wait_for_file_operations();
284        
285        for i in 0..35 {
286            let uuid = format!("cache-test-{:03}", i);
287            let file_path = test_base.join(format!("order_{}.log", uuid));
288            assert!(file_path.exists(), "Cache test file should exist for UUID {}", uuid);
289            
290            let content = fs::read_to_string(&file_path).expect("Should read cache test file");
291            assert!(content.contains(&format!("Cache test message {}", i)));
292        }
293        
294        cleanup_test_dir(&test_base);
295    }
296
297    #[test]
298    fn test_concurrent_logging() {
299        let (test_base, _guard) = setup_test_dir("concurrent");
300        
301        let handles: Vec<_> = (0..5).map(|thread_id| {
302            thread::spawn(move || {
303                for i in 0..5 {
304                    let uuid = format!("concurrent-{}-{}", thread_id, i);
305                    info!(target: "concurrent", uuid = uuid.as_str(); "Thread {} message {}", thread_id, i);
306                    info!(target: "concurrent", "Non-UUID message from thread {}", thread_id);
307                }
308            })
309        }).collect();
310        
311        for handle in handles {
312            handle.join().expect("Thread should complete");
313        }
314        
315        wait_for_file_operations();
316        
317        for thread_id in 0..5 {
318            for i in 0..5 {
319                let uuid = format!("concurrent-{}-{}", thread_id, i);
320                let uuid_file = test_base.join(format!("order_{}.log", uuid));
321                assert!(uuid_file.exists(), "Concurrent UUID file should exist for {}", uuid);
322                
323                let uuid_content = fs::read_to_string(&uuid_file).expect("Should read concurrent UUID file");
324                assert!(uuid_content.contains(&format!("Thread {} message {}", thread_id, i)));
325            }
326        }
327        
328        cleanup_test_dir(&test_base);
329    }
330
331    #[test]
332    fn test_log_format_validation() {
333        let (test_base, _guard) = setup_test_dir("format");
334        
335        let format_uuid = "format-test-uuid";
336        info!(target: "format_test", uuid = format_uuid; "Message with UUID");
337        warn!(target: "format_test", "Message without UUID");
338        
339        wait_for_file_operations();
340        
341        let now = Local::now();
342        let daily_log = test_base.join(format!("log_{}_{}_{}.log", now.year(), now.month(), now.day()));
343        let format_file = test_base.join("order_format-test-uuid.log");
344        
345        assert!(daily_log.exists(), "Daily log should exist");
346        assert!(format_file.exists(), "Format test UUID file should exist");
347        
348        let daily_content = fs::read_to_string(&daily_log).expect("Should read daily log");
349        let format_content = fs::read_to_string(&format_file).expect("Should read format test file");
350        
351        assert!(format_content.contains("INFO|[format_test]<format-test-uuid>:Message with UUID"));
352        assert!(!format_content.contains("Message without UUID"));
353        
354        assert!(daily_content.contains("WARN|[format_test]: Message without UUID"));
355        assert!(daily_content.contains("INFO|[format_test]<format-test-uuid>:Message with UUID"));
356        
357        let lines: Vec<&str> = daily_content.lines().collect();
358        for line in lines {
359            if !line.is_empty() {
360                assert!(line.contains("T"), "Log line should contain timestamp with 'T': {}", line);
361                assert!(line.contains("|"), "Log line should contain level separator '|': {}", line);
362            }
363        }
364        
365        cleanup_test_dir(&test_base);
366    }
367
368    #[test]
369    fn test_mixed_targets_and_levels() {
370        let (test_base, _guard) = setup_test_dir("mixed");
371        
372        let mixed_uuid = "mixed-test-uuid";
373        info!(target: "vending", uuid = mixed_uuid; "Vending machine info");
374        debug!(target: "vending", uuid = mixed_uuid; "Vending machine debug");
375        error!(target: "payment", uuid = mixed_uuid; "Payment error");
376        warn!(target: "ui", "UI warning without UUID");
377        trace!(target: "system", "System trace without UUID");
378        
379        wait_for_file_operations();
380        
381        let now = Local::now();
382        let daily_log = test_base.join(format!("log_{}_{}_{}.log", now.year(), now.month(), now.day()));
383        let mixed_file = test_base.join("order_mixed-test-uuid.log");
384        
385        assert!(daily_log.exists(), "Daily log should exist");
386        assert!(mixed_file.exists(), "Mixed test UUID file should exist");
387        
388        let daily_content = fs::read_to_string(&daily_log).expect("Should read daily log");
389        let mixed_content = fs::read_to_string(&mixed_file).expect("Should read mixed test file");
390        
391        assert!(mixed_content.contains("Vending machine info"));
392        assert!(mixed_content.contains("Vending machine debug"));
393        assert!(mixed_content.contains("Payment error"));
394        assert!(!mixed_content.contains("UI warning"));
395        assert!(!mixed_content.contains("System trace"));
396        
397        assert!(daily_content.contains("[vending]<mixed-test-uuid>"));
398        assert!(daily_content.contains("[payment]<mixed-test-uuid>"));
399        assert!(daily_content.contains("[ui]: UI warning"));
400        assert!(daily_content.contains("[system]: System trace"));
401        
402        cleanup_test_dir(&test_base);
403    }
404
405    #[test]
406    fn test_directory_creation() {
407        let (test_base, _guard) = setup_test_dir("directory");
408        let nested_uuid = "nested-dir-test";
409        info!(target: "nested_test", uuid = nested_uuid; "Testing nested directory creation");
410        wait_for_file_operations();
411
412        let nested_file = test_base.join("order_nested-dir-test.log");
413        assert!(
414            nested_file.exists(),
415            "Nested directory test file should exist"
416        );
417        let content = fs::read_to_string(&nested_file).expect("Should read nested test file");
418        assert!(content.contains("Testing nested directory creation"));
419        cleanup_test_dir(&test_base);
420    }
421}