Skip to main content

zlayer_observability/
logging.rs

1//! Structured logging with JSON/pretty output and file rotation
2
3use std::io;
4use tracing_appender::non_blocking::WorkerGuard;
5use tracing_subscriber::{
6    fmt::{self, format::FmtSpan},
7    layer::SubscriberExt,
8    util::SubscriberInitExt,
9    EnvFilter,
10};
11
12use crate::config::{FileLoggingConfig, LogFormat, LoggingConfig, RotationStrategy};
13use crate::error::Result;
14
15/// Guard that must be held to keep the async file writer running
16pub struct LogGuard {
17    _guard: Option<WorkerGuard>,
18}
19
20impl LogGuard {
21    fn new(guard: Option<WorkerGuard>) -> Self {
22        Self { _guard: guard }
23    }
24}
25
26/// Initialize logging with the given configuration
27///
28/// Returns a guard that must be held for the lifetime of the application
29/// to ensure logs are flushed properly.
30pub fn init_logging(config: &LoggingConfig) -> Result<LogGuard> {
31    let env_filter = EnvFilter::try_from_default_env()
32        .unwrap_or_else(|_| EnvFilter::new(level_to_string(config.level)));
33
34    // Handle file logging setup
35    let (file_writer, guard) = if let Some(file_config) = &config.file {
36        let (writer, guard) = create_file_writer(file_config)?;
37        (Some(writer), Some(guard))
38    } else {
39        (None, None)
40    };
41
42    // Initialize based on format and file configuration
43    // We need separate branches because of tracing-subscriber's complex type system
44    match (config.format, file_writer) {
45        (LogFormat::Pretty, Some(file_writer)) => {
46            let console_layer = fmt::layer()
47                .with_writer(io::stdout)
48                .with_target(config.include_target)
49                .with_file(config.include_location)
50                .with_line_number(config.include_location)
51                .with_span_events(FmtSpan::CLOSE)
52                .pretty();
53
54            let file_layer = fmt::layer()
55                .with_writer(file_writer)
56                .with_target(config.include_target)
57                .with_file(config.include_location)
58                .with_line_number(config.include_location)
59                .with_span_events(FmtSpan::CLOSE)
60                .with_ansi(false)
61                .json();
62
63            tracing_subscriber::registry()
64                .with(env_filter)
65                .with(console_layer)
66                .with(file_layer)
67                .init();
68        }
69        (LogFormat::Pretty, None) => {
70            let console_layer = fmt::layer()
71                .with_writer(io::stdout)
72                .with_target(config.include_target)
73                .with_file(config.include_location)
74                .with_line_number(config.include_location)
75                .with_span_events(FmtSpan::CLOSE)
76                .pretty();
77
78            tracing_subscriber::registry()
79                .with(env_filter)
80                .with(console_layer)
81                .init();
82        }
83        (LogFormat::Json, Some(file_writer)) => {
84            let console_layer = fmt::layer()
85                .with_writer(io::stdout)
86                .with_target(config.include_target)
87                .with_file(config.include_location)
88                .with_line_number(config.include_location)
89                .with_span_events(FmtSpan::CLOSE)
90                .json();
91
92            let file_layer = fmt::layer()
93                .with_writer(file_writer)
94                .with_target(config.include_target)
95                .with_file(config.include_location)
96                .with_line_number(config.include_location)
97                .with_span_events(FmtSpan::CLOSE)
98                .with_ansi(false)
99                .json();
100
101            tracing_subscriber::registry()
102                .with(env_filter)
103                .with(console_layer)
104                .with(file_layer)
105                .init();
106        }
107        (LogFormat::Json, None) => {
108            let console_layer = fmt::layer()
109                .with_writer(io::stdout)
110                .with_target(config.include_target)
111                .with_file(config.include_location)
112                .with_line_number(config.include_location)
113                .with_span_events(FmtSpan::CLOSE)
114                .json();
115
116            tracing_subscriber::registry()
117                .with(env_filter)
118                .with(console_layer)
119                .init();
120        }
121        (LogFormat::Compact, Some(file_writer)) => {
122            let console_layer = fmt::layer()
123                .with_writer(io::stdout)
124                .with_target(config.include_target)
125                .with_file(config.include_location)
126                .with_line_number(config.include_location)
127                .with_span_events(FmtSpan::CLOSE)
128                .compact();
129
130            let file_layer = fmt::layer()
131                .with_writer(file_writer)
132                .with_target(config.include_target)
133                .with_file(config.include_location)
134                .with_line_number(config.include_location)
135                .with_span_events(FmtSpan::CLOSE)
136                .with_ansi(false)
137                .json();
138
139            tracing_subscriber::registry()
140                .with(env_filter)
141                .with(console_layer)
142                .with(file_layer)
143                .init();
144        }
145        (LogFormat::Compact, None) => {
146            let console_layer = fmt::layer()
147                .with_writer(io::stdout)
148                .with_target(config.include_target)
149                .with_file(config.include_location)
150                .with_line_number(config.include_location)
151                .with_span_events(FmtSpan::CLOSE)
152                .compact();
153
154            tracing_subscriber::registry()
155                .with(env_filter)
156                .with(console_layer)
157                .init();
158        }
159    }
160
161    Ok(LogGuard::new(guard))
162}
163
164fn level_to_string(level: crate::config::LogLevel) -> String {
165    match level {
166        crate::config::LogLevel::Trace => "trace",
167        crate::config::LogLevel::Debug => "debug",
168        crate::config::LogLevel::Info => "info",
169        crate::config::LogLevel::Warn => "warn",
170        crate::config::LogLevel::Error => "error",
171    }
172    .to_string()
173}
174
175fn create_file_writer(
176    config: &FileLoggingConfig,
177) -> Result<(tracing_appender::non_blocking::NonBlocking, WorkerGuard)> {
178    let file_appender = match config.rotation {
179        RotationStrategy::Daily => {
180            tracing_appender::rolling::daily(&config.directory, &config.prefix)
181        }
182        RotationStrategy::Hourly => {
183            tracing_appender::rolling::hourly(&config.directory, &config.prefix)
184        }
185        RotationStrategy::Never => {
186            tracing_appender::rolling::never(&config.directory, &config.prefix)
187        }
188    };
189
190    Ok(tracing_appender::non_blocking(file_appender))
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_log_level_conversion() {
199        assert_eq!(level_to_string(crate::config::LogLevel::Info), "info");
200        assert_eq!(level_to_string(crate::config::LogLevel::Debug), "debug");
201        assert_eq!(level_to_string(crate::config::LogLevel::Trace), "trace");
202        assert_eq!(level_to_string(crate::config::LogLevel::Warn), "warn");
203        assert_eq!(level_to_string(crate::config::LogLevel::Error), "error");
204    }
205
206    #[test]
207    fn test_log_guard_creation() {
208        let guard = LogGuard::new(None);
209        assert!(guard._guard.is_none());
210    }
211}