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