use std::collections::HashMap;
use tracing::warn;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{
EnvFilter, Layer, Registry, fmt,
layer::{Layered, SubscriberExt},
};
use super::{config::*, writer};
#[derive(Debug)]
pub(crate) struct TracingHouseKeeper {
#[allow(dead_code)]
guards: Vec<WorkerGuard>,
}
type BoxedLayer = Box<dyn Layer<tracing_subscriber::Registry> + Send + Sync + 'static>;
pub(crate) type Subscriber = Layered<Vec<BoxedLayer>, Registry>;
#[derive(Default)]
pub struct TracingBuilder {
use_config: bool,
layers: HashMap<String, BoxedLayer>,
}
impl TracingBuilder {
pub fn new() -> Self {
Self {
use_config: true,
layers: HashMap::new(),
}
}
pub fn add_layer<N: Into<String>, L>(&mut self, name: N, layer: L) -> &mut Self
where
L: Layer<Registry> + Send + Sync + 'static,
{
let name = name.into();
self.layers.insert(name.clone(), Box::new(layer));
self
}
pub fn with_config(&mut self, use_config: bool) -> &mut Self {
self.use_config = use_config;
self
}
pub(crate) fn build(&mut self, config: &TracingConfig) -> (Subscriber, TracingHouseKeeper) {
let mut layers: Vec<BoxedLayer> = self.layers.drain().map(|(_, v)| v).collect();
let mut guards: Vec<WorkerGuard> = Vec::new();
if self.use_config {
let rust_log = get_rust_log_env();
let selected_layers = get_selected_layers(config);
for target in &selected_layers {
if let Some(config_layer) = config.layers.get(target) {
let (layer, guard) = create_layer(config, config_layer, rust_log.as_deref());
layers.push(layer);
guards.push(guard);
} else {
warn!(
"Tracing target '{}' is not configured in [tracing.layers]",
target
);
}
}
}
let subscriber: Subscriber = Registry::default().with(layers);
(subscriber, TracingHouseKeeper { guards })
}
}
fn get_rust_log_env() -> Option<String> {
std::env::var("RUST_LOG").ok()
}
fn resolve_filter_string(
layer_filter: Option<&str>,
rust_log_env: Option<&str>,
config_filter: &str,
) -> String {
if let Some(filter) = layer_filter {
return filter.to_string();
}
if let Some(env_filter) = rust_log_env {
return env_filter.to_string();
}
if !config_filter.is_empty() {
return config_filter.to_string();
}
"info".to_string()
}
fn build_env_filter(filter_str: &str) -> EnvFilter {
EnvFilter::try_new(filter_str).unwrap_or_else(|e| {
warn!(
"Invalid filter string '{}': {}. Falling back to 'info'",
filter_str, e
);
EnvFilter::new("info")
})
}
fn get_selected_layers(config: &TracingConfig) -> Vec<String> {
if let Ok(lxy_log) = std::env::var("LXY_LOG") {
return lxy_log
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
config.targets.clone()
}
fn create_layer(
config: &TracingConfig,
layer_config: &TracingLayer,
rust_log: Option<&str>,
) -> (BoxedLayer, WorkerGuard) {
let (writer, guard) = match &layer_config.kind {
TracingLayerType::Stdout(_) => writer::create_stdout_writer(),
TracingLayerType::File(opts) => writer::create_rolling_file_writer(opts),
};
let filter_str = resolve_filter_string(layer_config.filter.as_deref(), rust_log, &config.filter);
let env_filter = build_env_filter(&filter_str);
let format = layer_config
.format
.clone()
.unwrap_or_else(|| config.format.clone());
let mut layer = fmt::layer().with_writer(writer).with_target(true);
if let TracingLayerType::Stdout(opts) = &layer_config.kind {
layer = layer.with_ansi(opts.color);
}
let layer: BoxedLayer = match format {
LogFormat::Json => Box::new(layer.json().with_filter(env_filter)),
LogFormat::Plaintext => Box::new(layer.with_filter(env_filter)),
};
(layer, guard)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_config() -> TracingConfig {
toml::from_str(
r#"
filter = "info"
format = "plaintext"
layers = [
{ name = "test_stdout", type = "stdout" }
]
use = ["test_stdout"]
"#,
)
.unwrap()
}
#[test]
fn test_builder_with_config_enabled() {
let config = create_test_config();
let mut builder = TracingBuilder::new();
builder.with_config(true);
let (subscriber, housekeeper) = builder.build(&config);
let inner = subscriber.downcast_ref::<Vec<BoxedLayer>>().unwrap();
assert_eq!(housekeeper.guards.len(), 1);
assert_eq!(inner.len(), 1);
}
#[test]
fn test_builder_with_config_disabled() {
let config = create_test_config();
let mut builder = TracingBuilder::new();
builder.with_config(false);
let (subscriber, housekeeper) = builder.build(&config);
let inner = subscriber.downcast_ref::<Vec<BoxedLayer>>().unwrap();
assert_eq!(housekeeper.guards.len(), 0);
assert_eq!(inner.len(), 0);
}
#[test]
fn test_builder_add_custom_layer() {
let config = TracingConfig::default();
let mut builder = TracingBuilder::new();
builder
.with_config(false)
.add_layer("custom1", fmt::layer().with_filter(EnvFilter::new("info")))
.add_layer("custom2", fmt::layer().with_filter(EnvFilter::new("warn")));
let (subscriber, _) = builder.build(&config);
let inner = subscriber.downcast_ref::<Vec<BoxedLayer>>().unwrap();
assert_eq!(inner.len(), 2);
}
#[test]
fn test_builder_config_with_custom_layer() {
let config = create_test_config();
let mut builder = TracingBuilder::new();
builder.add_layer(
"custom_stdout",
fmt::layer().with_filter(EnvFilter::new("debug")),
);
let (subscriber, housekeeper) = builder.build(&config);
let inner = subscriber.downcast_ref::<Vec<BoxedLayer>>().unwrap();
assert_eq!(housekeeper.guards.len(), 1);
assert_eq!(inner.len(), 2);
}
}