//! Logging infrastructure with non-blocking file I/O. use std::path::PathBuf; use tracing::Level; use tracing_appender::{ non_blocking::WorkerGuard, rolling::{RollingFileAppender, Rotation}, }; use tracing_log::LogTracer; use tracing_subscriber::{ fmt::time::ChronoUtc, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, }; use super::otel_trace::get_otel_layer; use crate::config::TraceConfig; const TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; const DEFAULT_LOG_TARGET: &str = "smg"; #[derive(Debug, Clone)] pub struct LoggingConfig { pub level: Level, pub json_format: bool, pub log_dir: Option, pub colorize: bool, pub log_file_name: String, pub log_targets: Option>, } impl Default for LoggingConfig { #[inline] fn default() -> Self { Self { level: Level::INFO, json_format: false, log_dir: None, colorize: true, log_file_name: "smg".to_string(), log_targets: Some(vec![DEFAULT_LOG_TARGET.to_string()]), } } } /// Guard that keeps the file appender thread alive. #[allow(dead_code)] pub struct LogGuard { _file_guard: Option, } #[inline] const fn level_to_str(level: Level) -> &'static str { match level { Level::TRACE => "trace", Level::DEBUG => "debug", Level::INFO => "info", Level::WARN => "warn", Level::ERROR => "error", } } #[inline] fn build_filter_string(targets: &[String], level_filter: &str) -> String { // Exact capacity: sum of target lengths + "=" and level per target + commas between let capacity = targets.iter().map(String::len).sum::() + targets.len() * (1 + level_filter.len()) + targets.len().saturating_sub(1); let mut filter_string = String::with_capacity(capacity); for (i, target) in targets.iter().enumerate() { if i > 0 { filter_string.push(','); } filter_string.push_str(target); filter_string.push('='); filter_string.push_str(level_filter); } filter_string } pub fn init_logging(config: LoggingConfig, otel_layer_config: Option) -> LogGuard { let _ = LogTracer::init(); let level_filter = level_to_str(config.level); let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { let filter_string = match &config.log_targets { Some(targets) if !targets.is_empty() => build_filter_string(targets, level_filter), _ => { let mut s = String::with_capacity(DEFAULT_LOG_TARGET.len() + 1 + level_filter.len()); s.push_str(DEFAULT_LOG_TARGET); s.push('='); s.push_str(level_filter); s } }; EnvFilter::new(filter_string) }); let mut layers = Vec::with_capacity(3); let stdout_layer = tracing_subscriber::fmt::layer() .with_ansi(config.colorize) .with_file(true) .with_line_number(true) .with_timer(ChronoUtc::new(TIME_FORMAT.to_string())); let stdout_layer = if config.json_format { stdout_layer.json().flatten_event(true).boxed() } else { stdout_layer.boxed() }; layers.push(stdout_layer); let mut file_guard = None; if let Some(log_dir) = &config.log_dir { let log_dir = PathBuf::from(log_dir); if !log_dir.exists() { if let Err(e) = std::fs::create_dir_all(&log_dir) { eprintln!("Failed to create log directory: {}", e); return LogGuard { _file_guard: None }; } } let file_appender = RollingFileAppender::new(Rotation::DAILY, log_dir, &config.log_file_name); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); file_guard = Some(guard); let file_layer = tracing_subscriber::fmt::layer() .with_ansi(false) .with_file(true) .with_line_number(true) .with_timer(ChronoUtc::new(TIME_FORMAT.to_string())) .with_writer(non_blocking); let file_layer = if config.json_format { file_layer.json().flatten_event(true).boxed() } else { file_layer.boxed() }; layers.push(file_layer); } if let Some(otel_layer_config) = &otel_layer_config { if otel_layer_config.enable_trace { match get_otel_layer() { Ok(otel_layer) => { layers.push(otel_layer); } Err(e) => { eprintln!("Failed to initialize OpenTelemetry: {}", e); } } } } let _ = tracing_subscriber::registry() .with(env_filter) .with(layers) .try_init(); LogGuard { _file_guard: file_guard, } }