kumo_server_common/
diagnostic_logging.rs

1use anyhow::Context;
2use clap::ValueEnum;
3use metrics_prometheus::recorder::Layer as _;
4use std::path::PathBuf;
5use std::sync::OnceLock;
6use tracing_subscriber::fmt::writer::BoxMakeWriter;
7use tracing_subscriber::prelude::*;
8use tracing_subscriber::{fmt, EnvFilter, Layer};
9
10// Why in the heck is this a function and not simply the reload handle itself?
11// The reason is because the tracing_subscriber crate makes heavy use of composed
12// generic types and with the configuration we have chosen, some of the layers have
13// `impl Layer` types that cannot be named here.
14// <https://en.wiktionary.org/wiki/Voldemort_type>
15//
16// Even if it could be named, writing out its type here would make your eyes bleed.
17// Even the rust compiler doesn't want to print the name, instead writing it out
18// to a separate debugging file in its diagnostics!
19//
20// The approach taken is to stash a closure into this, and the closure capture
21// the reload handle and operates upon it.
22//
23// This way we don't need to name the type, and won't need to struggle with re-naming
24// it if we change the layering of the log subscriber.
25static TRACING_FILTER_RELOAD_HANDLE: OnceLock<
26    Box<dyn Fn(&str) -> anyhow::Result<()> + Send + Sync>,
27> = OnceLock::new();
28
29pub fn set_diagnostic_log_filter(new_filter: &str) -> anyhow::Result<()> {
30    let func = TRACING_FILTER_RELOAD_HANDLE
31        .get()
32        .ok_or_else(|| anyhow::anyhow!("unable to retrieve filter reload handle"))?;
33    (func)(new_filter)
34}
35
36#[derive(Debug, Clone, Copy, ValueEnum)]
37#[clap(rename_all = "kebab_case")]
38pub enum DiagnosticFormat {
39    Pretty,
40    Full,
41    Compact,
42    Json,
43}
44
45pub struct LoggingConfig<'a> {
46    pub log_dir: Option<PathBuf>,
47    pub filter_env_var: &'a str,
48    pub default_filter: &'a str,
49    pub diag_format: DiagnosticFormat,
50}
51
52impl LoggingConfig<'_> {
53    pub fn init(&self) -> anyhow::Result<()> {
54        let (non_blocking, _non_blocking_flusher);
55        let log_writer = if let Some(log_dir) = &self.log_dir {
56            let file_appender = tracing_appender::rolling::hourly(log_dir, "log");
57            (non_blocking, _non_blocking_flusher) = tracing_appender::non_blocking(file_appender);
58            BoxMakeWriter::new(non_blocking)
59        } else {
60            BoxMakeWriter::new(std::io::stderr)
61        };
62
63        let layer = fmt::layer().with_thread_names(true).with_writer(log_writer);
64        let layer = match self.diag_format {
65            DiagnosticFormat::Pretty => layer.pretty().boxed(),
66            DiagnosticFormat::Full => layer.boxed(),
67            DiagnosticFormat::Compact => layer.compact().boxed(),
68            DiagnosticFormat::Json => layer.json().boxed(),
69        };
70
71        let env_filter = EnvFilter::try_new(
72            std::env::var(self.filter_env_var)
73                .as_deref()
74                .unwrap_or(self.default_filter),
75        )?;
76        let (env_filter, reload_handle) = tracing_subscriber::reload::Layer::new(env_filter);
77        tracing_subscriber::registry()
78            .with(layer.with_filter(env_filter))
79            .with(metrics_tracing_context::MetricsLayer::new())
80            .init();
81
82        TRACING_FILTER_RELOAD_HANDLE
83            .set(Box::new(move |new_filter: &str| {
84                let f = EnvFilter::try_new(new_filter)
85                    .with_context(|| format!("parsing log filter '{new_filter}'"))?;
86                reload_handle.reload(f).context("applying new log filter")
87            }))
88            .map_err(|_| anyhow::anyhow!("failed to assign reloadable logging filter"))?;
89
90        metrics::set_global_recorder(
91            metrics_tracing_context::TracingContextLayer::all()
92                .layer(metrics_prometheus::Recorder::builder().build()),
93        )?;
94        Ok(())
95    }
96}