kumo_server_common/
diagnostic_logging.rs

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