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, OnceLock, Weak};
7use tokio::{fs, time};
8
9static GLOBAL: OnceLock<Arc<LoggerManager>> = OnceLock::new();
12
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
17pub enum LogLevel {
18 Debug,
19 Info,
20 Warn,
21 Error,
22}
23
24impl LogLevel {
25 fn as_str(&self) -> &'static str {
26 match self {
27 LogLevel::Debug => "DEBUG",
28 LogLevel::Info => "INFO",
29 LogLevel::Warn => "WARN",
30 LogLevel::Error => "ERROR",
31 }
32 }
33}
34
35impl std::fmt::Display for LogLevel {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.write_str(self.as_str())
38 }
39}
40
41#[derive(Debug, Serialize)]
42struct LogEntry {
43 timestamp: String,
44 level: String,
45 app: String,
46 message: String,
47 module: String,
48 file: String,
49 line: u32,
50 thread: String,
51}
52
53enum LogMessage {
54 Entry(LogEntry),
55 Shutdown,
56}
57
58pub struct LoggerManager {
88 app_name: String,
89 log_path: PathBuf,
90 retention_days: i64,
91 level: LogLevel,
92 sender: std_mpsc::SyncSender<LogMessage>,
93 writer_thread: std::sync::Mutex<Option<std::thread::JoinHandle<()>>>,
94}
95
96pub struct LoggerManagerBuilder {
100 app_name: String,
101 level: LogLevel,
102 log_path: PathBuf,
103 retention_days: i64,
104}
105
106impl LoggerManagerBuilder {
107 pub fn level(mut self, level: LogLevel) -> Self {
109 self.level = level;
110 self
111 }
112
113 pub fn log_path(mut self, path: impl Into<PathBuf>) -> Self {
115 self.log_path = path.into();
116 self
117 }
118
119 pub fn retention_days(mut self, days: i64) -> Self {
121 self.retention_days = days;
122 self
123 }
124
125 pub async fn build(self) -> Arc<LoggerManager> {
127 if let Err(e) = fs::create_dir_all(&self.log_path).await {
128 eprintln!(
129 "[LoggerManager] Failed to create log directory {:?}: {}",
130 self.log_path, e
131 );
132 }
133
134 let (sender, receiver) = std_mpsc::sync_channel::<LogMessage>(4096);
135 let (log_path, app_name) = (self.log_path.clone(), self.app_name.clone());
136
137 let manager = Arc::new(LoggerManager {
138 app_name: self.app_name,
139 log_path: self.log_path,
140 retention_days: self.retention_days,
141 level: self.level,
142 sender,
143 writer_thread: std::sync::Mutex::new(None),
144 });
145
146 *manager.writer_thread.lock().unwrap() = Some(std::thread::spawn(move || {
147 LoggerManager::writer_fn(receiver, log_path, app_name)
148 }));
149
150 manager.cleanup_old_logs().await;
151
152 let weak: Weak<LoggerManager> = Arc::downgrade(&manager);
153 tokio::spawn(async move {
154 let mut interval = time::interval(time::Duration::from_secs(86_400));
155 interval.tick().await;
156 loop {
157 interval.tick().await;
158 match weak.upgrade() {
159 Some(m) => m.cleanup_old_logs().await,
160 None => break,
161 }
162 }
163 });
164
165 let panic_sender = manager.sender.clone();
166 let panic_app = manager.app_name.clone();
167 std::panic::set_hook(Box::new(move |info| {
168 let loc = info
169 .location()
170 .map(|l| format!(" at {}:{}", l.file(), l.line()))
171 .unwrap_or_default();
172 let payload = info
173 .payload()
174 .downcast_ref::<&str>()
175 .map(|s| (*s).to_string())
176 .or_else(|| info.payload().downcast_ref::<String>().cloned())
177 .unwrap_or_else(|| "unknown panic".to_string());
178 let _ = panic_sender.try_send(LogMessage::Entry(LogEntry {
179 timestamp: Local::now().to_rfc3339(),
180 level: "PANIC".to_string(),
181 app: panic_app.clone(),
182 message: format!("panic{}: {}", loc, payload),
183 module: String::new(),
184 file: String::new(),
185 line: 0,
186 thread: std::thread::current()
187 .name()
188 .unwrap_or("unnamed")
189 .to_string(),
190 }));
191 std::thread::sleep(std::time::Duration::from_millis(200));
192 }));
193
194 manager._log_internal(
195 LogLevel::Info,
196 format!(
197 "LoggerManager started | app={} path={:?} retain={}d level={}",
198 manager.app_name, manager.log_path, manager.retention_days, manager.level
199 ),
200 module_path!(),
201 file!(),
202 line!(),
203 );
204
205 manager
206 }
207}
208
209impl LoggerManager {
210 pub fn builder(app_name: impl Into<String>) -> LoggerManagerBuilder {
226 LoggerManagerBuilder {
227 app_name: app_name.into(),
228 level: LogLevel::Info,
229 log_path: PathBuf::from("logs"),
230 retention_days: 7,
231 }
232 }
233
234 pub fn set_as_global(self: &Arc<Self>) {
244 let _ = GLOBAL.set(Arc::clone(self));
245 }
246
247 pub fn global() -> Option<Arc<LoggerManager>> {
250 GLOBAL.get().cloned()
251 }
252
253 #[doc(hidden)]
257 pub fn _log_internal(
258 &self,
259 level: LogLevel,
260 message: String,
261 module: &str,
262 file: &str,
263 line: u32,
264 ) {
265 if level < self.level {
266 return;
267 }
268 let _ = self.sender.try_send(LogMessage::Entry(LogEntry {
269 timestamp: Local::now().to_rfc3339(),
270 level: level.as_str().to_string(),
271 app: self.app_name.clone(),
272 message,
273 module: module.to_string(),
274 file: file.to_string(),
275 line,
276 thread: std::thread::current()
277 .name()
278 .unwrap_or("unnamed")
279 .to_string(),
280 }));
281 }
282
283 #[track_caller]
286 pub fn log(&self, level: LogLevel, message: impl Into<String>) {
287 let loc = std::panic::Location::caller();
288 self._log_internal(level, message.into(), "", loc.file(), loc.line());
289 }
290
291 #[track_caller]
292 pub fn debug(&self, msg: impl Into<String>) {
293 self.log(LogLevel::Debug, msg);
294 }
295 #[track_caller]
296 pub fn info(&self, msg: impl Into<String>) {
297 self.log(LogLevel::Info, msg);
298 }
299 #[track_caller]
300 pub fn warn(&self, msg: impl Into<String>) {
301 self.log(LogLevel::Warn, msg);
302 }
303 #[track_caller]
304 pub fn error(&self, msg: impl Into<String>) {
305 self.log(LogLevel::Error, msg);
306 }
307
308 fn writer_fn(receiver: std_mpsc::Receiver<LogMessage>, log_path: PathBuf, app_name: String) {
311 if let Err(e) = std::fs::create_dir_all(&log_path) {
312 eprintln!(
313 "[LoggerManager] Cannot create log dir {:?}: {}",
314 log_path, e
315 );
316 }
317 while let Ok(msg) = receiver.recv() {
318 let LogMessage::Entry(entry) = msg else { break };
319
320 let (pad, color) = match entry.level.as_str() {
322 "DEBUG" => ("DEBUG", "\x1b[90m"),
323 "INFO" => ("INFO ", "\x1b[32m"),
324 "WARN" => ("WARN ", "\x1b[33m"),
325 "ERROR" => ("ERROR", "\x1b[31m"),
326 "PANIC" => ("PANIC", "\x1b[1;31m"),
327 other => (other, ""),
328 };
329 let loc = if !entry.file.is_empty() && entry.line > 0 {
330 format!(" \x1b[2m{}:{}\x1b[0m", entry.file, entry.line)
331 } else if !entry.module.is_empty() {
332 format!(" \x1b[2m{}\x1b[0m", entry.module)
333 } else {
334 String::new()
335 };
336 println!(
337 "\x1b[2m{}\x1b[0m {}{}\x1b[0m \x1b[1m{}\x1b[0m{} {}",
338 Local::now().format("%H:%M:%S"),
339 color,
340 pad,
341 entry.app,
342 loc,
343 entry.message,
344 );
345
346 let file_path = log_path.join(format!(
348 "{}_{}.jsonl",
349 Local::now().format("%Y-%m-%d"),
350 app_name
351 ));
352 if let Ok(json) = serde_json::to_string(&entry) {
353 match std::fs::OpenOptions::new()
354 .create(true)
355 .append(true)
356 .open(&file_path)
357 {
358 Ok(mut f) => {
359 if let Err(e) = writeln!(f, "{}", json) {
360 eprintln!("[LoggerManager] Write error: {}", e);
361 }
362 }
363 Err(e) => eprintln!("[LoggerManager] Cannot open {:?}: {}", file_path, e),
364 }
365 }
366 }
367 }
368
369 async fn cleanup_old_logs(&self) {
370 let cutoff = (Local::now() - Duration::days(self.retention_days)).date_naive();
371 let mut entries = match fs::read_dir(&self.log_path).await {
372 Ok(e) => e,
373 Err(e) => {
374 eprintln!("[LoggerManager] Cannot read log dir: {}", e);
375 return;
376 }
377 };
378 loop {
379 match entries.next_entry().await {
380 Ok(Some(entry)) => {
381 let path = entry.path();
382 if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
383 continue;
384 }
385 let is_old = path
386 .file_name()
387 .and_then(|n| n.to_str())
388 .and_then(|n| n.get(..10))
389 .and_then(|d| NaiveDate::parse_from_str(d, "%Y-%m-%d").ok())
390 .map_or(false, |d| d < cutoff);
391 if is_old {
392 if let Err(e) = fs::remove_file(&path).await {
393 eprintln!("[LoggerManager] Failed to delete {:?}: {}", path, e);
394 }
395 }
396 }
397 Ok(None) => break,
398 Err(e) => {
399 eprintln!("[LoggerManager] Dir read error: {}", e);
400 break;
401 }
402 }
403 }
404 }
405}
406
407#[macro_export]
411macro_rules! lgr_debug {
412 ($logger:expr, $($arg:tt)*) => {
413 $logger._log_internal(
414 $crate::utils::logger_manager::LogLevel::Debug,
415 format!($($arg)*), module_path!(), file!(), line!(),
416 )
417 };
418}
419
420#[macro_export]
422macro_rules! lgr_info {
423 ($logger:expr, $($arg:tt)*) => {
424 $logger._log_internal(
425 $crate::utils::logger_manager::LogLevel::Info,
426 format!($($arg)*), module_path!(), file!(), line!(),
427 )
428 };
429}
430
431#[macro_export]
433macro_rules! lgr_warn {
434 ($logger:expr, $($arg:tt)*) => {
435 $logger._log_internal(
436 $crate::utils::logger_manager::LogLevel::Warn,
437 format!($($arg)*), module_path!(), file!(), line!(),
438 )
439 };
440}
441
442#[macro_export]
444macro_rules! lgr_error {
445 ($logger:expr, $($arg:tt)*) => {
446 $logger._log_internal(
447 $crate::utils::logger_manager::LogLevel::Error,
448 format!($($arg)*), module_path!(), file!(), line!(),
449 )
450 };
451}
452
453#[macro_export]
464macro_rules! println {
465 () => {
466 match $crate::utils::logger_manager::LoggerManager::global() {
467 Some(lgr) => lgr.debug(""),
468 None => ::std::println!(),
469 }
470 };
471 ($($arg:tt)*) => {
472 match $crate::utils::logger_manager::LoggerManager::global() {
473 Some(lgr) => lgr.debug(::std::format!($($arg)*)),
474 None => ::std::println!($($arg)*),
475 }
476 };
477}
478
479#[macro_export]
482macro_rules! eprintln {
483 () => {
484 match $crate::utils::logger_manager::LoggerManager::global() {
485 Some(lgr) => lgr.debug(""),
486 None => ::std::eprintln!(),
487 }
488 };
489 ($($arg:tt)*) => {
490 match $crate::utils::logger_manager::LoggerManager::global() {
491 Some(lgr) => lgr.debug(::std::format!($($arg)*)),
492 None => ::std::eprintln!($($arg)*),
493 }
494 };
495}
496
497#[macro_export]
500macro_rules! print {
501 () => {
502 match $crate::utils::logger_manager::LoggerManager::global() {
503 Some(lgr) => lgr.debug(""),
504 None => ::std::print!(),
505 }
506 };
507 ($($arg:tt)*) => {
508 match $crate::utils::logger_manager::LoggerManager::global() {
509 Some(lgr) => lgr.debug(::std::format!($($arg)*)),
510 None => ::std::print!($($arg)*),
511 }
512 };
513}
514
515#[macro_export]
518macro_rules! eprint {
519 () => {
520 match $crate::utils::logger_manager::LoggerManager::global() {
521 Some(lgr) => lgr.debug(""),
522 None => ::std::eprint!(),
523 }
524 };
525 ($($arg:tt)*) => {
526 match $crate::utils::logger_manager::LoggerManager::global() {
527 Some(lgr) => lgr.debug(::std::format!($($arg)*)),
528 None => ::std::eprint!($($arg)*),
529 }
530 };
531}
532
533impl Drop for LoggerManager {
534 fn drop(&mut self) {
535 let _ = self.sender.send(LogMessage::Shutdown);
536 if let Ok(mut guard) = self.writer_thread.lock() {
537 if let Some(handle) = guard.take() {
538 let _ = handle.join();
539 }
540 }
541 }
542}
543
544#[cfg(test)]
547mod tests {
548 use super::*;
549 use tempfile::tempdir;
550
551 #[tokio::test]
552 async fn test_creates_log_file() {
553 let dir = tempdir().unwrap();
554 let logger = LoggerManager::builder("testapp")
555 .level(LogLevel::Debug)
556 .log_path(dir.path())
557 .retention_days(7)
558 .build()
559 .await;
560
561 logger.info("hello from test");
562 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
563
564 let date = Local::now().format("%Y-%m-%d");
565 let log_file = dir.path().join(format!("{}_testapp.jsonl", date));
566 assert!(log_file.exists(), "log file should have been created");
567
568 let content = std::fs::read_to_string(&log_file).unwrap();
569 assert!(content.contains("\"level\":\"INFO\""));
570 assert!(content.contains("hello from test"));
571 }
572
573 #[tokio::test]
574 async fn test_level_filtering() {
575 let dir = tempdir().unwrap();
576 let logger = LoggerManager::builder("filterapp")
577 .level(LogLevel::Warn)
578 .log_path(dir.path())
579 .retention_days(7)
580 .build()
581 .await;
582
583 logger.debug("ignored debug");
584 logger.info("ignored info");
585 logger.warn("visible warn");
586 logger.error("visible error");
587 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
588
589 let date = Local::now().format("%Y-%m-%d");
590 let log_file = dir.path().join(format!("{}_filterapp.jsonl", date));
591 let content = std::fs::read_to_string(&log_file).unwrap();
592 assert!(!content.contains("ignored"));
593 assert!(content.contains("visible warn"));
594 assert!(content.contains("visible error"));
595 }
596
597 #[tokio::test]
598 async fn test_creates_directory_if_missing() {
599 let dir = tempdir().unwrap();
600 let nested = dir.path().join("deep/nested/logs");
601 let logger = LoggerManager::builder("nestapp")
602 .log_path(nested.clone())
603 .build()
604 .await;
605 logger.info("test");
606 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
607 assert!(nested.exists());
608 }
609
610 #[tokio::test]
611 async fn test_json_format() {
612 let dir = tempdir().unwrap();
613 let logger = LoggerManager::builder("jsonapp")
614 .level(LogLevel::Debug)
615 .log_path(dir.path())
616 .build()
617 .await;
618 logger.error("boom");
619 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
620
621 let date = Local::now().format("%Y-%m-%d");
622 let log_file = dir.path().join(format!("{}_jsonapp.jsonl", date));
623 let content = std::fs::read_to_string(&log_file).unwrap();
624 let line = content.lines().find(|l| l.contains("boom")).unwrap();
625 let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
626 assert_eq!(parsed["level"], "ERROR");
627 assert_eq!(parsed["app"], "jsonapp");
628 assert_eq!(parsed["message"], "boom");
629 assert!(parsed["timestamp"].is_string());
630 }
631
632 #[tokio::test]
633 async fn test_old_logs_are_deleted() {
634 let dir = tempdir().unwrap();
635 let old_file = dir.path().join("2000-01-01_oldapp.jsonl");
637 std::fs::write(&old_file, "old\n").unwrap();
638
639 let logger = LoggerManager::builder("oldapp")
640 .log_path(dir.path())
641 .retention_days(7)
642 .build()
643 .await;
644 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
646 assert!(!old_file.exists(), "old log file should have been deleted");
647 drop(logger);
648 }
649}