175 lines
4.9 KiB
Rust
175 lines
4.9 KiB
Rust
//! 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<String>,
|
|
pub colorize: bool,
|
|
pub log_file_name: String,
|
|
pub log_targets: Option<Vec<String>>,
|
|
}
|
|
|
|
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<WorkerGuard>,
|
|
}
|
|
|
|
#[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::<usize>()
|
|
+ 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<TraceConfig>) -> 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,
|
|
}
|
|
}
|