zlayer_observability/
logging.rs1use 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
15pub struct LogGuard {
17 #[allow(dead_code)]
19 guard: Option<WorkerGuard>,
20}
21
22impl LogGuard {
23 fn new(guard: Option<WorkerGuard>) -> Self {
24 Self { guard }
25 }
26}
27
28#[allow(clippy::too_many_lines)]
39pub fn init_logging(config: &LoggingConfig) -> Result<LogGuard> {
40 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
41 if let Some(ref directives) = config.filter_directives {
42 EnvFilter::new(directives)
43 } else {
44 EnvFilter::new(level_to_string(config.level))
45 }
46 });
47
48 let (file_writer, guard) = if let Some(file_config) = &config.file {
50 let (writer, guard) = create_file_writer(file_config)?;
51 (Some(writer), Some(guard))
52 } else {
53 (None, None)
54 };
55
56 match (config.format, file_writer) {
59 (LogFormat::Pretty, Some(file_writer)) => {
60 let console_layer = fmt::layer()
70 .with_writer(io::stdout)
71 .with_target(config.include_target)
72 .with_file(config.include_location)
73 .with_line_number(config.include_location)
74 .with_span_events(FmtSpan::CLOSE)
75 .pretty();
76
77 let file_layer = fmt::layer()
78 .with_writer(file_writer)
79 .with_target(config.include_target)
80 .with_file(config.include_location)
81 .with_line_number(config.include_location)
82 .with_span_events(FmtSpan::CLOSE)
83 .with_ansi(false)
84 .json();
85
86 tracing_subscriber::registry()
87 .with(env_filter)
88 .with(console_layer)
89 .with(file_layer)
90 .init();
91 }
92 (LogFormat::Pretty, None) => {
93 let console_layer = fmt::layer()
94 .with_writer(io::stderr)
95 .with_target(config.include_target)
96 .with_file(config.include_location)
97 .with_line_number(config.include_location)
98 .with_span_events(FmtSpan::CLOSE)
99 .pretty();
100
101 tracing_subscriber::registry()
102 .with(env_filter)
103 .with(console_layer)
104 .init();
105 }
106 (LogFormat::Json, Some(file_writer)) => {
107 let console_layer = fmt::layer()
110 .with_writer(io::stdout)
111 .with_target(config.include_target)
112 .with_file(config.include_location)
113 .with_line_number(config.include_location)
114 .with_span_events(FmtSpan::CLOSE)
115 .json();
116
117 let file_layer = fmt::layer()
118 .with_writer(file_writer)
119 .with_target(config.include_target)
120 .with_file(config.include_location)
121 .with_line_number(config.include_location)
122 .with_span_events(FmtSpan::CLOSE)
123 .with_ansi(false)
124 .json();
125
126 tracing_subscriber::registry()
127 .with(env_filter)
128 .with(console_layer)
129 .with(file_layer)
130 .init();
131 }
132 (LogFormat::Json, None) => {
133 let console_layer = fmt::layer()
134 .with_writer(io::stderr)
135 .with_target(config.include_target)
136 .with_file(config.include_location)
137 .with_line_number(config.include_location)
138 .with_span_events(FmtSpan::CLOSE)
139 .json();
140
141 tracing_subscriber::registry()
142 .with(env_filter)
143 .with(console_layer)
144 .init();
145 }
146 (LogFormat::Compact, Some(file_writer)) => {
147 let console_layer = fmt::layer()
150 .with_writer(io::stdout)
151 .with_target(config.include_target)
152 .with_file(config.include_location)
153 .with_line_number(config.include_location)
154 .with_span_events(FmtSpan::CLOSE)
155 .compact();
156
157 let file_layer = fmt::layer()
158 .with_writer(file_writer)
159 .with_target(config.include_target)
160 .with_file(config.include_location)
161 .with_line_number(config.include_location)
162 .with_span_events(FmtSpan::CLOSE)
163 .with_ansi(false)
164 .json();
165
166 tracing_subscriber::registry()
167 .with(env_filter)
168 .with(console_layer)
169 .with(file_layer)
170 .init();
171 }
172 (LogFormat::Compact, None) => {
173 let console_layer = fmt::layer()
174 .with_writer(io::stderr)
175 .with_target(config.include_target)
176 .with_file(config.include_location)
177 .with_line_number(config.include_location)
178 .with_span_events(FmtSpan::CLOSE)
179 .compact();
180
181 tracing_subscriber::registry()
182 .with(env_filter)
183 .with(console_layer)
184 .init();
185 }
186 }
187
188 Ok(LogGuard::new(guard))
189}
190
191fn level_to_string(level: crate::config::LogLevel) -> String {
192 match level {
193 crate::config::LogLevel::Trace => "trace",
194 crate::config::LogLevel::Debug => "debug",
195 crate::config::LogLevel::Info => "info",
196 crate::config::LogLevel::Warn => "warn",
197 crate::config::LogLevel::Error => "error",
198 }
199 .to_string()
200}
201
202#[allow(clippy::unnecessary_wraps)]
203fn create_file_writer(
204 config: &FileLoggingConfig,
205) -> Result<(tracing_appender::non_blocking::NonBlocking, WorkerGuard)> {
206 if let Some(max) = config.max_files {
208 cleanup_rotated_files(&config.directory, &config.prefix, max);
209 }
210
211 let file_appender = match config.rotation {
212 RotationStrategy::Daily => {
213 tracing_appender::rolling::daily(&config.directory, &config.prefix)
214 }
215 RotationStrategy::Hourly => {
216 tracing_appender::rolling::hourly(&config.directory, &config.prefix)
217 }
218 RotationStrategy::Never => {
219 tracing_appender::rolling::never(&config.directory, &config.prefix)
220 }
221 };
222
223 Ok(tracing_appender::non_blocking(file_appender))
224}
225
226fn cleanup_rotated_files(directory: &std::path::Path, prefix: &str, max_files: usize) {
232 let Ok(entries) = std::fs::read_dir(directory) else {
233 return;
234 };
235
236 let dot_prefix = format!("{prefix}.");
237 let mut files: Vec<std::path::PathBuf> = entries
238 .filter_map(std::result::Result::ok)
239 .map(|e| e.path())
240 .filter(|p| {
241 p.file_name()
242 .and_then(|n| n.to_str())
243 .is_some_and(|n| n.starts_with(&dot_prefix))
244 })
245 .collect();
246
247 if files.len() <= max_files {
248 return;
249 }
250
251 files.sort();
253
254 let to_remove = files.len() - max_files;
255 for path in files.into_iter().take(to_remove) {
256 let _ = std::fs::remove_file(&path);
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_log_level_conversion() {
266 assert_eq!(level_to_string(crate::config::LogLevel::Info), "info");
267 assert_eq!(level_to_string(crate::config::LogLevel::Debug), "debug");
268 assert_eq!(level_to_string(crate::config::LogLevel::Trace), "trace");
269 assert_eq!(level_to_string(crate::config::LogLevel::Warn), "warn");
270 assert_eq!(level_to_string(crate::config::LogLevel::Error), "error");
271 }
272
273 #[test]
274 fn test_log_guard_creation() {
275 let guard = LogGuard::new(None);
276 assert!(guard.guard.is_none());
277 }
278}