pretty_logging/
lib.rs

1//! A minimal and pretty logger for the [`log`] crate.
2//!
3//! To initialize it, call the [`init()`] function.
4//!
5//! ```
6//! use log::LevelFilter;
7//!
8//! pretty_logging::init(LevelFilter::Info, []);
9//!
10//! log::trace!("Hello pretty logger!");
11//! log::debug!("Hello pretty logger!");
12//! log::info!("Hello pretty logger!");
13//! log::warn!("Hello pretty logger!");
14//! log::error!("Hello pretty logger!");
15//! panic!("Hello pretty logger!");
16//! ```
17//!
18//! The [`init()`] function spawns a thread which reads all incoming log messages and writes them
19//! to the standard/error output. It holds a lock on the standard output to ensure that log
20//! messages are printed in the order they are received, so once the logger is initialized, you
21//! must avoid using [`println!`] and [`eprintln!`].
22//!
23//! You should note that when using this logger, the [`init()`] function will set a custom panic
24//! hook, which will override any previous panic hooks set. If you use custom panic hooks, make
25//! sure to set them after [`init()`] is called.
26
27use std::{io::Write, panic, sync::mpsc::Sender, thread};
28
29use colored::Colorize;
30use log::{Level, LevelFilter};
31use time::{OffsetDateTime, macros::format_description};
32
33#[derive(Clone)]
34struct Logger(Vec<String>, Sender<(OutputChannel, String)>);
35
36enum OutputChannel {
37    Standard,
38    Error,
39}
40
41impl From<Level> for OutputChannel {
42    fn from(level: Level) -> Self {
43        match level {
44            Level::Error => OutputChannel::Error,
45            _ => OutputChannel::Standard,
46        }
47    }
48}
49
50impl Logger {
51    fn new(modules: Vec<String>) -> Self {
52        let (sender, receiver) = std::sync::mpsc::channel();
53
54        thread::spawn(move || {
55            let mut std_lock = std::io::stdout().lock();
56            let mut err_lock = std::io::stderr().lock();
57
58            for (output, line) in receiver {
59                match output {
60                    OutputChannel::Standard => {
61                        writeln!(std_lock, "{line}").ok();
62                        std_lock.flush().ok();
63                    }
64                    OutputChannel::Error => {
65                        writeln!(err_lock, "{line}").ok();
66                        err_lock.flush().ok();
67                    }
68                }
69            }
70        });
71
72        Self(modules, sender)
73    }
74}
75
76impl log::Log for Logger {
77    fn enabled(&self, metadata: &log::Metadata) -> bool {
78        if self.0.is_empty() {
79            return true;
80        }
81
82        for module in &self.0 {
83            if metadata.target() == *module || metadata.target().starts_with(&format!("{module}::"))
84            {
85                return true;
86            }
87        }
88
89        false
90    }
91
92    fn log(&self, record: &log::Record) {
93        if self.enabled(record.metadata()) {
94            self.1
95                .send((
96                    record.level().into(),
97                    format!(
98                        "{} {} {}",
99                        get_formatted_timestamp(),
100                        get_formatted_level(record.level().as_str()),
101                        record.args(),
102                    ),
103                ))
104                .ok();
105        }
106    }
107
108    fn flush(&self) {}
109}
110
111use std::sync::OnceLock;
112
113static LOGGER: OnceLock<Logger> = OnceLock::new();
114
115/// Initializes the logger. This function spawns a thread to read log messages and write them to
116/// the appropriate output without blocking the current task.
117///
118/// This function also sets a custom panic hook to log panics. If you need to set a custom panic
119/// hook, set it after this function is called to prevent your custom hook from being overriden.
120///
121/// Once this function is called, you must avoid calling [`println!`] and [`eprintln!`].
122///
123/// Arguments:
124/// * `filter` - The level filter for the logger.
125/// * `modules` - A list of root module names which to log. An empty array will log all modules.
126///   You may want to set this to your crate's name, like `["my_crate_name"]`, to only display logs
127///   from your crate's modules.
128/// 
129/// Example:
130/// ```
131/// use log::LevelFilter;
132/// 
133/// // Displays all logs from all crates.
134/// pretty_logging::init(LevelFilter::Trace, []);
135/// ```
136pub fn init(filter: LevelFilter, modules: impl IntoIterator<Item = impl ToString>) {
137    LOGGER
138        .set(Logger::new(
139            modules.into_iter().map(|m| m.to_string()).collect(),
140        ))
141        .ok();
142
143    log::set_logger(LOGGER.get().unwrap())
144        .map(|()| log::set_max_level(filter))
145        .unwrap();
146
147    panic::set_hook(Box::new(move |panic_info| {
148        if filter == LevelFilter::Off {
149            return;
150        }
151
152        let line = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
153            format!(
154                "{} {} {}",
155                get_formatted_timestamp(),
156                get_formatted_level("PANIC"),
157                s,
158            )
159        } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
160            format!(
161                "{} {} {}",
162                get_formatted_timestamp(),
163                get_formatted_level("PANIC"),
164                s,
165            )
166        } else {
167            format!(
168                "{} {} A panic occurred! Exitting...",
169                get_formatted_timestamp(),
170                get_formatted_level("PANIC"),
171            )
172        };
173
174        LOGGER
175            .get()
176            .unwrap()
177            .1
178            .send((OutputChannel::Error, line))
179            .ok();
180    }));
181}
182
183fn get_formatted_timestamp() -> String {
184    let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
185
186    let format = format_description!(
187        "[day]/[month]/[year] at [hour]:[minute]:[second].[subsecond digits:2]"
188    );
189
190    now.format(&format).unwrap().dimmed().to_string()
191}
192
193fn get_formatted_level(level: &str) -> String {
194    let string = format!("[{level}]");
195    let string = format!("{string:<7}");
196
197    match level {
198        "TRACE" => string.dimmed().to_string(),
199        "DEBUG" => string.white().to_string(),
200        "INFO" => string.blue().to_string(),
201        "WARN" => string.yellow().to_string(),
202        "ERROR" | "PANIC" => string.red().bold().to_string(),
203        _ => string.red().bold().to_string(),
204    }
205}