Skip to main content

cuprated/
logging.rs

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