1use chrono::prelude::{DateTime, Utc};
7use log::{Level, Log, Metadata, Record};
8use std::collections::BTreeMap;
9use std::sync::{LazyLock, RwLock};
10use thread_id;
11
12static LOGGER: Logger = Logger;
13
14pub struct Logger;
15static PATHS: LazyLock<RwLock<BTreeMap<String, Level>>> =
16 LazyLock::new(|| RwLock::new(BTreeMap::new()));
17impl Log for Logger {
18 fn enabled(&self, metadata: &Metadata) -> bool {
19 let paths = PATHS.read().unwrap();
20 for (path, level) in paths.iter().rev() {
22 if path.is_empty() {
23 return metadata.level() <= *level;
24 }
25 if metadata.target().replace("::", "/").starts_with(path) {
26 return metadata.level() <= *level;
27 }
28 }
29 false
30 }
31
32 fn log(&self, record: &Record) {
33 if self.enabled(record.metadata()) {
34 let now: DateTime<Utc> = Utc::now();
35
36 let file = if let Some(file) = record.file() {
37 match file.split_once("src/") {
39 Some((_, file)) => file,
40 None => file,
41 }
42 } else {
43 "(unknown)"
44 };
45
46 let package_name = if let Some(package_name) = record.target().split_once(':') {
47 package_name.0
48 } else {
49 record.target()
50 };
51
52 println!(
53 "{} ({:>5}) [{}] -- {:}/{:}:{:} {:?}",
54 now.format("%Y-%m-%d %H:%M:%S.%3f"),
55 thread_id::get() % 100000,
56 record.level(),
57 package_name,
58 file,
59 record.line().unwrap(),
60 record.args(),
61 );
62 }
63 }
64
65 fn flush(&self) {}
66}
67
68impl Logger {
69 pub fn init(levels: &str) {
75 let mut max_level = Level::Trace;
76 {
77 let mut paths = PATHS.write().unwrap();
78 paths.clear();
79 paths.insert("".to_string(), Level::Info); }
81 for level in levels.split(',') {
82 let mut parts = level.splitn(2, '=');
83 let mut path = parts.next().unwrap().trim();
84 let level = if let Some(lvl) = parts.next() {
85 lvl.trim()
86 } else {
87 let lvl = path;
89 path = ""; lvl
91 };
92
93 let level = match level.to_uppercase().as_str() {
94 "TRACE" => Level::Trace,
95 "DEBUG" => Level::Debug,
96 "INFO" => Level::Info,
97 "WARN" => Level::Warn,
98 "ERROR" => Level::Error,
99 _ => {
100 eprintln!("Invalid log level: {}, defaulting to INFO", level);
101 Level::Info
102 }
103 };
104
105 max_level = std::cmp::max(max_level, level);
106 PATHS.write().unwrap().insert(path.to_string(), level);
107 }
108
109 log::set_logger(&LOGGER).ok();
110 log::set_max_level(max_level.to_level_filter());
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use serial_test::serial;
118
119 #[test]
120 #[serial]
121 fn it_works() {
122 Logger::init("INFO");
123 log::info!("Hello, world!");
124 }
125
126 #[test]
127 #[serial]
128 fn test_log_levels() {
129 Logger::init("DEBUG,path=TRACE,crate/module=ERROR");
130 assert_eq!(
131 LOGGER.enabled(
132 &Metadata::builder()
133 .level(Level::Info)
134 .target("crate")
135 .build()
136 ),
137 true
138 );
139 assert_eq!(
140 LOGGER.enabled(
141 &Metadata::builder()
142 .level(Level::Debug)
143 .target("crate")
144 .build()
145 ),
146 true
147 );
148 assert_eq!(
149 LOGGER.enabled(
150 &Metadata::builder()
151 .level(Level::Trace)
152 .target("crate")
153 .build()
154 ),
155 false
156 );
157 assert_eq!(
158 LOGGER.enabled(
159 &Metadata::builder()
160 .level(Level::Error)
161 .target("crate/module")
162 .build()
163 ),
164 true
165 );
166 assert_eq!(
167 LOGGER.enabled(
168 &Metadata::builder()
169 .level(Level::Warn)
170 .target("crate/module")
171 .build()
172 ),
173 false
174 );
175 assert_eq!(
176 LOGGER.enabled(
177 &Metadata::builder()
178 .level(Level::Info)
179 .target("other")
180 .build()
181 ),
182 true
183 );
184 assert_eq!(
185 LOGGER.enabled(
186 &Metadata::builder()
187 .level(Level::Debug)
188 .target("other")
189 .build()
190 ),
191 true
192 );
193 assert_eq!(
194 LOGGER.enabled(
195 &Metadata::builder()
196 .level(Level::Trace)
197 .target("other")
198 .build()
199 ),
200 false
201 );
202 }
203
204 #[test]
205 #[serial]
206 fn test_log_wrong_level() {
207 Logger::init("WRONG");
208 assert_eq!(
209 LOGGER.enabled(
210 &Metadata::builder()
211 .level(Level::Info)
212 .target("crate")
213 .build()
214 ),
215 true,
216 "Default level is INFO"
217 );
218 assert_eq!(
219 LOGGER.enabled(
220 &Metadata::builder()
221 .level(Level::Debug)
222 .target("crate")
223 .build()
224 ),
225 false
226 );
227 }
228}