Skip to main content

rustlavel_core/
log.rs

1//! Leveled logging that doubles as instrumentation.
2//!
3//! Every log line is also dispatched on the event bus, so Telescope and a JSON
4//! log shipper see the same records without the caller doing anything twice.
5
6use crate::events::Event;
7use crate::json::Json;
8use std::io::Write;
9use std::sync::atomic::{AtomicU8, Ordering};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
12pub enum Level {
13    Debug = 0,
14    Info = 1,
15    Warn = 2,
16    Error = 3,
17}
18
19impl Level {
20    pub fn as_str(self) -> &'static str {
21        match self {
22            Level::Debug => "debug",
23            Level::Info => "info",
24            Level::Warn => "warn",
25            Level::Error => "error",
26        }
27    }
28
29    fn color(self) -> &'static str {
30        match self {
31            Level::Debug => "\x1b[38;5;244m",
32            Level::Info => "\x1b[38;5;39m",
33            Level::Warn => "\x1b[38;5;214m",
34            Level::Error => "\x1b[38;5;203m",
35        }
36    }
37
38    pub fn parse(value: &str) -> Option<Level> {
39        match value.to_ascii_lowercase().as_str() {
40            "debug" => Some(Level::Debug),
41            "info" => Some(Level::Info),
42            "warn" | "warning" => Some(Level::Warn),
43            "error" => Some(Level::Error),
44            _ => None,
45        }
46    }
47}
48
49static MIN_LEVEL: AtomicU8 = AtomicU8::new(Level::Info as u8);
50static USE_JSON: AtomicU8 = AtomicU8::new(0);
51
52/// Drop anything below this level.
53pub fn set_level(level: Level) {
54    MIN_LEVEL.store(level as u8, Ordering::Relaxed);
55}
56
57/// Emit one JSON object per line instead of the human format. This is what
58/// production deployments want so log aggregators can parse the output.
59pub fn set_json(enabled: bool) {
60    USE_JSON.store(u8::from(enabled), Ordering::Relaxed);
61}
62
63pub fn enabled(level: Level) -> bool {
64    level as u8 >= MIN_LEVEL.load(Ordering::Relaxed)
65}
66
67pub fn log(level: Level, message: impl AsRef<str>) {
68    let message = message.as_ref();
69
70    if enabled(level) {
71        let mut stderr = std::io::stderr().lock();
72        if USE_JSON.load(Ordering::Relaxed) == 1 {
73            let line = Json::object([
74                ("level", Json::from(level.as_str())),
75                ("message", Json::from(message)),
76                ("timestamp", Json::from(unix_seconds())),
77            ]);
78            let _ = writeln!(stderr, "{}", line);
79        } else {
80            let _ = writeln!(
81                stderr,
82                "{}{:>5}\x1b[0m  {}",
83                level.color(),
84                level.as_str().to_uppercase(),
85                message
86            );
87        }
88    }
89
90    Event::new("log")
91        .with("level", level.as_str())
92        .with("message", message)
93        .dispatch();
94}
95
96fn unix_seconds() -> f64 {
97    std::time::SystemTime::now()
98        .duration_since(std::time::UNIX_EPOCH)
99        .map(|d| d.as_secs_f64())
100        .unwrap_or_default()
101}
102
103#[macro_export]
104macro_rules! debug {
105    ($($arg:tt)*) => { $crate::log::log($crate::log::Level::Debug, format!($($arg)*)) };
106}
107
108#[macro_export]
109macro_rules! info {
110    ($($arg:tt)*) => { $crate::log::log($crate::log::Level::Info, format!($($arg)*)) };
111}
112
113#[macro_export]
114macro_rules! warn {
115    ($($arg:tt)*) => { $crate::log::log($crate::log::Level::Warn, format!($($arg)*)) };
116}
117
118#[macro_export]
119macro_rules! error {
120    ($($arg:tt)*) => { $crate::log::log($crate::log::Level::Error, format!($($arg)*)) };
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn level_threshold_is_respected() {
129        set_level(Level::Warn);
130        assert!(!enabled(Level::Info));
131        assert!(enabled(Level::Error));
132        set_level(Level::Info);
133    }
134
135    #[test]
136    fn levels_parse_from_configuration_strings() {
137        assert_eq!(Level::parse("WARNING"), Some(Level::Warn));
138        assert_eq!(Level::parse("nonsense"), None);
139    }
140}