Skip to main content

lunar_lib/
log.rs

1use std::{
2    fmt::{self, Arguments, Debug},
3    io::{self, Write},
4    str::FromStr,
5    sync::{
6        OnceLock,
7        atomic::{AtomicU8, Ordering},
8    },
9};
10
11static LOGGER: OnceLock<&'static dyn Log> = OnceLock::new();
12
13/// Sets the logger for the running program. This can only be done once and cannot be reversed
14///
15/// # Errors
16///
17/// Errors if a logger is already set, returns the set logger
18pub fn set_logger(logger: &'static dyn Log) -> Result<(), &'static dyn Log> {
19    LOGGER.set(logger)
20}
21
22/// Gets the logger for the running program, if any
23pub fn get_logger() -> Option<&'static dyn Log> {
24    LOGGER.get().copied()
25}
26
27#[derive(Debug, thiserror::Error)]
28pub enum LogLevelParseError {
29    #[error("Invalid log level: {0}")]
30    InvalidInput(String),
31}
32
33/// A level filter for any incoming logs to filter logs that should be disabled
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
35pub enum LevelFilter {
36    Off,
37    Error,
38    Warn,
39    Info,
40    Debug,
41    Trace,
42}
43
44/// The log level of something.
45///
46/// This is used in logging to prevent logs which should be filtered out by the [`LevelFilter`]
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
48pub enum LogLevel {
49    Error = 1,
50    Warn,
51    Info,
52    Debug,
53    Trace,
54}
55
56impl fmt::Display for LogLevel {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            LogLevel::Error => f.write_str("Error"),
60            LogLevel::Warn => f.write_str("Warn"),
61            LogLevel::Info => f.write_str("Info"),
62            LogLevel::Debug => f.write_str("Debug"),
63            LogLevel::Trace => f.write_str("Trace"),
64        }
65    }
66}
67
68impl FromStr for LevelFilter {
69    type Err = LogLevelParseError;
70
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        match s.to_ascii_lowercase().as_str() {
73            "off" | "quiet" => Ok(LevelFilter::Off),
74            "error" => Ok(LevelFilter::Error),
75            "warn" => Ok(LevelFilter::Warn),
76            "info" => Ok(LevelFilter::Info),
77            "debug" => Ok(LevelFilter::Debug),
78            "trace" => Ok(LevelFilter::Trace),
79            _ => Err(LogLevelParseError::InvalidInput(s.to_owned())),
80        }
81    }
82}
83
84static MAX_LOG_LEVEL: AtomicU8 = AtomicU8::new(0);
85/// The max log level of a program. Defaults to [`LevelFilter::Off`]
86///
87/// This function does not have to be used if you want custom log filtering logic. See [`Log::enabled()`] for more info
88pub fn set_max_log_level(level: LevelFilter) {
89    MAX_LOG_LEVEL.store(level as u8, Ordering::Relaxed);
90}
91
92/// A basic function to check if a [`LogLevel`] is filtered out by the max log level's [`LevelFilter`]
93///
94/// This function does not have to be used if you want custom log filtering logic. See [`Log::enabled()`] for more info
95pub fn can_log(level: LogLevel) -> bool {
96    MAX_LOG_LEVEL.load(Ordering::Relaxed) >= level as u8
97}
98
99/// A very simple logger that prints to `stdout` or `stderr`
100#[derive(Debug)]
101pub struct CliLogger;
102
103pub trait Log: Sync + Debug {
104    /// This function should return true for the given log level if its allowed to be logged
105    fn enabled(&self, level: LogLevel) -> bool;
106
107    /// This function should provide an unchecked log function, like calling `println!()`
108    ///
109    /// See [`Self::log()`] for a version that uses [`Self::enabled()`] to only log a string if logging is enabled
110    fn log_unchecked(&self, level: LogLevel, msg: Arguments);
111
112    /// Logs a string if allowed
113    fn log(&self, level: LogLevel, msg: Arguments) {
114        if self.enabled(level) {
115            self.log_unchecked(level, msg);
116        }
117    }
118}
119
120impl Log for CliLogger {
121    fn enabled(&self, level: LogLevel) -> bool {
122        MAX_LOG_LEVEL.load(Ordering::Relaxed) >= level as u8
123    }
124
125    fn log_unchecked(&self, level: LogLevel, msg: Arguments) {
126        let output: &mut dyn Write = match level {
127            LogLevel::Error | LogLevel::Warn => &mut io::stderr(),
128            _ => &mut io::stdout(),
129        };
130
131        match level {
132            LogLevel::Info => {}
133            _ => {
134                let _ = write!(output, "[{level}]: ");
135            }
136        }
137        let _ = output.write_fmt(msg);
138        let _ = output.write(b"\n");
139    }
140}
141
142/// Attempts to log format args to the current logger lazily
143///
144/// If the logger rejects the log or none is set, the args will not be parsed
145#[macro_export]
146macro_rules! log {
147    ($level:expr, $($arg:tt)*) => {{
148        if let Some(logger) = $crate::log::get_logger()
149            && logger.enabled($level) {
150                logger.log_unchecked($level, format_args!($($arg)*))
151        }
152    }};
153}
154
155/// Attempts to log format args as an error to the current logger lazily
156///
157/// If the logger rejects the log or none is set, the args will not be parsed
158#[macro_export]
159macro_rules! error {
160    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Error, $($arg)*) };
161}
162
163/// Attempts to log format args as a warning to the current logger lazily
164///
165/// If the logger rejects the log or none is set, the args will not be parsed
166#[macro_export]
167macro_rules! warn {
168    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Warn, $($arg)*) };
169}
170
171/// Attempts to log format args as info to the current logger lazily
172///
173/// If the logger rejects the log or none is set, the args will not be parsed
174#[macro_export]
175macro_rules! info {
176    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Info, $($arg)*) };
177}
178
179/// Attempts to log format args as debug to the current logger lazily
180///
181/// If the logger rejects the log or none is set, the args will not be parsed
182#[macro_export]
183macro_rules! debug {
184    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Debug, $($arg)*) };
185}
186
187/// Attempts to log format args as a trace to the current logger lazily
188///
189/// If the logger rejects the log or none is set, the args will not be parsed
190#[macro_export]
191macro_rules! trace {
192    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Trace, $($arg)*) };
193}