Skip to main content

reth_tracing/
layers.rs

1use crate::{formatter::LogFormat, LayerInfo, LogFilterReloadHandle};
2#[cfg(feature = "otlp-logs")]
3use reth_tracing_otlp::{log_layer, OtlpLogsConfig};
4#[cfg(feature = "otlp")]
5use reth_tracing_otlp::{span_layer, OtlpConfig};
6use rolling_file::{RollingConditionBasic, RollingFileAppender};
7use std::{
8    fmt,
9    path::{Path, PathBuf},
10};
11use tracing_appender::non_blocking::WorkerGuard;
12use tracing_subscriber::{filter::Directive, reload, EnvFilter, Layer, Registry};
13
14/// A worker guard returned by the file layer.
15///
16///  When a guard is dropped, all events currently in-memory are flushed to the log file this guard
17///  belongs to.
18pub type FileWorkerGuard = tracing_appender::non_blocking::WorkerGuard;
19
20///  A boxed tracing [Layer].
21pub(crate) type BoxedLayer<S> = Box<dyn Layer<S> + Send + Sync>;
22
23/// Default [directives](Directive) for [`EnvFilter`] which:
24/// 1. Disable high-frequency debug logs from dependencies such as `hyper`, `hickory-resolver`,
25///    `hickory_proto`, `discv5`, `jsonrpsee-server`, and `hyper_util::client::legacy::pool`.
26/// 2. Set noisy crates like `opentelemetry_*`, `rustls`, and `tungstenite` to `WARN`.
27const DEFAULT_ENV_FILTER_DIRECTIVES: [&str; 11] = [
28    "hyper::proto::h1=off",
29    "hickory_resolver=off",
30    "hickory_proto=off",
31    "discv5=off",
32    "jsonrpsee-server=off",
33    "opentelemetry-otlp=warn",
34    "opentelemetry_sdk=warn",
35    "opentelemetry-http=warn",
36    "hyper_util::client::legacy::pool=off",
37    "rustls=warn",
38    "tungstenite=warn",
39];
40
41/// Manages the collection of layers for a tracing subscriber.
42///
43/// `Layers` acts as a container for different logging layers such as stdout, file, or journald.
44/// Each layer can be configured separately and then combined into a tracing subscriber.
45#[derive(Default)]
46pub struct Layers {
47    inner: Vec<BoxedLayer<Registry>>,
48}
49
50impl fmt::Debug for Layers {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.debug_struct("Layers").field("layers_count", &self.inner.len()).finish()
53    }
54}
55
56impl Layers {
57    /// Creates a new `Layers` instance.
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Adds a layer to the collection of layers.
63    pub fn add_layer<L>(&mut self, layer: L)
64    where
65        L: Layer<Registry> + Send + Sync,
66    {
67        self.inner.push(layer.boxed());
68    }
69
70    /// Consumes the `Layers` instance, returning the inner vector of layers.
71    pub(crate) fn into_inner(self) -> Vec<BoxedLayer<Registry>> {
72        self.inner
73    }
74
75    /// Adds a journald layer to the layers collection.
76    ///
77    /// # Arguments
78    /// * `filter` - A string containing additional filter directives for this layer.
79    ///
80    /// # Returns
81    /// An `eyre::Result<()>` indicating the success or failure of the operation.
82    pub(crate) fn journald(&mut self, filter: &str) -> eyre::Result<()> {
83        let journald_filter = build_env_filter(None, filter)?;
84        let layer = tracing_journald::layer()?.with_filter(journald_filter);
85        self.add_layer(layer);
86        Ok(())
87    }
88
89    /// Adds a stdout layer with specified formatting and filtering.
90    ///
91    /// # Arguments
92    /// * `format` - The log message format.
93    /// * `default_directive` - Directive for the default logging level.
94    /// * `filters` - Additional filter directives as a string.
95    /// * `color` - Optional color configuration for the log messages.
96    /// * `reloadable` - If true, wraps the filter in a reload layer so it can be changed at
97    ///   runtime, and returns the reload handle.
98    ///
99    /// # Returns
100    /// An `eyre::Result` with an optional [`LogFilterReloadHandle`] (present when `reloadable` is
101    /// true).
102    pub(crate) fn stdout(
103        &mut self,
104        format: LogFormat,
105        default_directive: Directive,
106        filters: &str,
107        color: Option<String>,
108        reloadable: bool,
109    ) -> eyre::Result<Option<LogFilterReloadHandle>> {
110        let filter = build_env_filter(Some(default_directive), filters)?;
111
112        // When reloadable, always show target since the user may switch to DEBUG/TRACE
113        // at runtime via RPC — freezing target=false at init would hide module paths.
114        // Otherwise, only show target when initial level is higher than INFO.
115        let show_target = reloadable ||
116            filter.max_level_hint().is_none_or(|max_level| max_level > tracing::Level::INFO);
117
118        if reloadable {
119            let (reloadable_filter, handle) = reload::Layer::new(filter);
120            let layer = format.apply(reloadable_filter, color, show_target, None);
121            self.add_layer(layer);
122            Ok(Some(handle))
123        } else {
124            let layer = format.apply(filter, color, show_target, None);
125            self.add_layer(layer);
126            Ok(None)
127        }
128    }
129
130    /// Adds a file logging layer to the layers collection.
131    ///
132    /// # Arguments
133    /// * `format` - The format for log messages.
134    /// * `filter` - Additional filter directives as a string.
135    /// * `file_info` - Information about the log file including path and rotation strategy.
136    /// * `reloadable` - If true, wraps the filter in a reload layer so it can be changed at
137    ///   runtime, and returns the reload handle.
138    ///
139    /// # Returns
140    /// An `eyre::Result` with the file worker guard and an optional [`LogFilterReloadHandle`]
141    /// (present when `reloadable` is true).
142    pub(crate) fn file(
143        &mut self,
144        format: LogFormat,
145        filter: &str,
146        file_info: FileInfo,
147        reloadable: bool,
148    ) -> eyre::Result<(FileWorkerGuard, Option<LogFilterReloadHandle>)> {
149        let (writer, guard) = file_info.create_log_writer()?;
150        let file_filter = build_env_filter(None, filter)?;
151
152        if reloadable {
153            let (reloadable_filter, handle) = reload::Layer::new(file_filter);
154            self.add_layer(format.apply(reloadable_filter, None, true, Some(writer)));
155            Ok((guard, Some(handle)))
156        } else {
157            self.add_layer(format.apply(file_filter, None, true, Some(writer)));
158            Ok((guard, None))
159        }
160    }
161
162    pub(crate) fn samply(&mut self, config: LayerInfo) -> eyre::Result<()> {
163        self.add_layer(
164            tracing_samply::SamplyLayer::new()
165                .map_err(|e| eyre::eyre!("Failed to create samply layer: {e}"))?
166                .with_filter(build_env_filter(
167                    Some(config.default_directive.parse()?),
168                    &config.filters,
169                )?),
170        );
171        Ok(())
172    }
173
174    #[cfg(feature = "tracy")]
175    pub(crate) fn tracy(&mut self, config: LayerInfo) -> eyre::Result<()> {
176        // Newtype wrapper around `DefaultFields` so that `FormattedFields<TracyFields>` uses a
177        // distinct extension key from the fmt layer's `FormattedFields<DefaultFields>`. Without
178        // this, when both layers are active the fmt layer may insert ANSI-colored fields first,
179        // and the Tracy layer reuses them — leaking escape codes into Tracy zone text.
180        struct TracyFields(tracing_subscriber::fmt::format::DefaultFields);
181        impl<'writer> tracing_subscriber::fmt::FormatFields<'writer> for TracyFields {
182            fn format_fields<R: tracing_subscriber::field::RecordFields>(
183                &self,
184                writer: tracing_subscriber::fmt::format::Writer<'writer>,
185                fields: R,
186            ) -> core::fmt::Result {
187                self.0.format_fields(writer, fields)
188            }
189        }
190
191        struct Config(TracyFields);
192        impl tracing_tracy::Config for Config {
193            type Formatter = TracyFields;
194            fn formatter(&self) -> &Self::Formatter {
195                &self.0
196            }
197            fn format_fields_in_zone_name(&self) -> bool {
198                false
199            }
200        }
201
202        self.add_layer(
203            tracing_tracy::TracyLayer::new(Config(TracyFields(Default::default()))).with_filter(
204                build_env_filter(Some(config.default_directive.parse()?), &config.filters)?,
205            ),
206        );
207        Ok(())
208    }
209
210    /// Add OTLP spans layer to the layer collection
211    #[cfg(feature = "otlp")]
212    pub fn with_span_layer(
213        &mut self,
214        otlp_config: OtlpConfig,
215        filter: EnvFilter,
216    ) -> eyre::Result<()> {
217        // Create the span provider
218
219        let span_layer = span_layer(otlp_config)
220            .map_err(|e| eyre::eyre!("Failed to build OTLP span exporter {}", e))?
221            .with_filter(filter);
222
223        self.add_layer(span_layer);
224
225        Ok(())
226    }
227
228    /// Add OTLP logs layer to the layer collection
229    #[cfg(feature = "otlp-logs")]
230    pub fn with_log_layer(
231        &mut self,
232        otlp_config: OtlpLogsConfig,
233        filter: EnvFilter,
234    ) -> eyre::Result<()> {
235        let log_layer = log_layer(otlp_config)
236            .map_err(|e| eyre::eyre!("Failed to build OTLP log exporter {}", e))?
237            .with_filter(filter);
238
239        self.add_layer(log_layer);
240
241        Ok(())
242    }
243}
244
245/// Holds configuration information for file logging.
246///
247/// Contains details about the log file's path, name, size, and rotation strategy.
248#[derive(Debug, Clone)]
249pub struct FileInfo {
250    dir: PathBuf,
251    file_name: String,
252    max_size_bytes: u64,
253    max_files: usize,
254}
255
256impl FileInfo {
257    /// Creates a new `FileInfo` instance.
258    pub const fn new(
259        dir: PathBuf,
260        file_name: String,
261        max_size_bytes: u64,
262        max_files: usize,
263    ) -> Self {
264        Self { dir, file_name, max_size_bytes, max_files }
265    }
266
267    /// Creates the log directory if it doesn't exist.
268    fn create_log_dir(&self) -> eyre::Result<&Path> {
269        let log_dir: &Path = self.dir.as_ref();
270        if !log_dir.exists() {
271            std::fs::create_dir_all(log_dir)
272                .map_err(|err| eyre::eyre!("Could not create log directory {log_dir:?}: {err}"))?;
273        }
274        Ok(log_dir)
275    }
276
277    /// Creates a non-blocking writer for the log file.
278    fn create_log_writer(
279        &self,
280    ) -> eyre::Result<(tracing_appender::non_blocking::NonBlocking, WorkerGuard)> {
281        let log_dir = self.create_log_dir()?;
282        let (writer, guard) = tracing_appender::non_blocking(
283            RollingFileAppender::new(
284                log_dir.join(&self.file_name),
285                RollingConditionBasic::new().max_size(self.max_size_bytes),
286                self.max_files,
287            )
288            .map_err(|err| eyre::eyre!("Could not initialize file logging: {err}"))?,
289        );
290        Ok((writer, guard))
291    }
292}
293
294/// Builds an environment filter for logging.
295///
296/// The events are filtered by `default_directive`, unless overridden by `RUST_LOG`.
297///
298/// # Arguments
299/// * `default_directive` - An optional `Directive` that sets the default directive.
300/// * `directives` - Additional directives as a comma-separated string.
301///
302/// # Returns
303/// An `eyre::Result<EnvFilter>` that can be used to configure a tracing subscriber.
304fn build_env_filter(
305    default_directive: Option<Directive>,
306    directives: &str,
307) -> eyre::Result<EnvFilter> {
308    let env_filter = if let Some(default_directive) = default_directive {
309        EnvFilter::builder().with_default_directive(default_directive).from_env_lossy()
310    } else {
311        EnvFilter::builder().from_env_lossy()
312    };
313
314    DEFAULT_ENV_FILTER_DIRECTIVES
315        .into_iter()
316        .chain(directives.split(',').filter(|d| !d.is_empty()))
317        .try_fold(env_filter, |env_filter, directive| {
318            Ok(env_filter.add_directive(directive.parse()?))
319        })
320}