1use colored::{ColoredString, Colorize};
2use std::env;
3use std::fs::{File, OpenOptions};
4use std::io::{BufWriter, Write};
5use std::sync::{LazyLock, Mutex};
6
7static LOG_LEVEL: LazyLock<Level> = LazyLock::new(Level::from_env);
8static LOG_FILE: LazyLock<Mutex<Option<BufWriter<File>>>> = LazyLock::new(|| Mutex::new(None));
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum Level {
12 Trace = 0,
13 Debug = 1,
14 Info = 2,
15 Warn = 3,
16 Error = 4,
17}
18
19impl Level {
20 fn from_env() -> Self {
21 dotenv::dotenv().ok();
22
23 if let Ok(path) = env::var("LOG_FILE") {
24 init(&path).ok();
25 }
26
27 env::var("LOG_LEVEL")
28 .ok()
29 .and_then(|s| match s.to_lowercase().as_str() {
30 "trace" => Some(Level::Trace),
31 "debug" => Some(Level::Debug),
32 "info" => Some(Level::Info),
33 "warn" => Some(Level::Warn),
34 "error" => Some(Level::Error),
35 _ => None,
36 })
37 .unwrap_or(Level::Info)
38 }
39}
40
41#[inline]
42fn should_log(level: Level) -> bool {
43 level >= *LOG_LEVEL
44}
45
46pub fn init(path: &str) -> Result<(), std::io::Error> {
47 let stem = path.strip_suffix(".log").unwrap_or(path);
48 let timestamped = format!(
49 "{}_{}.log",
50 stem,
51 chrono::Local::now().format("%Y%m%d_%H%M%S")
52 );
53 let file = OpenOptions::new()
54 .create(true)
55 .append(true)
56 .open(×tamped)?;
57 *LOG_FILE.lock().unwrap() = Some(BufWriter::new(file));
58 Ok(())
59}
60
61#[inline]
62pub fn log(level: Level, msg: ColoredString) {
63 if !should_log(level) {
64 return;
65 }
66
67 let now = chrono::Local::now();
68
69 let level_str = match level {
70 Level::Trace => "TRACE".dimmed(),
71 Level::Debug => "DEBUG".blue().bold(),
72 Level::Info => "INFO".green().bold(),
73 Level::Warn => "WARN".yellow().bold(),
74 Level::Error => "ERROR".red().bold(),
75 };
76
77 println!("[{}][{}] {}", now.format("%H:%M:%S"), level_str, msg);
78
79 if let Ok(mut guard) = LOG_FILE.lock()
80 && let Some(ref mut file) = *guard
81 {
82 let level_plain = match level {
83 Level::Trace => "TRACE",
84 Level::Debug => "DEBUG",
85 Level::Info => "INFO",
86 Level::Warn => "WARN",
87 Level::Error => "ERROR",
88 };
89 let _ = writeln!(
90 file,
91 "[{}][{}] {}",
92 now.format("%Y-%m-%d %H:%M:%S"),
93 level_plain,
94 &*msg
95 );
96 let _ = file.flush();
97 }
98}
99
100#[macro_export]
101macro_rules! trace {
102 ($($arg:tt)*) => {
103 {
104 use colored::Colorize;
105 $crate::log::log($crate::log::Level::Trace, format!($($arg)*).dimmed())
106 }
107 };
108}
109
110#[macro_export]
111macro_rules! debug {
112 ($($arg:tt)*) => {
113 {
114 use colored::Colorize;
115 $crate::log::log($crate::log::Level::Debug, format!($($arg)*).blue())
116 }
117 };
118}
119
120#[macro_export]
121macro_rules! info {
122 ($($arg:tt)*) => {
123 {
124 use colored::Colorize;
125 $crate::log::log($crate::log::Level::Info, format!($($arg)*).green())
126 }
127 };
128}
129
130#[macro_export]
131macro_rules! warn {
132 ($($arg:tt)*) => {
133 {
134 use colored::Colorize;
135 $crate::log::log($crate::log::Level::Warn, format!($($arg)*).yellow())
136 }
137 };
138}
139
140#[macro_export]
141macro_rules! error {
142 ($($arg:tt)*) => {
143 {
144 use colored::Colorize;
145 $crate::log::log($crate::log::Level::Error, format!($($arg)*).red())
146 }
147 };
148}