Skip to main content

yog_logging/
lib.rs

1//! Yog logging — lightweight logging macros with a consistent `[yog]` format.
2//!
3//! The macros forward to [`log`], which does the formatting. Because they expand
4//! to a single call that only formats its arguments when actually invoked, there
5//! is no cost at a call site that is compiled out or behind a disabled level.
6//!
7//! ```
8//! yog_logging::info!("loaded {} mods", 3);
9//! yog_logging::warn!("slow tick: {}ms", 51);
10//! yog_logging::error!("failed: {}", "boom");
11//! ```
12
13use std::io::Write;
14use std::sync::OnceLock;
15
16/// Severity of a log record.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[repr(u32)]
19pub enum Level {
20    Info = 0,
21    Warn = 1,
22    Error = 2,
23}
24
25impl Level {
26    #[inline]
27    const fn tag(self) -> &'static str {
28        match self {
29            Level::Info => "INFO",
30            Level::Warn => "WARN",
31            Level::Error => "ERROR",
32        }
33    }
34
35    /// The level a host passes as a number ([`Level`] as `u32`); unknown
36    /// numbers are errors, so nothing is quieter than it was meant to be.
37    pub const fn from_u32(n: u32) -> Level {
38        match n {
39            0 => Level::Info,
40            1 => Level::Warn,
41            _ => Level::Error,
42        }
43    }
44}
45
46/// Where records go once a host has a log of its own (the game's): the
47/// runtime and every mod install it once at start, so their records land in
48/// the game's log with everything else.
49pub type Sink = fn(Level, &str);
50
51static SINK: OnceLock<Sink> = OnceLock::new();
52
53/// Sends every record from now on to `sink` instead of stdout/stderr. The
54/// first call wins: a process has one log.
55pub fn set_sink(sink: Sink) {
56    let _ = SINK.set(sink);
57}
58
59/// Formats and writes one record — to the installed [`Sink`], or, before
60/// there is one, to stderr (errors) or stdout.
61#[doc(hidden)]
62#[inline]
63pub fn log(level: Level, args: std::fmt::Arguments<'_>) {
64    if let Some(sink) = SINK.get() {
65        sink(level, &args.to_string());
66        return;
67    }
68    let line = format_args!("[yog] [{}] {}", level.tag(), args);
69    // Nowhere else to report a failed write of a log line.
70    if matches!(level, Level::Error) {
71        let _ = writeln!(std::io::stderr().lock(), "{line}");
72    } else {
73        let _ = writeln!(std::io::stdout().lock(), "{line}");
74    }
75}
76
77/// Log at INFO level.
78#[macro_export]
79macro_rules! info {
80    ($($arg:tt)*) => {
81        $crate::log($crate::Level::Info, ::core::format_args!($($arg)*))
82    };
83}
84
85/// Log at WARN level.
86#[macro_export]
87macro_rules! warn {
88    ($($arg:tt)*) => {
89        $crate::log($crate::Level::Warn, ::core::format_args!($($arg)*))
90    };
91}
92
93/// Log at ERROR level.
94#[macro_export]
95macro_rules! error {
96    ($($arg:tt)*) => {
97        $crate::log($crate::Level::Error, ::core::format_args!($($arg)*))
98    };
99}