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}