Skip to main content

rustlog/
lib.rs

1#![forbid(missing_docs, unsafe_code)]
2//! A minimal logging crate.
3
4use core::fmt::Arguments;
5use std::io::{self, IsTerminal, Write};
6use std::path::Path;
7use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
8use std::sync::{Mutex as StdMutex, OnceLock};
9use std::time::Instant;
10
11/// Local logger
12pub mod local;
13
14/// Log levels
15#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
16#[repr(u8)]
17pub enum Level {
18    /// Trace
19    Trace = 0,
20    /// Debug
21    Debug,
22    /// Info
23    Info,
24    /// Warn
25    Warn,
26    /// Error
27    Error,
28    /// Fatal
29    Fatal,
30}
31
32// In debug builds, include all levels (Trace+).
33// In release builds, compile out TRACE/DEBUG entirely for zero overhead.
34#[cfg(debug_assertions)]
35const CT_MIN: Level = Level::Trace;
36#[cfg(not(debug_assertions))]
37const CT_MIN: Level = Level::Info;
38static RUNTIME_LEVEL: AtomicU8 = AtomicU8::new(Level::Info as u8);
39static SHOW_TID: AtomicBool = AtomicBool::new(cfg!(feature = "thread-id"));
40static SHOW_TIME: AtomicBool = AtomicBool::new(cfg!(feature = "timestamp"));
41static SHOW_GROUP: AtomicBool = AtomicBool::new(true);
42static SHOW_FILE_LINE: AtomicBool = AtomicBool::new(cfg!(feature = "file-line"));
43
44/// Color mode
45#[derive(Copy, Clone, Eq, PartialEq, Debug)]
46#[repr(u8)]
47pub enum ColorMode {
48    /// Auto
49    Auto,
50    /// Always
51    Always,
52    /// Never
53    Never,
54}
55static COLOR_MODE: AtomicU8 = AtomicU8::new(ColorMode::Auto as u8);
56#[inline]
57const fn level_from_u8(x: u8) -> Level {
58    match x {
59        0 => Level::Trace,
60        1 => Level::Debug,
61        3 => Level::Warn,
62        4 => Level::Error,
63        5 => Level::Fatal,
64        _ => Level::Info, // sane default
65    }
66}
67#[inline]
68const fn color_mode_from_u8(x: u8) -> ColorMode {
69    match x {
70        1 => ColorMode::Always,
71        2 => ColorMode::Never,
72        _ => ColorMode::Auto,
73    }
74}
75#[inline]
76fn color_mode() -> ColorMode {
77    color_mode_from_u8(COLOR_MODE.load(Ordering::Relaxed))
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81/// parsing color mode error
82pub struct ParseColorModeError;
83
84impl core::str::FromStr for ColorMode {
85    type Err = ParseColorModeError;
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        if s.eq_ignore_ascii_case("always") {
88            Ok(Self::Always)
89        } else if s.eq_ignore_ascii_case("never") {
90            Ok(Self::Never)
91        } else if s.is_empty() || s.eq_ignore_ascii_case("auto") {
92            Ok(Self::Auto)
93        } else {
94            Err(ParseColorModeError)
95        }
96    }
97}
98
99impl core::convert::TryFrom<&str> for ColorMode {
100    type Error = ParseColorModeError;
101    fn try_from(s: &str) -> Result<Self, Self::Error> {
102        s.parse()
103    }
104}
105
106/// Output target
107#[derive(Copy, Clone)]
108pub enum Target {
109    /// stdout
110    Stdout,
111    /// stderr
112    Stderr,
113    /// custom
114    Writer,
115}
116static TARGET: OnceLock<Target> = OnceLock::new();
117static WRITER: OnceLock<StdMutex<Box<dyn Write + Send>>> = OnceLock::new();
118/// Sets the output target once. Subsequent calls are ignored.
119/// Call this early (e.g., at program start) if you need `Stdout` or a custom `Writer`.
120pub fn set_target(t: Target) {
121    let _ = TARGET.set(t);
122}
123/// Sets the output target to a custom writer.
124///
125/// Note: the target is configured only once; call this before emitting any logs.
126pub fn set_writer(w: Box<dyn Write + Send>) {
127    let _ = WRITER.set(StdMutex::new(w));
128    // Best-effort: if the target hasn't been selected yet, route output to the writer.
129    let _ = TARGET.set(Target::Writer);
130}
131/// Sets the output target to a file.
132/// # Errors
133/// This function will return an error if the file cannot be opened for writing.
134pub fn set_file(path: impl AsRef<Path>) -> io::Result<()> {
135    let f = std::fs::OpenOptions::new()
136        .create(true)
137        .append(true)
138        .open(path)?;
139    set_writer(Box::new(f));
140    set_target(Target::Writer);
141    Ok(())
142}
143#[inline]
144fn target() -> Target {
145    *TARGET.get_or_init(|| Target::Stderr)
146}
147
148static EMIT_LOCK: StdMutex<()> = StdMutex::new(());
149
150/// Returns `true` if the logger is enabled for the given level
151#[inline]
152#[must_use]
153pub const fn ct_enabled(l: Level) -> bool {
154    (l as u8) >= (CT_MIN as u8)
155}
156#[inline]
157fn rt_enabled(l: Level) -> bool {
158    (l as u8) >= RUNTIME_LEVEL.load(Ordering::Relaxed)
159}
160
161#[cfg(feature = "color")]
162mod color {
163    pub const RST: &str = "\x1b[0m";
164    pub const BOLD: &str = "\x1b[1m";
165    pub const TRACE: &str = "\x1b[90m"; // bright black
166    pub const DEBUG: &str = "\x1b[36m"; // cyan
167    pub const INFO: &str = "\x1b[32m"; // green
168    pub const WARN: &str = "\x1b[33m"; // yellow
169    pub const ERROR: &str = "\x1b[31m"; // red
170    pub const FATAL: &str = "\x1b[35m"; // magenta
171}
172/// Returns the color code for the given level
173#[cfg(feature = "color")]
174#[inline]
175const fn level_color(l: Level) -> &'static str {
176    use color::{DEBUG, ERROR, FATAL, INFO, TRACE, WARN};
177    match l {
178        Level::Trace => TRACE,
179        Level::Debug => DEBUG,
180        Level::Info => INFO,
181        Level::Warn => WARN,
182        Level::Error => ERROR,
183        Level::Fatal => FATAL,
184    }
185}
186
187/// Returns the uppercase level name
188#[inline]
189const fn level_name(l: Level) -> &'static str {
190    match l {
191        Level::Trace => "TRACE",
192        Level::Debug => "DEBUG",
193        Level::Info => "INFO",
194        Level::Warn => "WARN",
195        Level::Error => "ERROR",
196        Level::Fatal => "FATAL",
197    }
198}
199
200fn use_color() -> bool {
201    #[cfg(not(feature = "color"))]
202    {
203        false
204    }
205    #[cfg(feature = "color")]
206    {
207        match color_mode() {
208            ColorMode::Always => true,
209            ColorMode::Never => false,
210            ColorMode::Auto => match target() {
211                Target::Stdout => io::stdout().is_terminal(),
212                Target::Stderr => io::stderr().is_terminal(),
213                Target::Writer => false, // unknown sink => assume no TTY
214            },
215        }
216    }
217}
218
219/// Returns the current logging level
220#[inline]
221pub fn level() -> Level {
222    level_from_u8(RUNTIME_LEVEL.load(Ordering::Relaxed))
223}
224/// Sets the current logging level
225pub fn set_level(l: Level) {
226    RUNTIME_LEVEL.store(l as u8, Ordering::Relaxed);
227}
228/// Show thread ids
229pub fn set_show_thread_id(on: bool) {
230    SHOW_TID.store(on, Ordering::Relaxed);
231}
232/// Show timestamps
233pub fn set_show_time(on: bool) {
234    SHOW_TIME.store(on, Ordering::Relaxed);
235}
236/// Show file and line
237pub fn set_show_file_line(on: bool) {
238    SHOW_FILE_LINE.store(on, Ordering::Relaxed);
239}
240/// Show group
241pub fn set_show_group(on: bool) {
242    SHOW_GROUP.store(on, Ordering::Relaxed);
243}
244/// Sets the color mode
245pub fn set_color_mode(mode: ColorMode) {
246    COLOR_MODE.store(mode as u8, Ordering::Relaxed);
247}
248/// Initialize the logger from environment variables
249pub fn init_from_env() {
250    if let Ok(s) = std::env::var("RUST_LOG_LEVEL") {
251        let l = match s.to_lowercase().as_str() {
252            "trace" => Level::Trace,
253            "debug" => Level::Debug,
254            "info" => Level::Info,
255            "warn" => Level::Warn,
256            "error" => Level::Error,
257            "fatal" => Level::Fatal,
258            _ => level(),
259        };
260        set_level(l);
261    }
262    if let Ok(s) = std::env::var("RUST_LOG_COLOR") {
263        set_color_mode(s.parse().unwrap_or(ColorMode::Auto));
264    }
265    if let Ok(s) = std::env::var("RUST_LOG_SHOW_TID") {
266        set_show_thread_id(s == "1" || s.eq_ignore_ascii_case("true"));
267    }
268    if let Ok(s) = std::env::var("RUST_LOG_SHOW_TIME") {
269        set_show_time(s == "1" || s.eq_ignore_ascii_case("true"));
270    }
271}
272
273/// Correct Gregorian Y-M-D from days since 1970-01-01
274#[inline]
275#[allow(dead_code)]
276const fn civil_from_days_utc(days_since_unix_epoch: i64) -> (i32, u32, u32) {
277    // Howard Hinnant’s algorithm
278    let z = days_since_unix_epoch + 719_468; // days since 0000-03-01
279    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
280    let doe = z - era * 146_097; // [0, 146096]
281    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; // [0,399]
282    let yd = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
283    let mp = (5 * yd + 2) / 153; // [0, 11]
284    let d = yd - (153 * mp + 2) / 5 + 1; // [1, 31]
285    let m = mp + 3 - 12 * (mp / 10); // [1, 12]
286    let y = 400 * era + yoe + (m <= 2) as i64; // year
287    #[allow(clippy::cast_possible_truncation)]
288    #[allow(clippy::cast_sign_loss)]
289    (y as i32, m as u32, d as u32)
290}
291#[inline]
292fn write_timestamp(mut w: impl Write) {
293    #[cfg(all(feature = "timestamp", not(feature = "localtime")))]
294    {
295        use std::time::{SystemTime, UNIX_EPOCH};
296        let now = SystemTime::now()
297            .duration_since(UNIX_EPOCH)
298            .unwrap_or_default();
299        let secs = now.as_secs() as i64;
300        let ms = now.subsec_millis();
301
302        let days = secs.div_euclid(86_400);
303        let sod = secs.rem_euclid(86_400);
304        let h = (sod / 3_600) as i64;
305        let m = (sod % 3_600 / 60) as i64;
306        let s = (sod % 60) as i64;
307
308        let (year, month, day) = civil_from_days_utc(days);
309        let _ = write!(
310            w,
311            "{year:04}-{month:02}-{day:02} {h:02}:{m:02}:{s:02}.{ms:03}Z "
312        );
313    }
314    #[cfg(all(feature = "timestamp", feature = "localtime"))]
315    {
316        // Local time via `time` crate if you enable the `localtime` feature
317        static TS_FMT: OnceLock<Vec<time::format_description::FormatItem<'static>>> =
318            OnceLock::new();
319        let fmt = TS_FMT.get_or_init(|| {
320            time::format_description::parse(
321                "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]",
322            )
323            .expect("valid timestamp format description")
324        });
325
326        let now = std::time::SystemTime::now();
327        let now: time::OffsetDateTime = now.into();
328        let now =
329            now.to_offset(time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC));
330        let _ = write!(w, "{} ", now.format(fmt).unwrap());
331    }
332}
333
334#[inline]
335fn write_tid(mut w: impl Write) {
336    if SHOW_TID.load(Ordering::Relaxed) {
337        #[cfg(feature = "thread-id")]
338        let _ = write!(w, " [{:?}]", std::thread::current().id());
339    }
340}
341
342#[inline]
343fn write_level(mut w: impl Write, l: Level, use_color: bool) {
344    #[cfg(feature = "color")]
345    if use_color {
346        let _ = write!(w, "{}{:<5}{}", level_color(l), level_name(l), color::RST);
347        return;
348    }
349    let _ = write!(w, "{:<5}", level_name(l));
350}
351
352fn emit_raw_bytes(bytes: &[u8]) {
353    let _g = EMIT_LOCK.lock().unwrap();
354    match target() {
355        Target::Stdout => {
356            let _ = io::stdout().lock().write_all(bytes);
357        }
358        Target::Stderr => {
359            let _ = io::stderr().lock().write_all(bytes);
360        }
361        Target::Writer => {
362            if let Some(m) = WRITER.get() {
363                let mut w = m.lock().unwrap();
364                let _ = w.write_all(bytes);
365            }
366        }
367    }
368}
369
370/// Emit a log message
371#[inline]
372pub fn emit(
373    l: Level,
374    group: Option<&'static str>,
375    file: &'static str,
376    line_no: u32,
377    args: Arguments,
378) {
379    if !rt_enabled(l) {
380        return;
381    }
382    let use_color = use_color();
383    let mut buf = Vec::<u8>::new();
384
385    if SHOW_TIME.load(Ordering::Relaxed) {
386        write_timestamp(&mut buf);
387    }
388    write_level(&mut buf, l, use_color);
389    write_tid(&mut buf);
390    if SHOW_FILE_LINE.load(Ordering::Relaxed) {
391        let _ = write!(&mut buf, " <{file}:{line_no}>");
392    }
393    if SHOW_GROUP.load(Ordering::Relaxed) {
394        if let Some(g) = group {
395            #[cfg(feature = "color")]
396            if use_color {
397                let _ = write!(
398                    &mut buf,
399                    " [{}{}{}{}]",
400                    color::BOLD,
401                    level_color(l),
402                    g,
403                    color::RST
404                );
405            } else {
406                let _ = write!(&mut buf, " [{g}]");
407            }
408            #[cfg(not(feature = "color"))]
409            {
410                let _ = write!(&mut buf, " [{g}]");
411            }
412        }
413    }
414    let _ = buf.write_all(b" ");
415    let _ = buf.write_fmt(args);
416    let _ = buf.write_all(b"\n");
417    emit_raw_bytes(&buf);
418}
419
420/// Emit a log message
421#[macro_export]
422macro_rules! __rustlog_log { ($lvl:expr, $grp:expr, $($t:tt)+) => {{ if $crate::ct_enabled($lvl) { $crate::emit($lvl, $grp, file!(), line!(), format_args!($($t)+)) } }} }
423/// trace
424#[macro_export]
425macro_rules! trace { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Trace, None, $($t)+) } }
426/// debug
427#[macro_export]
428macro_rules! debug { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Debug, None, $($t)+) } }
429/// info
430#[macro_export]
431macro_rules! info  { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Info,  None, $($t)+) } }
432/// warning
433#[macro_export]
434macro_rules! warn  { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Warn,  None, $($t)+) } }
435/// error
436#[macro_export]
437macro_rules! error { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Error, None, $($t)+) } }
438/// fatal
439#[macro_export]
440macro_rules! fatal { ($($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Fatal, None, $($t)+) } }
441/// trace group
442#[macro_export]
443macro_rules! trace_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Trace, Some($grp), $($t)+) } }
444/// debug group
445#[macro_export]
446macro_rules! debug_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Debug, Some($grp), $($t)+) } }
447/// info group
448#[macro_export]
449macro_rules! info_group  { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Info,  Some($grp), $($t)+) } }
450/// warning group
451#[macro_export]
452macro_rules! warn_group  { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Warn,  Some($grp), $($t)+) } }
453/// error group
454#[macro_export]
455macro_rules! error_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Error, Some($grp), $($t)+) } }
456/// fatal group
457#[macro_export]
458macro_rules! fatal_group { ($grp:expr, $($t:tt)+) => { $crate::__rustlog_log!($crate::Level::Fatal, Some($grp), $($t)+) } }
459/// Time a block
460#[macro_export]
461macro_rules! scope_time {
462    ($label:expr) => {
463        let _scope_time_guard = $crate::TimerGuard::new_at($label, file!(), line!());
464    };
465    ($label:expr, $body:block) => {{
466        let _scope_time_guard = $crate::TimerGuard::new_at($label, file!(), line!());
467        $body
468    }};
469}
470/// Human readable duration
471pub struct HumanDuration(pub std::time::Duration);
472impl core::fmt::Display for HumanDuration {
473    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
474        let d = self.0;
475        let secs = d.as_secs();
476        let nanos = d.subsec_nanos();
477        if secs == 0 {
478            if nanos < 1_000 {
479                write!(formatter, "{nanos} ns")
480            } else if nanos < 1_000_000 {
481                write!(formatter, "{} us", nanos / 1_000)
482            } else {
483                let ms = nanos / 1_000_000;
484                let us = (nanos / 1_000) % 1_000;
485                write!(formatter, "{ms}.{us:03} ms")
486            }
487        } else if secs < 60 {
488            let ms = nanos / 1_000_000;
489            write!(formatter, "{secs}.{ms:03} s")
490        } else if secs < 3_600 {
491            let m = secs / 60;
492            let s = secs % 60;
493            let ms = nanos / 1_000_000;
494            write!(formatter, "{m}m{s:02}.{ms:03}s")
495        } else if secs < 86_400 {
496            let h = secs / 3_600;
497            let m = (secs % 3_600) / 60;
498            let s = secs % 60;
499            let ms = nanos / 1_000_000;
500            write!(formatter, "{h}h{m:02}m{s:02}.{ms:03}s")
501        } else {
502            let days = secs / 86_400;
503            let rem = secs % 86_400;
504            let h = rem / 3_600;
505            let m = (rem % 3_600) / 60;
506            let s = rem % 60;
507            let ms = nanos / 1_000_000;
508            write!(formatter, "{days}d {h:02}h{m:02}m{s:02}.{ms:03}s")
509        }
510    }
511}
512impl From<std::time::Duration> for HumanDuration {
513    fn from(d: std::time::Duration) -> Self {
514        Self(d)
515    }
516}
517
518/// Timer guard
519pub struct TimerGuard {
520    label: &'static str,
521    start: Instant,
522    file: &'static str,
523    line: u32,
524}
525impl TimerGuard {
526    /// Create a new timer guard
527    #[inline]
528    #[must_use]
529    pub fn new_at(label: &'static str, file: &'static str, line: u32) -> Self {
530        Self {
531            label,
532            start: Instant::now(),
533            file,
534            line,
535        }
536    }
537}
538impl Drop for TimerGuard {
539    fn drop(&mut self) {
540        let elapsed = self.start.elapsed();
541        emit(
542            Level::Info,
543            Some(self.label),
544            self.file,
545            self.line,
546            format_args!("took {}", HumanDuration(elapsed)),
547        );
548    }
549}
550
551/// Emit a banner
552#[inline]
553pub fn banner_with(name: &str, version: &str) {
554    emit_raw_bytes(name.as_bytes());
555    emit_raw_bytes(b" v");
556    emit_raw_bytes(version.as_bytes());
557    emit_raw_bytes(b"\n");
558}
559
560#[macro_export]
561/// Emit a banner
562macro_rules! banner {
563    () => {
564        $crate::banner_with(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
565    };
566    ($name:expr, $version:expr) => {
567        $crate::banner_with($name, $version)
568    };
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use core::time::Duration as StdDuration;
575
576    #[test]
577    fn human_duration_formats_all_ranges() {
578        assert_eq!(
579            format!("{}", HumanDuration(StdDuration::from_nanos(500))),
580            "500 ns"
581        );
582        assert_eq!(
583            format!("{}", HumanDuration(StdDuration::from_nanos(1_500))),
584            "1 us"
585        );
586        assert_eq!(
587            format!("{}", HumanDuration(StdDuration::from_nanos(1_234_000))),
588            "1.234 ms"
589        );
590        assert_eq!(
591            format!("{}", HumanDuration(StdDuration::from_millis(1_234))),
592            "1.234 s"
593        );
594        assert_eq!(
595            format!("{}", HumanDuration(StdDuration::from_secs(65))),
596            "1m05.000s"
597        );
598        assert_eq!(
599            format!(
600                "{}",
601                HumanDuration(StdDuration::from_secs(3 * 3600 + 7 * 60 + 5))
602            ),
603            "3h07m05.000s"
604        );
605        assert_eq!(
606            format!("{}", HumanDuration(StdDuration::from_secs(2 * 86_400 + 5))),
607            "2d 00h00m05.000s"
608        );
609    }
610}