Skip to main content

rsnaker/game_logic/logger/
log_configuration.rs

1use clap::ValueEnum;
2use serde::{Deserialize, Serialize};
3use std::sync::OnceLock;
4use time::macros::format_description;
5use tracing_subscriber::fmt::time::LocalTime;
6use tracing_subscriber::prelude::*;
7use tracing_subscriber::{fmt, reload, EnvFilter, Registry};
8
9#[derive(Debug, Copy, Clone, Serialize, Deserialize, ValueEnum, Default, PartialEq, Eq)]
10#[serde(rename_all = "lowercase")]
11pub enum LogLevel {
12    #[default]
13    Off,
14    Error,
15    Warn,
16    Info,
17    Debug,
18    Trace,
19}
20/*
21[ App ]
2223▼ (by update_log_level)
24[ RELOAD_HANDLE ] ───(modify function)───► [ EnvFilter (e.g.: "info") ]
2526▼ (apply the filter)
27[ Log file ]
28Work thanks to modify function: pub fn modify(&self, f: impl FnOnce(&mut L)) -> Result<(), Error>
29Invokes a closure with a mutable reference to the current layer or filter, allowing it to be modified in place.
30So the RELOAD_HANDLE itself is only written once, that the EnvFilter, which is updated as a &mut param
31*/
32static RELOAD_HANDLE: OnceLock<reload::Handle<EnvFilter, Registry>> = OnceLock::new();
33
34/// Initializes the global logger.
35/// Writing a Lib? Only use the tracing crate and its macros (info!, span!). Do not initialize anything.
36/// Writing a Binary? Use tracing-subscriber to build the registry and handlers.
37/// Missing dependency logs? Check your `EnvFilter` string rules and ensure the "log" feature is enabled on tracing-subscriber to catch legacy logs
38/// Returns a `WorkerGuard` that must be kept alive to ensure logs are flushed to the file.
39/// If later wanna do rsyslog (libc): <https://docs.rs/syslog-tracing/0.3.1/syslog_tracing/struct.Syslog.html>
40/// Or the newcomer full rust: <https://crates.io/crates/tracing-rfc-5424>
41pub fn init_logger(
42    level: LogLevel,
43    file_name: &str,
44) -> Option<tracing_appender::non_blocking::WorkerGuard> {
45    let file_appender = tracing_appender::rolling::never(".", file_name);
46    let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
47    //To only keep my logs, as sled logs are useless (and otherwise collected),
48    // Each dependency log level can be configured with the EnvFilter, e.g.: "sled=info,rsnaker=debug"
49    let filter = EnvFilter::new(format!("rsnaker={},sled=off", log_level_to_str(level)));
50    let (filter_layer, reload_handle) = reload::Layer::new(filter);
51    //as default is:2026-05-16T12:49:06.919872Z, and we want to have [hour]:[minute]:[second][sub]
52    //LocalTime instead of UTC, to have the local time
53    let time_format = LocalTime::new(format_description!(
54        "[hour]:[minute]:[second].[subsecond digits:6]"
55    ));
56
57    let subscriber = tracing_subscriber::registry().with(filter_layer).with(
58        fmt::layer()
59            .with_writer(non_blocking)
60            .with_timer(time_format) // shorter time
61            .with_ansi(false) // Disable ANSI colors in the logfile
62            .with_target(false) //we do not want the name of the module as we have set the file and line number already
63            .with_thread_names(true) //or .with_thread_ids(), but name more useful
64            .with_line_number(true) // Added line numbers
65            .with_file(true), // Also added the filename for more context
66    );
67
68    if subscriber.try_init().is_err() {
69        eprintln!("Failed to initialize logger: logger already initialized");
70    } else {
71        let _ = RELOAD_HANDLE.set(reload_handle);
72    }
73
74    Some(guard)
75}
76
77/// Updates the global log level dynamically.
78pub fn update_log_level(level: LogLevel) {
79    if let Some(handle) = RELOAD_HANDLE.get() {
80        let _ = handle.modify(|filter| {
81            if let Ok(new_filter) =
82                EnvFilter::try_new(format!("rsnaker={},sled=off", log_level_to_str(level)))
83            {
84                *filter = new_filter;
85            }
86        });
87    }
88}
89
90fn log_level_to_str(level: LogLevel) -> &'static str {
91    //alt: use strum dependency, macro to get at compile time the code,
92    //but it is not really necessary here, as this is the only use case in the whole code
93    //// level.as_ref() could be auto-generated by strum with #[strum(serialize_all = "lowercase")], return &'static str
94    match level {
95        //See pub struct LevelFilter(Option<Level>); of tracing, defining the OFF level to disable logs
96        LogLevel::Off => "off",
97        LogLevel::Error => "error",
98        LogLevel::Warn => "warn",
99        LogLevel::Info => "info",
100        LogLevel::Debug => "debug",
101        LogLevel::Trace => "trace",
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_default_log_level() {
111        assert_eq!(LogLevel::default(), LogLevel::Off);
112    }
113}