1use std::collections::HashSet;
37use std::fmt;
38use std::time::SystemTime;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub enum LogLevel {
43 Error,
45 Warn,
47 Info,
49 Debug,
51 Trace,
53}
54
55impl LogLevel {
56 #[must_use]
58 #[inline]
59 pub const fn as_str(&self) -> &'static str {
60 match self {
61 Self::Error => "ERROR",
62 Self::Warn => "WARN",
63 Self::Info => "INFO",
64 Self::Debug => "DEBUG",
65 Self::Trace => "TRACE",
66 }
67 }
68
69 #[must_use]
71 #[inline]
72 pub const fn colored(&self) -> &'static str {
73 match self {
74 Self::Error => "\x1b[31mERROR\x1b[0m", Self::Warn => "\x1b[33mWARN\x1b[0m", Self::Info => "\x1b[32mINFO\x1b[0m", Self::Debug => "\x1b[36mDEBUG\x1b[0m", Self::Trace => "\x1b[90mTRACE\x1b[0m", }
80 }
81
82 #[must_use]
84 #[inline]
85 pub const fn should_log(&self, configured_level: &Self) -> bool {
86 (*self as u8) <= (*configured_level as u8)
87 }
88}
89
90impl fmt::Display for LogLevel {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(f, "{}", self.as_str())
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct LogConfig {
99 pub level: LogLevel,
101 pub include_timestamps: bool,
103 pub include_module_path: bool,
105 pub include_line_numbers: bool,
107 pub filter_modules: Vec<String>,
109}
110
111impl Default for LogConfig {
112 fn default() -> Self {
113 Self {
114 level: LogLevel::Info,
115 include_timestamps: true,
116 include_module_path: true,
117 include_line_numbers: false,
118 filter_modules: Vec::new(),
119 }
120 }
121}
122
123impl LogConfig {
124 #[must_use]
126 #[inline]
127 pub const fn new(level: LogLevel) -> Self {
128 Self {
129 level,
130 include_timestamps: true,
131 include_module_path: true,
132 include_line_numbers: false,
133 filter_modules: Vec::new(),
134 }
135 }
136
137 #[must_use]
139 #[inline]
140 pub const fn minimal(level: LogLevel) -> Self {
141 Self {
142 level,
143 include_timestamps: false,
144 include_module_path: false,
145 include_line_numbers: false,
146 filter_modules: Vec::new(),
147 }
148 }
149
150 #[must_use]
152 #[inline]
153 pub const fn verbose(level: LogLevel) -> Self {
154 Self {
155 level,
156 include_timestamps: true,
157 include_module_path: true,
158 include_line_numbers: true,
159 filter_modules: Vec::new(),
160 }
161 }
162
163 #[must_use]
165 pub fn with_module_filter(mut self, module: String) -> Self {
166 self.filter_modules.push(module);
167 self
168 }
169}
170
171pub struct Logger {
173 config: LogConfig,
174 filter_set: HashSet<String>,
175 use_color: bool,
176}
177
178impl Logger {
179 #[must_use]
181 pub fn new(config: LogConfig) -> Self {
182 let filter_set: HashSet<String> = config.filter_modules.iter().cloned().collect();
183
184 Self {
185 config,
186 filter_set,
187 use_color: is_terminal(),
188 }
189 }
190
191 #[must_use]
193 #[inline]
194 pub fn default_config() -> Self {
195 Self::new(LogConfig::default())
196 }
197
198 #[must_use]
200 #[inline]
201 fn should_log_module(&self, module: &str) -> bool {
202 if self.filter_set.is_empty() {
203 return true;
204 }
205
206 self.filter_set
207 .iter()
208 .any(|filter| module.starts_with(filter) || filter.starts_with(module))
209 }
210
211 pub fn log(&self, level: LogLevel, module: &str, message: &str, line: Option<u32>) {
213 if !level.should_log(&self.config.level) {
214 return;
215 }
216
217 if !self.should_log_module(module) {
218 return;
219 }
220
221 let mut parts = Vec::new();
222
223 if self.config.include_timestamps {
225 let timestamp = format_timestamp();
226 parts.push(timestamp);
227 }
228
229 let level_str = if self.use_color {
231 level.colored().to_string()
232 } else {
233 level.as_str().to_string()
234 };
235 parts.push(format!("[{}]", level_str));
236
237 if self.config.include_module_path {
239 parts.push(format!("[{}]", module));
240 }
241
242 if self.config.include_line_numbers {
244 if let Some(line_num) = line {
245 parts.push(format!("[L{}]", line_num));
246 }
247 }
248
249 parts.push(message.to_string());
251
252 println!("{}", parts.join(" "));
253 }
254
255 #[inline]
257 pub fn error(&self, module: &str, message: &str) {
258 self.log(LogLevel::Error, module, message, None);
259 }
260
261 #[inline]
263 pub fn warn(&self, module: &str, message: &str) {
264 self.log(LogLevel::Warn, module, message, None);
265 }
266
267 #[inline]
269 pub fn info(&self, module: &str, message: &str) {
270 self.log(LogLevel::Info, module, message, None);
271 }
272
273 #[inline]
275 pub fn debug(&self, module: &str, message: &str) {
276 self.log(LogLevel::Debug, module, message, None);
277 }
278
279 #[inline]
281 pub fn trace(&self, module: &str, message: &str) {
282 self.log(LogLevel::Trace, module, message, None);
283 }
284
285 #[inline]
287 pub fn error_at(&self, module: &str, message: &str, line: u32) {
288 self.log(LogLevel::Error, module, message, Some(line));
289 }
290
291 #[inline]
293 pub fn warn_at(&self, module: &str, message: &str, line: u32) {
294 self.log(LogLevel::Warn, module, message, Some(line));
295 }
296
297 pub fn structured(
299 &self,
300 level: LogLevel,
301 module: &str,
302 message: &str,
303 fields: &[(&str, &str)],
304 ) {
305 if !level.should_log(&self.config.level) {
306 return;
307 }
308
309 if !self.should_log_module(module) {
310 return;
311 }
312
313 let fields_str: Vec<String> = fields.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
314
315 let full_message = if fields_str.is_empty() {
316 message.to_string()
317 } else {
318 format!("{} {}", message, fields_str.join(" "))
319 };
320
321 self.log(level, module, &full_message, None);
322 }
323
324 #[inline]
326 pub fn perf(&self, module: &str, operation: &str, duration_ms: u64) {
327 let message = format!("{} completed in {}ms", operation, duration_ms);
328 self.structured(
329 LogLevel::Debug,
330 module,
331 &message,
332 &[
333 ("operation", operation),
334 ("duration_ms", &duration_ms.to_string()),
335 ],
336 );
337 }
338
339 #[must_use]
341 #[inline]
342 pub const fn level(&self) -> LogLevel {
343 self.config.level
344 }
345
346 pub fn set_level(&mut self, level: LogLevel) {
348 self.config.level = level;
349 }
350
351 #[inline]
353 pub fn set_color(&mut self, use_color: bool) {
354 self.use_color = use_color;
355 }
356}
357
358#[must_use]
360fn format_timestamp() -> String {
361 let now = SystemTime::now()
362 .duration_since(SystemTime::UNIX_EPOCH)
363 .unwrap_or_default();
364
365 let secs = now.as_secs();
366 let millis = now.subsec_millis();
367
368 let hours = (secs / 3600) % 24;
370 let minutes = (secs / 60) % 60;
371 let seconds = secs % 60;
372
373 format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
374}
375
376#[must_use]
378#[inline]
379fn is_terminal() -> bool {
380 std::env::var("TERM").is_ok()
382}
383
384#[macro_export]
386macro_rules! log_error {
387 ($logger:expr, $($arg:tt)*) => {
388 $logger.error(module_path!(), &format!($($arg)*))
389 };
390}
391
392#[macro_export]
394macro_rules! log_warn {
395 ($logger:expr, $($arg:tt)*) => {
396 $logger.warn(module_path!(), &format!($($arg)*))
397 };
398}
399
400#[macro_export]
402macro_rules! log_info {
403 ($logger:expr, $($arg:tt)*) => {
404 $logger.info(module_path!(), &format!($($arg)*))
405 };
406}
407
408#[macro_export]
410macro_rules! log_debug {
411 ($logger:expr, $($arg:tt)*) => {
412 $logger.debug(module_path!(), &format!($($arg)*))
413 };
414}
415
416#[macro_export]
418macro_rules! log_trace {
419 ($logger:expr, $($arg:tt)*) => {
420 $logger.trace(module_path!(), &format!($($arg)*))
421 };
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
429 fn test_log_level_ordering() {
430 assert!(LogLevel::Error < LogLevel::Warn);
431 assert!(LogLevel::Warn < LogLevel::Info);
432 assert!(LogLevel::Info < LogLevel::Debug);
433 assert!(LogLevel::Debug < LogLevel::Trace);
434 }
435
436 #[test]
437 fn test_should_log() {
438 let configured_level = LogLevel::Info;
439
440 assert!(LogLevel::Error.should_log(&configured_level));
441 assert!(LogLevel::Warn.should_log(&configured_level));
442 assert!(LogLevel::Info.should_log(&configured_level));
443 assert!(!LogLevel::Debug.should_log(&configured_level));
444 assert!(!LogLevel::Trace.should_log(&configured_level));
445 }
446
447 #[test]
448 fn test_log_config_default() {
449 let config = LogConfig::default();
450 assert_eq!(config.level, LogLevel::Info);
451 assert!(config.include_timestamps);
452 assert!(config.include_module_path);
453 assert!(!config.include_line_numbers);
454 }
455
456 #[test]
457 fn test_log_config_minimal() {
458 let config = LogConfig::minimal(LogLevel::Debug);
459 assert_eq!(config.level, LogLevel::Debug);
460 assert!(!config.include_timestamps);
461 assert!(!config.include_module_path);
462 assert!(!config.include_line_numbers);
463 }
464
465 #[test]
466 fn test_log_config_verbose() {
467 let config = LogConfig::verbose(LogLevel::Trace);
468 assert_eq!(config.level, LogLevel::Trace);
469 assert!(config.include_timestamps);
470 assert!(config.include_module_path);
471 assert!(config.include_line_numbers);
472 }
473
474 #[test]
475 fn test_logger_creation() {
476 let config = LogConfig::default();
477 let logger = Logger::new(config);
478 assert_eq!(logger.level(), LogLevel::Info);
479 }
480
481 #[test]
482 fn test_module_filtering() {
483 let config = LogConfig::default().with_module_filter("chie_core::storage".to_string());
484 let logger = Logger::new(config);
485
486 assert!(logger.should_log_module("chie_core::storage"));
487 assert!(logger.should_log_module("chie_core::storage::chunk"));
488 assert!(!logger.should_log_module("chie_core::network"));
489 }
490
491 #[test]
492 fn test_logger_level_change() {
493 let mut logger = Logger::default_config();
494 assert_eq!(logger.level(), LogLevel::Info);
495
496 logger.set_level(LogLevel::Debug);
497 assert_eq!(logger.level(), LogLevel::Debug);
498 }
499
500 #[test]
501 fn test_log_level_display() {
502 assert_eq!(LogLevel::Error.to_string(), "ERROR");
503 assert_eq!(LogLevel::Warn.to_string(), "WARN");
504 assert_eq!(LogLevel::Info.to_string(), "INFO");
505 assert_eq!(LogLevel::Debug.to_string(), "DEBUG");
506 assert_eq!(LogLevel::Trace.to_string(), "TRACE");
507 }
508}