use anyhow::Context;
use clap::ValueEnum;
use metrics_prometheus::recorder::Layer as _;
use std::path::PathBuf;
use std::sync::OnceLock;
use tracing_subscriber::fmt::writer::BoxMakeWriter;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter, Layer};
static TRACING_FILTER_RELOAD_HANDLE: OnceLock<
Box<dyn Fn(&str) -> anyhow::Result<()> + Send + Sync>,
> = OnceLock::new();
pub fn set_diagnostic_log_filter(new_filter: &str) -> anyhow::Result<()> {
let func = TRACING_FILTER_RELOAD_HANDLE
.get()
.ok_or_else(|| anyhow::anyhow!("unable to retrieve filter reload handle"))?;
(func)(new_filter)
}
#[derive(Debug, Clone, Copy, ValueEnum)]
#[clap(rename_all = "kebab_case")]
pub enum DiagnosticFormat {
Pretty,
Full,
Compact,
Json,
}
pub struct LoggingConfig<'a> {
pub log_dir: Option<PathBuf>,
pub filter_env_var: &'a str,
pub default_filter: &'a str,
pub diag_format: DiagnosticFormat,
}
impl<'a> LoggingConfig<'a> {
pub fn init(&self) -> anyhow::Result<()> {
let (non_blocking, _non_blocking_flusher);
let log_writer = if let Some(log_dir) = &self.log_dir {
let file_appender = tracing_appender::rolling::hourly(log_dir, "log");
(non_blocking, _non_blocking_flusher) = tracing_appender::non_blocking(file_appender);
BoxMakeWriter::new(non_blocking)
} else {
BoxMakeWriter::new(std::io::stderr)
};
let layer = fmt::layer().with_thread_names(true).with_writer(log_writer);
let layer = match self.diag_format {
DiagnosticFormat::Pretty => layer.pretty().boxed(),
DiagnosticFormat::Full => layer.boxed(),
DiagnosticFormat::Compact => layer.compact().boxed(),
DiagnosticFormat::Json => layer.json().boxed(),
};
let env_filter = EnvFilter::try_new(
std::env::var(self.filter_env_var)
.as_deref()
.unwrap_or(self.default_filter),
)?;
let (env_filter, reload_handle) = tracing_subscriber::reload::Layer::new(env_filter);
tracing_subscriber::registry()
.with(layer.with_filter(env_filter))
.with(metrics_tracing_context::MetricsLayer::new())
.init();
TRACING_FILTER_RELOAD_HANDLE
.set(Box::new(move |new_filter: &str| {
let f = EnvFilter::try_new(new_filter)
.with_context(|| format!("parsing log filter '{new_filter}'"))?;
Ok(reload_handle.reload(f).context("applying new log filter")?)
}))
.map_err(|_| anyhow::anyhow!("failed to assign reloadable logging filter"))?;
metrics::set_global_recorder(
metrics_tracing_context::TracingContextLayer::all()
.layer(metrics_prometheus::Recorder::builder().build()),
)?;
Ok(())
}
}