Skip to main content

cuprated/
logging.rs

1//! Logging
2//!
3//! `cuprated` log filtering settings and related functionality.
4use std::{
5    fmt::{Display, Formatter},
6    io::IsTerminal,
7    sync::OnceLock,
8};
9use tracing::{level_filters::LevelFilter, subscriber::Interest, Metadata};
10use tracing_appender::{
11    non_blocking::{NonBlocking, WorkerGuard},
12    rolling::Rotation,
13};
14use tracing_subscriber::{
15    filter::Filtered,
16    fmt::{
17        self,
18        format::{DefaultFields, Format},
19        Layer as FmtLayer,
20    },
21    layer::{Context, Filter, Layered, SubscriberExt},
22    reload::{Handle, Layer as ReloadLayer},
23    util::SubscriberInitExt,
24    Layer, Registry,
25};
26
27use cuprate_helper::fs::logs_path;
28
29use crate::config::Config;
30
31/// A [`OnceLock`] which holds the [`Handle`] to update the file logging output.
32///
33/// Initialized in [`init_logging`].
34static FILE_WRITER_FILTER_HANDLE: OnceLock<Handle<CupratedTracingFilter, Registry>> =
35    OnceLock::new();
36
37/// A [`OnceLock`] which holds the [`Handle`] to update the stdout logging output.
38///
39/// Initialized in [`init_logging`].
40#[expect(clippy::type_complexity)] // factoring out isn't going to help readability.
41static STDOUT_FILTER_HANDLE: OnceLock<
42    Handle<
43        CupratedTracingFilter,
44        Layered<
45            Filtered<
46                FmtLayer<Registry, DefaultFields, Format, NonBlocking>,
47                ReloadLayer<CupratedTracingFilter, Registry>,
48                Registry,
49            >,
50            Registry,
51            Registry,
52        >,
53    >,
54> = OnceLock::new();
55
56/// The [`Filter`] used to alter cuprated's log output.
57#[derive(Debug)]
58pub struct CupratedTracingFilter {
59    pub level: LevelFilter,
60}
61
62// Custom display behavior for command output.
63impl Display for CupratedTracingFilter {
64    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65        f.debug_struct("Filter")
66            .field("minimum_level", &self.level.to_string())
67            .finish()
68    }
69}
70
71impl<S> Filter<S> for CupratedTracingFilter {
72    fn enabled(&self, meta: &Metadata<'_>, cx: &Context<'_, S>) -> bool {
73        Filter::<S>::enabled(&self.level, meta, cx)
74    }
75
76    fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
77        Filter::<S>::callsite_enabled(&self.level, meta)
78    }
79
80    fn max_level_hint(&self) -> Option<LevelFilter> {
81        Some(self.level)
82    }
83}
84
85/// Initialize [`tracing`] for logging to stdout and to a file.
86pub fn init_logging(config: &Config) -> WorkerGuard {
87    // initialize the stdout filter, set `STDOUT_FILTER_HANDLE` and create the layer.
88    let (stdout_filter, stdout_handle) = ReloadLayer::new(CupratedTracingFilter {
89        level: config.tracing.stdout.level,
90    });
91
92    STDOUT_FILTER_HANDLE.set(stdout_handle).unwrap();
93
94    let stdout_layer = FmtLayer::default()
95        .with_target(false)
96        .with_ansi(ansi_enabled(&std::io::stdout()))
97        .with_filter(stdout_filter);
98
99    // create the tracing appender.
100    let appender_config = &config.tracing.file;
101    let (appender, guard) = tracing_appender::non_blocking(
102        tracing_appender::rolling::Builder::new()
103            .rotation(Rotation::DAILY)
104            .max_log_files(appender_config.max_log_files)
105            .build(logs_path(&config.fs.fast_data_directory, config.network()))
106            .unwrap(),
107    );
108
109    // initialize the appender filter, set `FILE_WRITER_FILTER_HANDLE` and create the layer.
110    let (appender_filter, appender_handle) = ReloadLayer::new(CupratedTracingFilter {
111        level: appender_config.level,
112    });
113    FILE_WRITER_FILTER_HANDLE.set(appender_handle).unwrap();
114
115    let appender_layer = fmt::layer()
116        .with_target(false)
117        .with_ansi(false)
118        .with_writer(appender)
119        .with_filter(appender_filter);
120
121    // initialize tracing with the 2 layers.
122    tracing_subscriber::registry()
123        .with(appender_layer)
124        .with(stdout_layer)
125        .init();
126
127    guard
128}
129
130/// Modify the stdout [`CupratedTracingFilter`].
131///
132/// Must only be called after [`init_logging`].
133pub fn modify_stdout_output(f: impl FnOnce(&mut CupratedTracingFilter)) {
134    STDOUT_FILTER_HANDLE.get().unwrap().modify(f).unwrap();
135}
136
137/// Modify the file appender [`CupratedTracingFilter`].
138///
139/// Must only be called after [`init_logging`].
140pub fn modify_file_output(f: impl FnOnce(&mut CupratedTracingFilter)) {
141    FILE_WRITER_FILTER_HANDLE.get().unwrap().modify(f).unwrap();
142}
143
144/// Prints some text using [`eprintln`], with [`nu_ansi_term::Color::Red`] applied.
145pub fn eprintln_red(s: &str) {
146    if ansi_enabled(&std::io::stderr()) {
147        eprintln!("{}", nu_ansi_term::Color::Red.bold().paint(s));
148    } else {
149        eprintln!("{s}");
150    }
151}
152
153/// Whether to emit ANSI escape codes on `stream`.
154fn ansi_enabled(stream: &impl IsTerminal) -> bool {
155    if std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty()) {
156        return false;
157    }
158    stream.is_terminal()
159}