1use std::collections::HashMap;
2use std::fmt;
3use std::io::Write;
4use std::sync::OnceLock;
5use std::time::SystemTime;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[repr(u8)]
17pub enum LogLevel {
18 Trace = 0,
19 Debug = 1,
20 Info = 2,
21 Warn = 3,
22 Error = 4,
23}
24
25impl LogLevel {
26 #[inline]
28 fn priority(self) -> u8 {
29 self as u8
30 }
31}
32
33impl PartialOrd for LogLevel {
34 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
35 Some(self.cmp(other))
36 }
37}
38
39impl Ord for LogLevel {
40 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
41 self.priority().cmp(&other.priority())
42 }
43}
44
45impl fmt::Display for LogLevel {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 let tag = match self {
48 LogLevel::Trace => "TRACE",
49 LogLevel::Debug => "DEBUG",
50 LogLevel::Info => "INFO ",
51 LogLevel::Warn => "WARN ",
52 LogLevel::Error => "ERROR",
53 };
54 f.write_str(tag)
55 }
56}
57
58#[derive(Debug, Clone)]
64pub struct LogEntry {
65 pub level: LogLevel,
66 pub message: String,
67 pub target: String,
68 pub timestamp: String,
69 pub fields: HashMap<String, String>,
70}
71
72impl LogEntry {
73 pub fn new(
75 level: LogLevel,
76 target: impl Into<String>,
77 message: impl Into<String>,
78 fields: HashMap<String, String>,
79 ) -> Self {
80 Self {
81 level,
82 message: message.into(),
83 target: target.into(),
84 timestamp: iso8601_now(),
85 fields,
86 }
87 }
88
89 pub fn format(&self) -> String {
91 let mut buf = format!(
92 "[{}] {} [{}] {}",
93 self.timestamp, self.level, self.target, self.message,
94 );
95
96 if !self.fields.is_empty() {
99 let mut keys: Vec<&String> = self.fields.keys().collect();
100 keys.sort();
101 for key in keys {
102 buf.push(' ');
103 buf.push_str(key);
104 buf.push('=');
105 buf.push_str(&self.fields[key]);
106 }
107 }
108
109 buf
110 }
111}
112
113impl fmt::Display for LogEntry {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 f.write_str(&self.format())
116 }
117}
118
119pub struct Logger {
130 min_level: LogLevel,
131}
132
133impl Logger {
134 pub fn new(min_level: LogLevel) -> Self {
135 Self { min_level }
136 }
137
138 #[inline]
140 pub fn enabled(&self, level: LogLevel) -> bool {
141 level >= self.min_level
142 }
143
144 pub fn log(
146 &self,
147 level: LogLevel,
148 target: &str,
149 message: &str,
150 fields: HashMap<String, String>,
151 ) {
152 if !self.enabled(level) {
153 return;
154 }
155
156 let entry = LogEntry::new(level, target, message, fields);
157 let line = format!("{}\n", entry.format());
158
159 let _ = std::io::stderr().write_all(line.as_bytes());
161 }
162
163 pub fn info(&self, target: &str, message: &str, fields: HashMap<String, String>) {
166 self.log(LogLevel::Info, target, message, fields);
167 }
168
169 pub fn warn(&self, target: &str, message: &str, fields: HashMap<String, String>) {
170 self.log(LogLevel::Warn, target, message, fields);
171 }
172
173 pub fn error(&self, target: &str, message: &str, fields: HashMap<String, String>) {
174 self.log(LogLevel::Error, target, message, fields);
175 }
176
177 pub fn debug(&self, target: &str, message: &str, fields: HashMap<String, String>) {
178 self.log(LogLevel::Debug, target, message, fields);
179 }
180
181 pub fn trace(&self, target: &str, message: &str, fields: HashMap<String, String>) {
182 self.log(LogLevel::Trace, target, message, fields);
183 }
184}
185
186static LOGGER: OnceLock<Logger> = OnceLock::new();
191
192pub fn init_logger(level: LogLevel) {
195 let _ = LOGGER.set(Logger::new(level));
196}
197
198pub fn logger() -> &'static Logger {
205 LOGGER
206 .get()
207 .expect("pylon: logger not initialised — call init_logger() first")
208}
209
210#[macro_export]
221macro_rules! log_info {
222 ($target:expr, $msg:expr $(, $key:expr => $val:expr)*) => {{
223 let fields = $crate::log::__build_fields(&[$(($key, $val)),*]);
224 $crate::log::logger().info($target, $msg, fields);
225 }};
226}
227
228#[macro_export]
230macro_rules! log_warn {
231 ($target:expr, $msg:expr $(, $key:expr => $val:expr)*) => {{
232 let fields = $crate::log::__build_fields(&[$(($key, $val)),*]);
233 $crate::log::logger().warn($target, $msg, fields);
234 }};
235}
236
237#[macro_export]
239macro_rules! log_error {
240 ($target:expr, $msg:expr $(, $key:expr => $val:expr)*) => {{
241 let fields = $crate::log::__build_fields(&[$(($key, $val)),*]);
242 $crate::log::logger().error($target, $msg, fields);
243 }};
244}
245
246#[macro_export]
248macro_rules! log_debug {
249 ($target:expr, $msg:expr $(, $key:expr => $val:expr)*) => {{
250 let fields = $crate::log::__build_fields(&[$(($key, $val)),*]);
251 $crate::log::logger().debug($target, $msg, fields);
252 }};
253}
254
255#[doc(hidden)]
262pub fn __build_fields(pairs: &[(&str, &str)]) -> HashMap<String, String> {
263 pairs
264 .iter()
265 .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
266 .collect()
267}
268
269fn iso8601_now() -> String {
273 let duration = SystemTime::now()
274 .duration_since(SystemTime::UNIX_EPOCH)
275 .unwrap_or_default();
276
277 let secs = duration.as_secs();
278
279 const SECS_PER_MINUTE: u64 = 60;
281 const SECS_PER_HOUR: u64 = 3_600;
282 const SECS_PER_DAY: u64 = 86_400;
283
284 let days = secs / SECS_PER_DAY;
285 let day_secs = secs % SECS_PER_DAY;
286 let hour = day_secs / SECS_PER_HOUR;
287 let minute = (day_secs % SECS_PER_HOUR) / SECS_PER_MINUTE;
288 let second = day_secs % SECS_PER_MINUTE;
289
290 let (year, month, day) = civil_from_days(days as i64);
292
293 format!(
294 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
295 year, month, day, hour, minute, second,
296 )
297}
298
299fn civil_from_days(days: i64) -> (i64, u32, u32) {
302 let z = days + 719_468;
303 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
304 let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400;
307 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = if m <= 2 { y + 1 } else { y };
312 (y, m as u32, d as u32)
313}
314
315#[cfg(test)]
320mod tests {
321 use super::*;
322 use std::cmp::Ordering;
323
324 #[test]
327 fn level_ordering_error_is_most_severe() {
328 assert!(LogLevel::Error > LogLevel::Warn);
329 assert!(LogLevel::Error > LogLevel::Info);
330 assert!(LogLevel::Error > LogLevel::Debug);
331 assert!(LogLevel::Error > LogLevel::Trace);
332 }
333
334 #[test]
335 fn level_ordering_warn_gt_info() {
336 assert!(LogLevel::Warn > LogLevel::Info);
337 assert!(LogLevel::Warn > LogLevel::Debug);
338 assert!(LogLevel::Warn > LogLevel::Trace);
339 }
340
341 #[test]
342 fn level_ordering_info_gt_debug() {
343 assert!(LogLevel::Info > LogLevel::Debug);
344 assert!(LogLevel::Info > LogLevel::Trace);
345 }
346
347 #[test]
348 fn level_ordering_debug_gt_trace() {
349 assert!(LogLevel::Debug > LogLevel::Trace);
350 }
351
352 #[test]
353 fn level_ordering_equal() {
354 assert_eq!(LogLevel::Info.cmp(&LogLevel::Info), Ordering::Equal);
355 }
356
357 #[test]
358 fn level_ordering_full_sequence() {
359 let mut levels = vec![
360 LogLevel::Warn,
361 LogLevel::Trace,
362 LogLevel::Error,
363 LogLevel::Debug,
364 LogLevel::Info,
365 ];
366 levels.sort();
367 assert_eq!(
368 levels,
369 vec![
370 LogLevel::Trace,
371 LogLevel::Debug,
372 LogLevel::Info,
373 LogLevel::Warn,
374 LogLevel::Error,
375 ]
376 );
377 }
378
379 #[test]
382 fn entry_format_without_fields() {
383 let entry = LogEntry {
384 level: LogLevel::Info,
385 message: "server started".into(),
386 target: "server".into(),
387 timestamp: "2024-01-15T12:00:00Z".into(),
388 fields: HashMap::new(),
389 };
390
391 assert_eq!(
392 entry.format(),
393 "[2024-01-15T12:00:00Z] INFO [server] server started"
394 );
395 }
396
397 #[test]
398 fn entry_format_with_fields() {
399 let mut fields = HashMap::new();
400 fields.insert("port".into(), "8080".into());
401 fields.insert("host".into(), "0.0.0.0".into());
402
403 let entry = LogEntry {
404 level: LogLevel::Warn,
405 message: "binding".into(),
406 target: "net".into(),
407 timestamp: "2024-01-15T12:00:00Z".into(),
408 fields,
409 };
410
411 assert_eq!(
413 entry.format(),
414 "[2024-01-15T12:00:00Z] WARN [net] binding host=0.0.0.0 port=8080"
415 );
416 }
417
418 #[test]
419 fn entry_format_error_level() {
420 let entry = LogEntry {
421 level: LogLevel::Error,
422 message: "disk full".into(),
423 target: "storage".into(),
424 timestamp: "2024-01-15T12:00:00Z".into(),
425 fields: HashMap::new(),
426 };
427
428 assert_eq!(
429 entry.format(),
430 "[2024-01-15T12:00:00Z] ERROR [storage] disk full"
431 );
432 }
433
434 #[test]
435 fn entry_display_matches_format() {
436 let entry = LogEntry {
437 level: LogLevel::Debug,
438 message: "cache miss".into(),
439 target: "cache".into(),
440 timestamp: "2024-01-15T12:00:00Z".into(),
441 fields: HashMap::new(),
442 };
443
444 assert_eq!(entry.to_string(), entry.format());
445 }
446
447 #[test]
450 fn logger_enabled_at_min_level() {
451 let logger = Logger::new(LogLevel::Info);
452 assert!(logger.enabled(LogLevel::Info));
453 assert!(logger.enabled(LogLevel::Warn));
454 assert!(logger.enabled(LogLevel::Error));
455 }
456
457 #[test]
458 fn logger_filters_below_min_level() {
459 let logger = Logger::new(LogLevel::Warn);
460 assert!(!logger.enabled(LogLevel::Trace));
461 assert!(!logger.enabled(LogLevel::Debug));
462 assert!(!logger.enabled(LogLevel::Info));
463 assert!(logger.enabled(LogLevel::Warn));
464 assert!(logger.enabled(LogLevel::Error));
465 }
466
467 #[test]
468 fn logger_trace_enables_everything() {
469 let logger = Logger::new(LogLevel::Trace);
470 assert!(logger.enabled(LogLevel::Trace));
471 assert!(logger.enabled(LogLevel::Debug));
472 assert!(logger.enabled(LogLevel::Info));
473 assert!(logger.enabled(LogLevel::Warn));
474 assert!(logger.enabled(LogLevel::Error));
475 }
476
477 #[test]
478 fn logger_error_only() {
479 let logger = Logger::new(LogLevel::Error);
480 assert!(!logger.enabled(LogLevel::Trace));
481 assert!(!logger.enabled(LogLevel::Debug));
482 assert!(!logger.enabled(LogLevel::Info));
483 assert!(!logger.enabled(LogLevel::Warn));
484 assert!(logger.enabled(LogLevel::Error));
485 }
486
487 #[test]
490 fn convenience_methods_do_not_panic() {
491 let logger = Logger::new(LogLevel::Error);
494 logger.info("t", "m", HashMap::new());
495 logger.warn("t", "m", HashMap::new());
496 logger.debug("t", "m", HashMap::new());
497 logger.trace("t", "m", HashMap::new());
498 logger.error("t", "m", HashMap::new());
499 }
500
501 #[test]
504 fn iso8601_now_looks_valid() {
505 let ts = iso8601_now();
506 assert_eq!(ts.len(), 20, "timestamp length: {ts}");
508 assert!(ts.ends_with('Z'));
509 assert_eq!(&ts[4..5], "-");
510 assert_eq!(&ts[7..8], "-");
511 assert_eq!(&ts[10..11], "T");
512 assert_eq!(&ts[13..14], ":");
513 assert_eq!(&ts[16..17], ":");
514 }
515
516 #[test]
517 fn civil_from_days_epoch() {
518 assert_eq!(civil_from_days(0), (1970, 1, 1));
520 }
521
522 #[test]
523 fn civil_from_days_known_date() {
524 assert_eq!(civil_from_days(19_737), (2024, 1, 15));
526 }
527
528 #[test]
531 fn build_fields_empty() {
532 let fields = __build_fields(&[]);
533 assert!(fields.is_empty());
534 }
535
536 #[test]
537 fn build_fields_with_pairs() {
538 let fields = __build_fields(&[("a", "1"), ("b", "2")]);
539 assert_eq!(fields.len(), 2);
540 assert_eq!(fields.get("a").unwrap(), "1");
541 assert_eq!(fields.get("b").unwrap(), "2");
542 }
543}