Skip to main content

network_protocol/utils/
logging.rs

1use std::sync::{Once, OnceLock};
2use tracing::Level;
3use tracing_appender::rolling;
4use tracing_subscriber::{
5    fmt::{self, format::FmtSpan},
6    prelude::*,
7    registry, EnvFilter,
8};
9
10static INIT: Once = Once::new();
11
12/// Keeps the non-blocking file writer running for the life of the process.
13static FILE_WRITER_GUARD: OnceLock<tracing_appender::non_blocking::WorkerGuard> = OnceLock::new();
14
15/// LogConfig provides options for configuring the logging system
16#[derive(Clone, Debug)]
17pub struct LogConfig {
18    /// The name of the application
19    pub app_name: String,
20    /// The log level (trace, debug, info, warn, error)
21    pub log_level: Level,
22    /// Whether to enable JSON log format (useful for log aggregation)
23    pub json_format: bool,
24    /// Directory where log files should be stored, None for console only
25    pub log_dir: Option<String>,
26    /// Whether to log to stdout in addition to files
27    pub log_to_stdout: bool,
28}
29
30impl Default for LogConfig {
31    fn default() -> Self {
32        Self {
33            app_name: "network-protocol".to_string(),
34            log_level: Level::INFO,
35            json_format: false,
36            log_dir: None,
37            log_to_stdout: true,
38        }
39    }
40}
41
42/// Initialize the tracing system with the given configuration
43///
44/// # Example
45/// ```
46/// use network_protocol::utils::logging::{LogConfig, init_logging};
47/// use tracing::Level;
48///
49/// let config = LogConfig {
50///     app_name: "my-service".to_string(),
51///     log_level: Level::DEBUG,
52///     ..Default::default()
53/// };
54///
55/// init_logging(&config);
56/// ```
57pub fn init_logging(config: &LogConfig) {
58    INIT.call_once(|| {
59        let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
60            EnvFilter::new(format!(
61                "{},{app_name}={level}",
62                std::env::var("RUST_LOG").unwrap_or_default(),
63                app_name = config.app_name,
64                level = config.log_level
65            ))
66        });
67
68        let registry = registry().with(filter);
69
70        match (&config.log_dir, config.log_to_stdout) {
71            // Log to both file and stdout
72            (Some(log_dir), true) => {
73                let file_appender = rolling::daily(log_dir, format!("{}.log", config.app_name));
74                let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
75                // The guard owns the background writer. Dropping it here (as before
76                // 1.3.0) shut the writer down, so nothing reached the log file.
77                let _ = FILE_WRITER_GUARD.set(guard);
78
79                if config.json_format {
80                    let file_layer = fmt::layer()
81                        .json()
82                        .with_writer(non_blocking)
83                        .with_span_events(FmtSpan::CLOSE);
84
85                    let stdout_layer = fmt::layer().with_writer(std::io::stdout).with_ansi(true);
86
87                    registry.with(file_layer).with(stdout_layer).init();
88                } else {
89                    let file_layer = fmt::layer()
90                        .with_writer(non_blocking)
91                        .with_span_events(FmtSpan::CLOSE);
92
93                    let stdout_layer = fmt::layer().with_writer(std::io::stdout).with_ansi(true);
94
95                    registry.with(file_layer).with(stdout_layer).init();
96                }
97            }
98
99            // Log only to file
100            (Some(log_dir), false) => {
101                let file_appender = rolling::daily(log_dir, format!("{}.log", config.app_name));
102                let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
103                // The guard owns the background writer. Dropping it here (as before
104                // 1.3.0) shut the writer down, so nothing reached the log file.
105                let _ = FILE_WRITER_GUARD.set(guard);
106
107                if config.json_format {
108                    let file_layer = fmt::layer()
109                        .json()
110                        .with_writer(non_blocking)
111                        .with_span_events(FmtSpan::CLOSE);
112
113                    registry.with(file_layer).init();
114                } else {
115                    let file_layer = fmt::layer()
116                        .with_writer(non_blocking)
117                        .with_span_events(FmtSpan::CLOSE);
118
119                    registry.with(file_layer).init();
120                }
121            }
122
123            // Log only to stdout
124            (None, true) => {
125                if config.json_format {
126                    let stdout_layer = fmt::layer()
127                        .json()
128                        .with_writer(std::io::stdout)
129                        .with_span_events(FmtSpan::CLOSE);
130
131                    registry.with(stdout_layer).init();
132                } else {
133                    let stdout_layer = fmt::layer()
134                        .with_writer(std::io::stdout)
135                        .with_ansi(true)
136                        .with_span_events(FmtSpan::CLOSE);
137
138                    registry.with(stdout_layer).init();
139                }
140            }
141
142            // No logging output configured, default to stdout
143            (None, false) => {
144                let stdout_layer = fmt::layer().with_writer(std::io::stdout).with_ansi(true);
145
146                registry.with(stdout_layer).init();
147
148                tracing::warn!("No log output configured, defaulting to stdout");
149            }
150        }
151
152        tracing::info!("Logging initialized at {} level", config.log_level);
153    });
154}
155
156/// Setup default logging configuration for quick startup
157pub fn setup_default_logging() {
158    init_logging(&LogConfig::default());
159}