1use chrono::{Duration, Local, NaiveDate};
2use serde::Serialize;
3use std::io::Write;
4use std::path::PathBuf;
5use std::sync::mpsc as std_mpsc;
6use std::sync::{Arc, Weak};
7use tokio::{fs, time};
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
13pub enum LogLevel {
14 Debug,
15 Info,
16 Warn,
17 Error,
18}
19
20impl LogLevel {
21 fn as_str(&self) -> &'static str {
22 match self {
23 LogLevel::Debug => "DEBUG",
24 LogLevel::Info => "INFO",
25 LogLevel::Warn => "WARN",
26 LogLevel::Error => "ERROR",
27 }
28 }
29}
30
31impl std::fmt::Display for LogLevel {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.write_str(self.as_str())
34 }
35}
36
37#[derive(Debug, Serialize)]
38struct LogEntry {
39 timestamp: String,
40 level: String,
41 app: String,
42 message: String,
43 module: String,
44 file: String,
45 line: u32,
46 thread: String,
47}
48
49enum LogMessage {
50 Entry(LogEntry),
51 Shutdown,
52}
53
54pub struct LoggerManager {
84 app_name: String,
85 log_path: PathBuf,
86 retention_days: i64,
87 level: LogLevel,
88 sender: std_mpsc::SyncSender<LogMessage>,
89 writer_thread: std::sync::Mutex<Option<std::thread::JoinHandle<()>>>,
90}
91
92pub struct LoggerManagerBuilder {
96 app_name: String,
97 level: LogLevel,
98 log_path: PathBuf,
99 retention_days: i64,
100}
101
102impl LoggerManagerBuilder {
103 pub fn level(mut self, level: LogLevel) -> Self {
105 self.level = level;
106 self
107 }
108
109 pub fn log_path(mut self, path: impl Into<PathBuf>) -> Self {
111 self.log_path = path.into();
112 self
113 }
114
115 pub fn retention_days(mut self, days: i64) -> Self {
117 self.retention_days = days;
118 self
119 }
120
121 pub async fn build(self) -> Arc<LoggerManager> {
123 if let Err(e) = fs::create_dir_all(&self.log_path).await {
124 eprintln!(
125 "[LoggerManager] Failed to create log directory {:?}: {}",
126 self.log_path, e
127 );
128 }
129
130 let (sender, receiver) = std_mpsc::sync_channel::<LogMessage>(4096);
131 let (log_path, app_name) = (self.log_path.clone(), self.app_name.clone());
132
133 let manager = Arc::new(LoggerManager {
134 app_name: self.app_name,
135 log_path: self.log_path,
136 retention_days: self.retention_days,
137 level: self.level,
138 sender,
139 writer_thread: std::sync::Mutex::new(None),
140 });
141
142 *manager.writer_thread.lock().unwrap() = Some(std::thread::spawn(move || {
143 LoggerManager::writer_fn(receiver, log_path, app_name)
144 }));
145
146 manager.cleanup_old_logs().await;
147
148 let weak: Weak<LoggerManager> = Arc::downgrade(&manager);
149 tokio::spawn(async move {
150 let mut interval = time::interval(time::Duration::from_secs(86_400));
151 interval.tick().await;
152 loop {
153 interval.tick().await;
154 match weak.upgrade() {
155 Some(m) => m.cleanup_old_logs().await,
156 None => break,
157 }
158 }
159 });
160
161 let panic_sender = manager.sender.clone();
162 let panic_app = manager.app_name.clone();
163 std::panic::set_hook(Box::new(move |info| {
164 let loc = info
165 .location()
166 .map(|l| format!(" at {}:{}", l.file(), l.line()))
167 .unwrap_or_default();
168 let payload = info
169 .payload()
170 .downcast_ref::<&str>()
171 .map(|s| (*s).to_string())
172 .or_else(|| info.payload().downcast_ref::<String>().cloned())
173 .unwrap_or_else(|| "unknown panic".to_string());
174 let _ = panic_sender.try_send(LogMessage::Entry(LogEntry {
175 timestamp: Local::now().to_rfc3339(),
176 level: "PANIC".to_string(),
177 app: panic_app.clone(),
178 message: format!("panic{}: {}", loc, payload),
179 module: String::new(),
180 file: String::new(),
181 line: 0,
182 thread: std::thread::current()
183 .name()
184 .unwrap_or("unnamed")
185 .to_string(),
186 }));
187 std::thread::sleep(std::time::Duration::from_millis(200));
188 }));
189
190 manager._log_internal(
191 LogLevel::Info,
192 format!(
193 "LoggerManager started | app={} path={:?} retain={}d level={}",
194 manager.app_name, manager.log_path, manager.retention_days, manager.level
195 ),
196 module_path!(),
197 file!(),
198 line!(),
199 );
200
201 manager
202 }
203}
204
205impl LoggerManager {
206 pub fn builder(app_name: impl Into<String>) -> LoggerManagerBuilder {
222 LoggerManagerBuilder {
223 app_name: app_name.into(),
224 level: LogLevel::Info,
225 log_path: PathBuf::from("logs"),
226 retention_days: 7,
227 }
228 }
229
230 #[doc(hidden)]
234 pub fn _log_internal(
235 &self,
236 level: LogLevel,
237 message: String,
238 module: &str,
239 file: &str,
240 line: u32,
241 ) {
242 if level < self.level {
243 return;
244 }
245 let _ = self.sender.try_send(LogMessage::Entry(LogEntry {
246 timestamp: Local::now().to_rfc3339(),
247 level: level.as_str().to_string(),
248 app: self.app_name.clone(),
249 message,
250 module: module.to_string(),
251 file: file.to_string(),
252 line,
253 thread: std::thread::current()
254 .name()
255 .unwrap_or("unnamed")
256 .to_string(),
257 }));
258 }
259
260 #[track_caller]
263 pub fn log(&self, level: LogLevel, message: impl Into<String>) {
264 let loc = std::panic::Location::caller();
265 self._log_internal(level, message.into(), "", loc.file(), loc.line());
266 }
267
268 #[track_caller]
269 pub fn debug(&self, msg: impl Into<String>) {
270 self.log(LogLevel::Debug, msg);
271 }
272 #[track_caller]
273 pub fn info(&self, msg: impl Into<String>) {
274 self.log(LogLevel::Info, msg);
275 }
276 #[track_caller]
277 pub fn warn(&self, msg: impl Into<String>) {
278 self.log(LogLevel::Warn, msg);
279 }
280 #[track_caller]
281 pub fn error(&self, msg: impl Into<String>) {
282 self.log(LogLevel::Error, msg);
283 }
284
285 fn writer_fn(receiver: std_mpsc::Receiver<LogMessage>, log_path: PathBuf, app_name: String) {
288 if let Err(e) = std::fs::create_dir_all(&log_path) {
289 eprintln!(
290 "[LoggerManager] Cannot create log dir {:?}: {}",
291 log_path, e
292 );
293 }
294 while let Ok(msg) = receiver.recv() {
295 let LogMessage::Entry(entry) = msg else { break };
296
297 let (pad, color) = match entry.level.as_str() {
299 "DEBUG" => ("DEBUG", "\x1b[90m"),
300 "INFO" => ("INFO ", "\x1b[32m"),
301 "WARN" => ("WARN ", "\x1b[33m"),
302 "ERROR" => ("ERROR", "\x1b[31m"),
303 "PANIC" => ("PANIC", "\x1b[1;31m"),
304 other => (other, ""),
305 };
306 let loc = if !entry.file.is_empty() && entry.line > 0 {
307 format!(" \x1b[2m{}:{}\x1b[0m", entry.file, entry.line)
308 } else if !entry.module.is_empty() {
309 format!(" \x1b[2m{}\x1b[0m", entry.module)
310 } else {
311 String::new()
312 };
313 println!(
314 "\x1b[2m{}\x1b[0m {}{}\x1b[0m \x1b[1m{}\x1b[0m{} {}",
315 Local::now().format("%H:%M:%S"),
316 color,
317 pad,
318 entry.app,
319 loc,
320 entry.message,
321 );
322
323 let file_path = log_path.join(format!(
325 "{}_{}.jsonl",
326 Local::now().format("%Y-%m-%d"),
327 app_name
328 ));
329 if let Ok(json) = serde_json::to_string(&entry) {
330 match std::fs::OpenOptions::new()
331 .create(true)
332 .append(true)
333 .open(&file_path)
334 {
335 Ok(mut f) => {
336 if let Err(e) = writeln!(f, "{}", json) {
337 eprintln!("[LoggerManager] Write error: {}", e);
338 }
339 }
340 Err(e) => eprintln!("[LoggerManager] Cannot open {:?}: {}", file_path, e),
341 }
342 }
343 }
344 }
345
346 async fn cleanup_old_logs(&self) {
347 let cutoff = (Local::now() - Duration::days(self.retention_days)).date_naive();
348 let mut entries = match fs::read_dir(&self.log_path).await {
349 Ok(e) => e,
350 Err(e) => {
351 eprintln!("[LoggerManager] Cannot read log dir: {}", e);
352 return;
353 }
354 };
355 loop {
356 match entries.next_entry().await {
357 Ok(Some(entry)) => {
358 let path = entry.path();
359 if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
360 continue;
361 }
362 let is_old = path
363 .file_name()
364 .and_then(|n| n.to_str())
365 .and_then(|n| n.get(..10))
366 .and_then(|d| NaiveDate::parse_from_str(d, "%Y-%m-%d").ok())
367 .map_or(false, |d| d < cutoff);
368 if is_old {
369 if let Err(e) = fs::remove_file(&path).await {
370 eprintln!("[LoggerManager] Failed to delete {:?}: {}", path, e);
371 }
372 }
373 }
374 Ok(None) => break,
375 Err(e) => {
376 eprintln!("[LoggerManager] Dir read error: {}", e);
377 break;
378 }
379 }
380 }
381 }
382}
383
384#[macro_export]
388macro_rules! lgr_debug {
389 ($logger:expr, $($arg:tt)*) => {
390 $logger._log_internal(
391 $crate::utils::logger_manager::LogLevel::Debug,
392 format!($($arg)*), module_path!(), file!(), line!(),
393 )
394 };
395}
396
397#[macro_export]
399macro_rules! lgr_info {
400 ($logger:expr, $($arg:tt)*) => {
401 $logger._log_internal(
402 $crate::utils::logger_manager::LogLevel::Info,
403 format!($($arg)*), module_path!(), file!(), line!(),
404 )
405 };
406}
407
408#[macro_export]
410macro_rules! lgr_warn {
411 ($logger:expr, $($arg:tt)*) => {
412 $logger._log_internal(
413 $crate::utils::logger_manager::LogLevel::Warn,
414 format!($($arg)*), module_path!(), file!(), line!(),
415 )
416 };
417}
418
419#[macro_export]
421macro_rules! lgr_error {
422 ($logger:expr, $($arg:tt)*) => {
423 $logger._log_internal(
424 $crate::utils::logger_manager::LogLevel::Error,
425 format!($($arg)*), module_path!(), file!(), line!(),
426 )
427 };
428}
429
430impl Drop for LoggerManager {
431 fn drop(&mut self) {
432 let _ = self.sender.send(LogMessage::Shutdown);
433 if let Ok(mut guard) = self.writer_thread.lock() {
434 if let Some(handle) = guard.take() {
435 let _ = handle.join();
436 }
437 }
438 }
439}
440
441#[cfg(test)]
444mod tests {
445 use super::*;
446 use tempfile::tempdir;
447
448 #[tokio::test]
449 async fn test_creates_log_file() {
450 let dir = tempdir().unwrap();
451 let logger = LoggerManager::builder("testapp")
452 .level(LogLevel::Debug)
453 .log_path(dir.path())
454 .retention_days(7)
455 .build()
456 .await;
457
458 logger.info("hello from test");
459 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
460
461 let date = Local::now().format("%Y-%m-%d");
462 let log_file = dir.path().join(format!("{}_testapp.jsonl", date));
463 assert!(log_file.exists(), "log file should have been created");
464
465 let content = std::fs::read_to_string(&log_file).unwrap();
466 assert!(content.contains("\"level\":\"INFO\""));
467 assert!(content.contains("hello from test"));
468 }
469
470 #[tokio::test]
471 async fn test_level_filtering() {
472 let dir = tempdir().unwrap();
473 let logger = LoggerManager::builder("filterapp")
474 .level(LogLevel::Warn)
475 .log_path(dir.path())
476 .retention_days(7)
477 .build()
478 .await;
479
480 logger.debug("ignored debug");
481 logger.info("ignored info");
482 logger.warn("visible warn");
483 logger.error("visible error");
484 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
485
486 let date = Local::now().format("%Y-%m-%d");
487 let log_file = dir.path().join(format!("{}_filterapp.jsonl", date));
488 let content = std::fs::read_to_string(&log_file).unwrap();
489 assert!(!content.contains("ignored"));
490 assert!(content.contains("visible warn"));
491 assert!(content.contains("visible error"));
492 }
493
494 #[tokio::test]
495 async fn test_creates_directory_if_missing() {
496 let dir = tempdir().unwrap();
497 let nested = dir.path().join("deep/nested/logs");
498 let logger = LoggerManager::builder("nestapp")
499 .log_path(nested.clone())
500 .build()
501 .await;
502 logger.info("test");
503 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
504 assert!(nested.exists());
505 }
506
507 #[tokio::test]
508 async fn test_json_format() {
509 let dir = tempdir().unwrap();
510 let logger = LoggerManager::builder("jsonapp")
511 .level(LogLevel::Debug)
512 .log_path(dir.path())
513 .build()
514 .await;
515 logger.error("boom");
516 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
517
518 let date = Local::now().format("%Y-%m-%d");
519 let log_file = dir.path().join(format!("{}_jsonapp.jsonl", date));
520 let content = std::fs::read_to_string(&log_file).unwrap();
521 let line = content.lines().find(|l| l.contains("boom")).unwrap();
522 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
523 assert_eq!(parsed["level"], "ERROR");
524 assert_eq!(parsed["app"], "jsonapp");
525 assert_eq!(parsed["message"], "boom");
526 assert!(parsed["timestamp"].is_string());
527 }
528
529 #[tokio::test]
530 async fn test_old_logs_are_deleted() {
531 let dir = tempdir().unwrap();
532 let old_file = dir.path().join("2000-01-01_oldapp.jsonl");
534 std::fs::write(&old_file, "old\n").unwrap();
535
536 let logger = LoggerManager::builder("oldapp")
537 .log_path(dir.path())
538 .retention_days(7)
539 .build()
540 .await;
541 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
543 assert!(!old_file.exists(), "old log file should have been deleted");
544 drop(logger);
545 }
546}