1use 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}