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