Skip to main content

reth_tracing/
log_handle.rs

1//! Global log handle for runtime filter changes.
2//!
3//! Provides a single global [`LogFilterHandle`] that collects reload handles from all
4//! reloadable layers (stdout, file, etc.). `set_log_verbosity` and `set_log_vmodule`
5//! update every registered layer in one shot.
6
7use crate::layers::DEFAULT_ENV_FILTER_DIRECTIVES;
8use tracing::level_filters::LevelFilter;
9use tracing_subscriber::{reload, EnvFilter, Registry};
10
11/// Type alias for a single layer's reload handle.
12pub type LogFilterReloadHandle = reload::Handle<EnvFilter, Registry>;
13
14/// Collects reload handles so all layers can be updated together.
15#[derive(Debug)]
16pub struct LogFilterHandle {
17    /// Each layer's handle and the filter it started with, so an empty pattern restores it.
18    handles: Vec<(LogFilterReloadHandle, String)>,
19}
20
21impl LogFilterHandle {
22    /// Creates a new, empty handle collection.
23    const fn new() -> Self {
24        Self { handles: Vec::new() }
25    }
26
27    /// Adds a reload handle for a layer.
28    fn push(&mut self, handle: LogFilterReloadHandle) {
29        let initial = handle.with_current(|filter| filter.to_string()).unwrap_or_default();
30        self.handles.push((handle, initial));
31    }
32
33    /// Returns `true` if at least one handle is registered.
34    const fn is_available(&self) -> bool {
35        !self.handles.is_empty()
36    }
37
38    /// Reloads every registered layer with a fresh filter built by `make_filter`, which receives
39    /// that layer's initial filter.
40    fn reload_all(
41        &self,
42        make_filter: impl Fn(&str) -> Result<EnvFilter, String>,
43    ) -> Result<(), String> {
44        for (handle, initial) in &self.handles {
45            let filter = make_filter(initial)?;
46            handle.reload(filter).map_err(|e| e.to_string())?;
47        }
48        Ok(())
49    }
50}
51
52/// Single global log handle shared by all reloadable layers.
53static LOG_HANDLE: std::sync::Mutex<LogFilterHandle> =
54    std::sync::Mutex::new(LogFilterHandle::new());
55
56/// Registers a reload handle for a layer (stdout, file, etc.).
57///
58/// Can be called multiple times — each handle is appended.
59pub fn install_log_handle(handle: LogFilterReloadHandle) {
60    LOG_HANDLE.lock().expect("log handle poisoned").push(handle);
61}
62
63/// Returns `true` if at least one global log handle is available.
64pub fn log_handle_available() -> bool {
65    LOG_HANDLE.lock().expect("log handle poisoned").is_available()
66}
67
68/// Sets the global log verbosity level.
69///
70/// - 0: OFF
71/// - 1: ERROR
72/// - 2: WARN
73/// - 3: INFO
74/// - 4: DEBUG
75/// - 5+: TRACE
76///
77/// Updates all reloadable layers (stdout, file, etc.). The default directives that suppress noisy
78/// dependencies are kept, as they are at startup.
79///
80/// Returns an error if no log handle is installed or if the reload fails.
81pub fn set_log_verbosity(level: usize) -> Result<(), String> {
82    let guard = LOG_HANDLE.lock().expect("log handle poisoned");
83
84    if !guard.is_available() {
85        return Err("Log filter reload not available".to_string());
86    }
87
88    let level_filter = match level {
89        0 => LevelFilter::OFF,
90        1 => LevelFilter::ERROR,
91        2 => LevelFilter::WARN,
92        3 => LevelFilter::INFO,
93        4 => LevelFilter::DEBUG,
94        _ => LevelFilter::TRACE,
95    };
96
97    guard.reload_all(|_| filter_with_defaults(&level_filter.to_string()))
98}
99
100/// Builds a filter from the default directives followed by `pattern`, as startup does, so the
101/// defaults still apply unless `pattern` names the same target.
102fn filter_with_defaults(pattern: &str) -> Result<EnvFilter, String> {
103    EnvFilter::try_new(format!("{},{pattern}", DEFAULT_ENV_FILTER_DIRECTIVES.join(",")))
104        .map_err(|e| format!("Invalid filter pattern: {e}"))
105}
106
107/// Sets module-specific log levels using a pattern string.
108///
109/// Pattern format follows the `RUST_LOG` environment variable syntax:
110/// - `module1=level1,module2=level2`
111/// - Example: `reth::sync=debug,reth::net=trace`
112/// - Example: `info,reth::stages=debug`
113///
114/// The default directives that suppress noisy dependencies are kept, as they are at startup. An
115/// empty string restores each layer's initial filter.
116///
117/// Updates all reloadable layers (stdout, file, etc.).
118///
119/// Returns an error if no log handle is installed or if parsing fails.
120pub fn set_log_vmodule(pattern: &str) -> Result<(), String> {
121    let guard = LOG_HANDLE.lock().expect("log handle poisoned");
122
123    if !guard.is_available() {
124        return Err("Log filter reload not available".to_string());
125    }
126
127    if pattern.trim().is_empty() {
128        // The initial filter is an `EnvFilter` rendered back to its directive list.
129        guard.reload_all(|initial| EnvFilter::try_new(initial).map_err(|e| e.to_string()))
130    } else {
131        // Validate the pattern once before reloading all handles
132        filter_with_defaults(pattern)?;
133        guard.reload_all(|_| filter_with_defaults(pattern))
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use std::sync::{Arc, Mutex};
141    use tracing::{Event, Subscriber};
142    use tracing_subscriber::{
143        layer::{Context, SubscriberExt},
144        Layer,
145    };
146
147    /// Records the target of every event it sees.
148    struct Targets(Arc<Mutex<Vec<String>>>);
149
150    impl<S: Subscriber> Layer<S> for Targets {
151        fn on_event(&self, event: &Event<'_>, _: Context<'_, S>) {
152            self.0.lock().unwrap().push(event.metadata().target().to_owned());
153        }
154    }
155
156    /// Emits `discv5` debug and warn events, both off by default, and a `reload_test` debug event.
157    fn emit() {
158        tracing::debug!(target: "discv5::unit", "suppressed by default");
159        tracing::warn!(target: "discv5::unit", "suppressed by default");
160        tracing::debug!(target: "reload_test", "kept");
161    }
162
163    fn seen_through(filter: EnvFilter) -> Vec<String> {
164        let seen = Arc::new(Mutex::new(Vec::new()));
165        let subscriber = Registry::default().with(Targets(Arc::clone(&seen)).with_filter(filter));
166        tracing::subscriber::with_default(subscriber, emit);
167        Arc::into_inner(seen).expect("the subscriber is dropped").into_inner().unwrap()
168    }
169
170    #[test]
171    fn a_reloaded_filter_keeps_the_default_directives() {
172        assert_eq!(
173            seen_through(filter_with_defaults(&LevelFilter::DEBUG.to_string()).unwrap()),
174            ["reload_test"]
175        );
176        assert_eq!(
177            seen_through(filter_with_defaults("debug,discv5=debug").unwrap()),
178            ["discv5::unit", "discv5::unit", "reload_test"]
179        );
180        assert!(filter_with_defaults("reload_test=nonsense").is_err());
181    }
182
183    #[test]
184    fn an_empty_pattern_restores_the_initial_filter() {
185        let initial = EnvFilter::try_new("warn,reload_test=debug").unwrap();
186        let (filter, handle) = reload::Layer::new(initial);
187        install_log_handle(handle);
188
189        let seen = Arc::new(Mutex::new(Vec::new()));
190        let subscriber = Registry::default().with(Targets(Arc::clone(&seen)).with_filter(filter));
191        tracing::subscriber::with_default(subscriber, || {
192            set_log_vmodule("error").unwrap();
193            emit();
194            set_log_vmodule("").unwrap();
195            emit();
196        });
197
198        // Under "error" nothing passes; the restored filter lets `reload_test` debug and the
199        // `discv5` warning through, exactly as it did before the first reload.
200        assert_eq!(*seen.lock().unwrap(), ["discv5::unit", "reload_test"]);
201    }
202}