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()
61 .with_writer(io::stdout)
62 .with_target(config.include_target)
63 .with_file(config.include_location)
64 .with_line_number(config.include_location)
65 .with_span_events(FmtSpan::CLOSE)
66 .pretty();
67
68 let file_layer = fmt::layer()
69 .with_writer(file_writer)
70 .with_target(config.include_target)
71 .with_file(config.include_location)
72 .with_line_number(config.include_location)
73 .with_span_events(FmtSpan::CLOSE)
74 .with_ansi(false)
75 .json();
76
77 tracing_subscriber::registry()
78 .with(env_filter)
79 .with(console_layer)
80 .with(file_layer)
81 .init();
82 }
83 (LogFormat::Pretty, None) => {
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 .pretty();
91
92 tracing_subscriber::registry()
93 .with(env_filter)
94 .with(console_layer)
95 .init();
96 }
97 (LogFormat::Json, Some(file_writer)) => {
98 let console_layer = fmt::layer()
99 .with_writer(io::stdout)
100 .with_target(config.include_target)
101 .with_file(config.include_location)
102 .with_line_number(config.include_location)
103 .with_span_events(FmtSpan::CLOSE)
104 .json();
105
106 let file_layer = fmt::layer()
107 .with_writer(file_writer)
108 .with_target(config.include_target)
109 .with_file(config.include_location)
110 .with_line_number(config.include_location)
111 .with_span_events(FmtSpan::CLOSE)
112 .with_ansi(false)
113 .json();
114
115 tracing_subscriber::registry()
116 .with(env_filter)
117 .with(console_layer)
118 .with(file_layer)
119 .init();
120 }
121 (LogFormat::Json, None) => {
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 .json();
129
130 tracing_subscriber::registry()
131 .with(env_filter)
132 .with(console_layer)
133 .init();
134 }
135 (LogFormat::Compact, Some(file_writer)) => {
136 let console_layer = fmt::layer()
137 .with_writer(io::stdout)
138 .with_target(config.include_target)
139 .with_file(config.include_location)
140 .with_line_number(config.include_location)
141 .with_span_events(FmtSpan::CLOSE)
142 .compact();
143
144 let file_layer = fmt::layer()
145 .with_writer(file_writer)
146 .with_target(config.include_target)
147 .with_file(config.include_location)
148 .with_line_number(config.include_location)
149 .with_span_events(FmtSpan::CLOSE)
150 .with_ansi(false)
151 .json();
152
153 tracing_subscriber::registry()
154 .with(env_filter)
155 .with(console_layer)
156 .with(file_layer)
157 .init();
158 }
159 (LogFormat::Compact, None) => {
160 let console_layer = fmt::layer()
161 .with_writer(io::stdout)
162 .with_target(config.include_target)
163 .with_file(config.include_location)
164 .with_line_number(config.include_location)
165 .with_span_events(FmtSpan::CLOSE)
166 .compact();
167
168 tracing_subscriber::registry()
169 .with(env_filter)
170 .with(console_layer)
171 .init();
172 }
173 }
174
175 Ok(LogGuard::new(guard))
176}
177
178fn level_to_string(level: crate::config::LogLevel) -> String {
179 match level {
180 crate::config::LogLevel::Trace => "trace",
181 crate::config::LogLevel::Debug => "debug",
182 crate::config::LogLevel::Info => "info",
183 crate::config::LogLevel::Warn => "warn",
184 crate::config::LogLevel::Error => "error",
185 }
186 .to_string()
187}
188
189#[allow(clippy::unnecessary_wraps)]
190fn create_file_writer(
191 config: &FileLoggingConfig,
192) -> Result<(tracing_appender::non_blocking::NonBlocking, WorkerGuard)> {
193 if let Some(max) = config.max_files {
195 cleanup_rotated_files(&config.directory, &config.prefix, max);
196 }
197
198 let file_appender = match config.rotation {
199 RotationStrategy::Daily => {
200 tracing_appender::rolling::daily(&config.directory, &config.prefix)
201 }
202 RotationStrategy::Hourly => {
203 tracing_appender::rolling::hourly(&config.directory, &config.prefix)
204 }
205 RotationStrategy::Never => {
206 tracing_appender::rolling::never(&config.directory, &config.prefix)
207 }
208 };
209
210 Ok(tracing_appender::non_blocking(file_appender))
211}
212
213fn cleanup_rotated_files(directory: &std::path::Path, prefix: &str, max_files: usize) {
219 let Ok(entries) = std::fs::read_dir(directory) else {
220 return;
221 };
222
223 let dot_prefix = format!("{prefix}.");
224 let mut files: Vec<std::path::PathBuf> = entries
225 .filter_map(std::result::Result::ok)
226 .map(|e| e.path())
227 .filter(|p| {
228 p.file_name()
229 .and_then(|n| n.to_str())
230 .is_some_and(|n| n.starts_with(&dot_prefix))
231 })
232 .collect();
233
234 if files.len() <= max_files {
235 return;
236 }
237
238 files.sort();
240
241 let to_remove = files.len() - max_files;
242 for path in files.into_iter().take(to_remove) {
243 let _ = std::fs::remove_file(&path);
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_log_level_conversion() {
253 assert_eq!(level_to_string(crate::config::LogLevel::Info), "info");
254 assert_eq!(level_to_string(crate::config::LogLevel::Debug), "debug");
255 assert_eq!(level_to_string(crate::config::LogLevel::Trace), "trace");
256 assert_eq!(level_to_string(crate::config::LogLevel::Warn), "warn");
257 assert_eq!(level_to_string(crate::config::LogLevel::Error), "error");
258 }
259
260 #[test]
261 fn test_log_guard_creation() {
262 let guard = LogGuard::new(None);
263 assert!(guard.guard.is_none());
264 }
265}