zlayer_observability/
logging.rs1use std::io;
4use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
5use tracing_subscriber::{
6 fmt::{self, format::FmtSpan, writer::BoxMakeWriter},
7 layer::SubscriberExt,
8 util::SubscriberInitExt,
9 EnvFilter, Layer, Registry,
10};
11
12use crate::config::{FileLoggingConfig, LogFormat, LoggingConfig, RotationStrategy};
13use crate::error::Result;
14
15type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync>;
18
19pub struct LogGuard {
21 #[allow(dead_code)]
23 guard: Option<WorkerGuard>,
24}
25
26impl LogGuard {
27 fn new(guard: Option<WorkerGuard>) -> Self {
28 Self { guard }
29 }
30
31 #[must_use]
35 pub(crate) fn noop() -> Self {
36 Self { guard: None }
37 }
38}
39
40pub fn init_logging(config: &LoggingConfig) -> Result<LogGuard> {
51 init_logging_inner(config)
52}
53
54pub(crate) fn init_logging_inner(config: &LoggingConfig) -> Result<LogGuard> {
72 let (file_writer, guard) = if let Some(file_config) = &config.file {
75 let (writer, guard) = create_file_writer(file_config)?;
76 (Some(writer), Some(guard))
77 } else {
78 (None, None)
79 };
80
81 let mut layers: Vec<BoxedLayer> = Vec::new();
82 layers.push(build_console_layer(config, file_writer.is_some()));
83 if let Some(writer) = file_writer {
84 layers.push(build_file_layer(config, writer));
85 }
86
87 tracing_subscriber::registry().with(layers).init();
88
89 Ok(LogGuard::new(guard))
90}
91
92fn build_console_layer(config: &LoggingConfig, to_stdout: bool) -> BoxedLayer {
104 let writer: BoxMakeWriter = if to_stdout {
105 BoxMakeWriter::new(io::stdout)
106 } else {
107 BoxMakeWriter::new(io::stderr)
108 };
109
110 let base = fmt::layer()
111 .with_writer(writer)
112 .with_target(config.include_target)
113 .with_file(config.include_location)
114 .with_line_number(config.include_location)
115 .with_span_events(FmtSpan::CLOSE);
116
117 let filter = make_filter(config);
118 match config.format {
119 LogFormat::Pretty => base.pretty().with_filter(filter).boxed(),
120 LogFormat::Json => base.json().with_filter(filter).boxed(),
121 LogFormat::Compact => base.compact().with_filter(filter).boxed(),
122 }
123}
124
125fn build_file_layer(config: &LoggingConfig, writer: NonBlocking) -> BoxedLayer {
127 fmt::layer()
128 .with_writer(writer)
129 .with_target(config.include_target)
130 .with_file(config.include_location)
131 .with_line_number(config.include_location)
132 .with_span_events(FmtSpan::CLOSE)
133 .with_ansi(false)
134 .json()
135 .with_filter(make_filter(config))
136 .boxed()
137}
138
139fn make_filter(config: &LoggingConfig) -> EnvFilter {
143 EnvFilter::try_from_default_env().unwrap_or_else(|_| {
144 config.filter_directives.as_ref().map_or_else(
145 || EnvFilter::new(level_to_string(config.level)),
146 EnvFilter::new,
147 )
148 })
149}
150
151fn level_to_string(level: crate::config::LogLevel) -> String {
152 match level {
153 crate::config::LogLevel::Trace => "trace",
154 crate::config::LogLevel::Debug => "debug",
155 crate::config::LogLevel::Info => "info",
156 crate::config::LogLevel::Warn => "warn",
157 crate::config::LogLevel::Error => "error",
158 }
159 .to_string()
160}
161
162#[allow(clippy::unnecessary_wraps)]
163fn create_file_writer(
164 config: &FileLoggingConfig,
165) -> Result<(tracing_appender::non_blocking::NonBlocking, WorkerGuard)> {
166 if let Some(max) = config.max_files {
168 cleanup_rotated_files(&config.directory, &config.prefix, max);
169 }
170
171 let file_appender = match config.rotation {
172 RotationStrategy::Daily => {
173 tracing_appender::rolling::daily(&config.directory, &config.prefix)
174 }
175 RotationStrategy::Hourly => {
176 tracing_appender::rolling::hourly(&config.directory, &config.prefix)
177 }
178 RotationStrategy::Never => {
179 tracing_appender::rolling::never(&config.directory, &config.prefix)
180 }
181 };
182
183 Ok(tracing_appender::non_blocking(file_appender))
184}
185
186fn cleanup_rotated_files(directory: &std::path::Path, prefix: &str, max_files: usize) {
192 let Ok(entries) = std::fs::read_dir(directory) else {
193 return;
194 };
195
196 let dot_prefix = format!("{prefix}.");
197 let mut files: Vec<std::path::PathBuf> = entries
198 .filter_map(std::result::Result::ok)
199 .map(|e| e.path())
200 .filter(|p| {
201 p.file_name()
202 .and_then(|n| n.to_str())
203 .is_some_and(|n| n.starts_with(&dot_prefix))
204 })
205 .collect();
206
207 if files.len() <= max_files {
208 return;
209 }
210
211 files.sort();
213
214 let to_remove = files.len() - max_files;
215 for path in files.into_iter().take(to_remove) {
216 let _ = std::fs::remove_file(&path);
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_log_level_conversion() {
226 assert_eq!(level_to_string(crate::config::LogLevel::Info), "info");
227 assert_eq!(level_to_string(crate::config::LogLevel::Debug), "debug");
228 assert_eq!(level_to_string(crate::config::LogLevel::Trace), "trace");
229 assert_eq!(level_to_string(crate::config::LogLevel::Warn), "warn");
230 assert_eq!(level_to_string(crate::config::LogLevel::Error), "error");
231 }
232
233 #[test]
234 fn test_log_guard_creation() {
235 let guard = LogGuard::new(None);
236 assert!(guard.guard.is_none());
237 }
238}