use std::path::Path;
use std::sync::OnceLock;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{
fmt::{self, format::FmtSpan, time::UtcTime},
layer::SubscriberExt,
reload,
util::SubscriberInitExt,
EnvFilter,
};
use super::config::{LogConfig, LogFormat, LogTarget};
use super::dynamic::LogLevelController;
use super::rotation::RotationStrategy;
use crate::error::Result;
static LOGGING_INITIALIZED: OnceLock<()> = OnceLock::new();
pub fn init_logging(config: &LogConfig) -> Result<()> {
if LOGGING_INITIALIZED.get().is_some() {
return Ok(());
}
config.validate()?;
let result = init_logging_internal(config);
if result.is_ok() {
LOGGING_INITIALIZED.get_or_init(|| ());
}
result
}
fn init_logging_internal(config: &LogConfig) -> Result<()> {
let filter = build_env_filter(config);
let span_events = build_span_events(config);
if config.dynamic_level {
init_with_dynamic_level(config, filter, span_events)
} else {
init_static(config, filter, span_events)
}
}
fn init_with_dynamic_level(
config: &LogConfig,
filter: EnvFilter,
span_events: FmtSpan,
) -> Result<()> {
let (filter_layer, reload_handle) = reload::Layer::new(filter);
match &config.target {
LogTarget::Stdout => {
init_stdout_with_reload(config, filter_layer, span_events)?;
}
LogTarget::Stderr => {
init_stderr_with_reload(config, filter_layer, span_events)?;
}
LogTarget::File {
directory,
filename_prefix,
rotation,
} => {
init_file_with_reload(
config,
filter_layer,
span_events,
directory,
filename_prefix,
rotation,
)?;
}
LogTarget::Multi(targets) => {
init_multi_with_reload(config, filter_layer, span_events, targets)?;
}
}
let controller = LogLevelController::new(reload_handle, config.level);
controller.register_global();
Ok(())
}
fn init_stdout_with_reload(
config: &LogConfig,
filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
span_events: FmtSpan,
) -> Result<()> {
let registry = tracing_subscriber::registry().with(filter_layer);
match config.format {
LogFormat::Json => {
let layer = fmt::layer()
.json()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_writer(std::io::stdout);
registry.with(layer).init();
}
LogFormat::Pretty => {
let layer = fmt::layer()
.pretty()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(config.ansi_colors)
.with_writer(std::io::stdout);
registry.with(layer).init();
}
LogFormat::Compact => {
let layer = fmt::layer()
.compact()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(config.ansi_colors)
.with_writer(std::io::stdout);
registry.with(layer).init();
}
LogFormat::Full => {
let layer = fmt::layer()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(config.ansi_colors)
.with_writer(std::io::stdout);
registry.with(layer).init();
}
}
Ok(())
}
fn init_stderr_with_reload(
config: &LogConfig,
filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
span_events: FmtSpan,
) -> Result<()> {
let registry = tracing_subscriber::registry().with(filter_layer);
let layer = fmt::layer()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(config.ansi_colors)
.with_writer(std::io::stderr);
registry.with(layer).init();
Ok(())
}
fn init_file_with_reload(
config: &LogConfig,
filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
span_events: FmtSpan,
directory: &Path,
filename_prefix: &str,
rotation_config: &super::rotation::RotationConfig,
) -> Result<()> {
let rotation = match rotation_config.strategy {
RotationStrategy::Daily => Rotation::DAILY,
RotationStrategy::Hourly => Rotation::HOURLY,
RotationStrategy::Minutely => Rotation::MINUTELY,
RotationStrategy::Never => Rotation::NEVER,
};
let file_appender = RollingFileAppender::new(rotation, directory, filename_prefix);
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
std::mem::forget(_guard);
let registry = tracing_subscriber::registry().with(filter_layer);
let layer = fmt::layer()
.json()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(false) .with_writer(non_blocking);
registry.with(layer).init();
Ok(())
}
fn init_multi_with_reload(
config: &LogConfig,
filter_layer: reload::Layer<EnvFilter, tracing_subscriber::Registry>,
span_events: FmtSpan,
targets: &[LogTarget],
) -> Result<()> {
let has_console = targets
.iter()
.any(|t| matches!(t, LogTarget::Stdout | LogTarget::Stderr));
let file_target = targets.iter().find_map(|t| {
if let LogTarget::File {
directory,
filename_prefix,
rotation,
} = t
{
Some((directory.clone(), filename_prefix.clone(), rotation.clone()))
} else {
None
}
});
let registry = tracing_subscriber::registry().with(filter_layer);
if has_console && file_target.is_some() {
let (directory, filename_prefix, rotation_config) = file_target.unwrap();
let rotation = match rotation_config.strategy {
RotationStrategy::Daily => Rotation::DAILY,
RotationStrategy::Hourly => Rotation::HOURLY,
RotationStrategy::Minutely => Rotation::MINUTELY,
RotationStrategy::Never => Rotation::NEVER,
};
let file_appender = RollingFileAppender::new(rotation, &directory, &filename_prefix);
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
std::mem::forget(_guard);
let console_layer = fmt::layer()
.pretty()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_span_events(span_events.clone())
.with_ansi(config.ansi_colors)
.with_writer(std::io::stdout);
let file_layer = fmt::layer()
.json()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_span_events(span_events)
.with_ansi(false)
.with_writer(non_blocking);
registry.with(console_layer).with(file_layer).init();
} else if has_console {
init_stdout_with_reload(
config,
reload::Layer::new(build_env_filter(config)).0,
span_events,
)?;
} else if let Some((directory, filename_prefix, rotation_config)) = file_target {
let reload_layer = reload::Layer::new(build_env_filter(config)).0;
init_file_with_reload(
config,
reload_layer,
span_events,
&directory,
&filename_prefix,
&rotation_config,
)?;
}
Ok(())
}
fn init_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
match &config.target {
LogTarget::Stdout => init_stdout_static(config, filter, span_events),
LogTarget::Stderr => init_stderr_static(config, filter, span_events),
LogTarget::File {
directory,
filename_prefix,
rotation,
} => init_file_static(
config,
filter,
span_events,
directory,
filename_prefix,
rotation,
),
LogTarget::Multi(_) => {
init_with_dynamic_level(config, filter, span_events)
}
}
}
fn init_stdout_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
let registry = tracing_subscriber::registry().with(filter);
match config.format {
LogFormat::Json => {
let layer = fmt::layer()
.json()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_writer(std::io::stdout);
registry.with(layer).init();
}
LogFormat::Pretty => {
let layer = fmt::layer()
.pretty()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(config.ansi_colors)
.with_writer(std::io::stdout);
registry.with(layer).init();
}
LogFormat::Compact => {
let layer = fmt::layer()
.compact()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(config.ansi_colors)
.with_writer(std::io::stdout);
registry.with(layer).init();
}
LogFormat::Full => {
let layer = fmt::layer()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(config.ansi_colors)
.with_writer(std::io::stdout);
registry.with(layer).init();
}
}
Ok(())
}
fn init_stderr_static(config: &LogConfig, filter: EnvFilter, span_events: FmtSpan) -> Result<()> {
let registry = tracing_subscriber::registry().with(filter);
let layer = fmt::layer()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(config.ansi_colors)
.with_writer(std::io::stderr);
registry.with(layer).init();
Ok(())
}
fn init_file_static(
config: &LogConfig,
filter: EnvFilter,
span_events: FmtSpan,
directory: &Path,
filename_prefix: &str,
rotation_config: &super::rotation::RotationConfig,
) -> Result<()> {
let rotation = match rotation_config.strategy {
RotationStrategy::Daily => Rotation::DAILY,
RotationStrategy::Hourly => Rotation::HOURLY,
RotationStrategy::Minutely => Rotation::MINUTELY,
RotationStrategy::Never => Rotation::NEVER,
};
let file_appender = RollingFileAppender::new(rotation, directory, filename_prefix);
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
std::mem::forget(_guard);
let registry = tracing_subscriber::registry().with(filter);
let layer = fmt::layer()
.json()
.with_timer(UtcTime::rfc_3339())
.with_file(config.include_location)
.with_line_number(config.include_location)
.with_target(config.include_target)
.with_thread_ids(config.include_thread_ids)
.with_thread_names(config.include_thread_names)
.with_span_events(span_events)
.with_ansi(false)
.with_writer(non_blocking);
registry.with(layer).init();
Ok(())
}
fn build_env_filter(config: &LogConfig) -> EnvFilter {
EnvFilter::try_from_default_env().unwrap_or_else(|_| {
let filter_str = config.build_filter_string();
EnvFilter::try_new(&filter_str)
.unwrap_or_else(|_| EnvFilter::new(config.level.as_filter_str()))
})
}
fn build_span_events(config: &LogConfig) -> FmtSpan {
if config.include_span_events {
FmtSpan::ENTER | FmtSpan::EXIT
} else {
FmtSpan::NONE
}
}
pub fn init_test_logging() {
let _ = init_logging(&LogConfig::test());
}
pub fn is_logging_initialized() -> bool {
LOGGING_INITIALIZED.get().is_some()
}
#[cfg(test)]
mod tests {
use super::super::config::LogLevel;
use super::*;
#[test]
fn test_build_env_filter() {
let config = LogConfig::builder().level(LogLevel::Debug).build();
let filter = build_env_filter(&config);
drop(filter);
}
#[test]
fn test_build_span_events() {
let config = LogConfig::builder().include_span_events(true).build();
let span_events = build_span_events(&config);
assert_ne!(span_events, FmtSpan::NONE);
let config = LogConfig::builder().include_span_events(false).build();
let span_events = build_span_events(&config);
assert_eq!(span_events, FmtSpan::NONE);
}
}