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 ]
22│
23▼ (by update_log_level)
24[ RELOAD_HANDLE ] ───(modify function)───► [ EnvFilter (e.g.: "info") ]
25│
26▼ (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}