use std::io;
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_subscriber::{
fmt::{self, format::FmtSpan, writer::BoxMakeWriter},
layer::SubscriberExt,
util::SubscriberInitExt,
EnvFilter, Layer, Registry,
};
use crate::config::{FileLoggingConfig, LogFormat, LoggingConfig, RotationStrategy};
use crate::error::Result;
type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync>;
pub struct LogGuard {
#[allow(dead_code)]
guard: Option<WorkerGuard>,
}
impl LogGuard {
fn new(guard: Option<WorkerGuard>) -> Self {
Self { guard }
}
#[must_use]
pub(crate) fn noop() -> Self {
Self { guard: None }
}
}
pub fn init_logging(config: &LoggingConfig) -> Result<LogGuard> {
init_logging_inner(config)
}
pub(crate) fn init_logging_inner(config: &LoggingConfig) -> Result<LogGuard> {
let (file_writer, guard) = if let Some(file_config) = &config.file {
let (writer, guard) = create_file_writer(file_config)?;
(Some(writer), Some(guard))
} else {
(None, None)
};
let mut layers: Vec<BoxedLayer> = Vec::new();
layers.push(build_console_layer(config, file_writer.is_some()));
if let Some(writer) = file_writer {
layers.push(build_file_layer(config, writer));
}
tracing_subscriber::registry().with(layers).init();
Ok(LogGuard::new(guard))
}
fn build_console_layer(config: &LoggingConfig, to_stdout: bool) -> BoxedLayer {
let writer: BoxMakeWriter = if to_stdout {
BoxMakeWriter::new(io::stdout)
} else {
BoxMakeWriter::new(io::stderr)
};
let base = fmt::layer()
.with_writer(writer)
.with_target(config.include_target)
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_span_events(FmtSpan::CLOSE);
let filter = make_filter(config);
match config.format {
LogFormat::Pretty => base.pretty().with_filter(filter).boxed(),
LogFormat::Json => base.json().with_filter(filter).boxed(),
LogFormat::Compact => base.compact().with_filter(filter).boxed(),
}
}
fn build_file_layer(config: &LoggingConfig, writer: NonBlocking) -> BoxedLayer {
fmt::layer()
.with_writer(writer)
.with_target(config.include_target)
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_span_events(FmtSpan::CLOSE)
.with_ansi(false)
.json()
.with_filter(make_filter(config))
.boxed()
}
fn make_filter(config: &LoggingConfig) -> EnvFilter {
EnvFilter::try_from_default_env().unwrap_or_else(|_| {
config.filter_directives.as_ref().map_or_else(
|| EnvFilter::new(level_to_string(config.level)),
EnvFilter::new,
)
})
}
fn level_to_string(level: crate::config::LogLevel) -> String {
match level {
crate::config::LogLevel::Trace => "trace",
crate::config::LogLevel::Debug => "debug",
crate::config::LogLevel::Info => "info",
crate::config::LogLevel::Warn => "warn",
crate::config::LogLevel::Error => "error",
}
.to_string()
}
#[allow(clippy::unnecessary_wraps)]
fn create_file_writer(
config: &FileLoggingConfig,
) -> Result<(tracing_appender::non_blocking::NonBlocking, WorkerGuard)> {
if let Some(max) = config.max_files {
cleanup_rotated_files(&config.directory, &config.prefix, max);
}
let file_appender = match config.rotation {
RotationStrategy::Daily => {
tracing_appender::rolling::daily(&config.directory, &config.prefix)
}
RotationStrategy::Hourly => {
tracing_appender::rolling::hourly(&config.directory, &config.prefix)
}
RotationStrategy::Never => {
tracing_appender::rolling::never(&config.directory, &config.prefix)
}
};
Ok(tracing_appender::non_blocking(file_appender))
}
fn cleanup_rotated_files(directory: &std::path::Path, prefix: &str, max_files: usize) {
let Ok(entries) = std::fs::read_dir(directory) else {
return;
};
let dot_prefix = format!("{prefix}.");
let mut files: Vec<std::path::PathBuf> = entries
.filter_map(std::result::Result::ok)
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with(&dot_prefix))
})
.collect();
if files.len() <= max_files {
return;
}
files.sort();
let to_remove = files.len() - max_files;
for path in files.into_iter().take(to_remove) {
let _ = std::fs::remove_file(&path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_conversion() {
assert_eq!(level_to_string(crate::config::LogLevel::Info), "info");
assert_eq!(level_to_string(crate::config::LogLevel::Debug), "debug");
assert_eq!(level_to_string(crate::config::LogLevel::Trace), "trace");
assert_eq!(level_to_string(crate::config::LogLevel::Warn), "warn");
assert_eq!(level_to_string(crate::config::LogLevel::Error), "error");
}
#[test]
fn test_log_guard_creation() {
let guard = LogGuard::new(None);
assert!(guard.guard.is_none());
}
}