Skip to main content

reth_tracing/
layers.rs

1use crate::{formatter::LogFormat, LayerInfo};
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, 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 `opentelemetry_*` crates log level to `WARN`, as `DEBUG` is too noisy.
27const DEFAULT_ENV_FILTER_DIRECTIVES: [&str; 9] = [
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];
38
39/// Manages the collection of layers for a tracing subscriber.
40///
41/// `Layers` acts as a container for different logging layers such as stdout, file, or journald.
42/// Each layer can be configured separately and then combined into a tracing subscriber.
43#[derive(Default)]
44pub struct Layers {
45    inner: Vec<BoxedLayer<Registry>>,
46}
47
48impl fmt::Debug for Layers {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.debug_struct("Layers").field("layers_count", &self.inner.len()).finish()
51    }
52}
53
54impl Layers {
55    /// Creates a new `Layers` instance.
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Adds a layer to the collection of layers.
61    pub fn add_layer<L>(&mut self, layer: L)
62    where
63        L: Layer<Registry> + Send + Sync,
64    {
65        self.inner.push(layer.boxed());
66    }
67
68    /// Consumes the `Layers` instance, returning the inner vector of layers.
69    pub(crate) fn into_inner(self) -> Vec<BoxedLayer<Registry>> {
70        self.inner
71    }
72
73    /// Adds a journald layer to the layers collection.
74    ///
75    /// # Arguments
76    /// * `filter` - A string containing additional filter directives for this layer.
77    ///
78    /// # Returns
79    /// An `eyre::Result<()>` indicating the success or failure of the operation.
80    pub(crate) fn journald(&mut self, filter: &str) -> eyre::Result<()> {
81        let journald_filter = build_env_filter(None, filter)?;
82        let layer = tracing_journald::layer()?.with_filter(journald_filter);
83        self.add_layer(layer);
84        Ok(())
85    }
86
87    /// Adds a stdout layer with specified formatting and filtering.
88    ///
89    /// # Type Parameters
90    /// * `S` - The type of subscriber that will use these layers.
91    ///
92    /// # Arguments
93    /// * `format` - The log message format.
94    /// * `directive` - Directive for the default logging level.
95    /// * `filter` - Additional filter directives as a string.
96    /// * `color` - Optional color configuration for the log messages.
97    ///
98    /// # Returns
99    /// An `eyre::Result<()>` indicating the success or failure of the operation.
100    pub(crate) fn stdout(
101        &mut self,
102        format: LogFormat,
103        default_directive: Directive,
104        filters: &str,
105        color: Option<String>,
106    ) -> eyre::Result<()> {
107        let filter = build_env_filter(Some(default_directive), filters)?;
108        let layer = format.apply(filter, color, None);
109        self.add_layer(layer);
110        Ok(())
111    }
112
113    /// Adds a file logging layer to the layers collection.
114    ///
115    /// # Arguments
116    /// * `format` - The format for log messages.
117    /// * `filter` - Additional filter directives as a string.
118    /// * `file_info` - Information about the log file including path and rotation strategy.
119    ///
120    /// # Returns
121    /// An `eyre::Result<FileWorkerGuard>` representing the file logging worker.
122    pub(crate) fn file(
123        &mut self,
124        format: LogFormat,
125        filter: &str,
126        file_info: FileInfo,
127    ) -> eyre::Result<FileWorkerGuard> {
128        let (writer, guard) = file_info.create_log_writer()?;
129        let file_filter = build_env_filter(None, filter)?;
130        let layer = format.apply(file_filter, None, Some(writer));
131        self.add_layer(layer);
132        Ok(guard)
133    }
134
135    pub(crate) fn samply(&mut self, config: LayerInfo) -> eyre::Result<()> {
136        self.add_layer(
137            tracing_samply::SamplyLayer::new()
138                .map_err(|e| eyre::eyre!("Failed to create samply layer: {e}"))?
139                .with_filter(build_env_filter(
140                    Some(config.default_directive.parse()?),
141                    &config.filters,
142                )?),
143        );
144        Ok(())
145    }
146
147    #[cfg(feature = "tracy")]
148    pub(crate) fn tracy(&mut self, config: LayerInfo) -> eyre::Result<()> {
149        struct Config(tracing_subscriber::fmt::format::DefaultFields);
150        impl tracing_tracy::Config for Config {
151            type Formatter = tracing_subscriber::fmt::format::DefaultFields;
152            fn formatter(&self) -> &Self::Formatter {
153                &self.0
154            }
155            fn format_fields_in_zone_name(&self) -> bool {
156                false
157            }
158        }
159
160        self.add_layer(tracing_tracy::TracyLayer::new(Config(Default::default())).with_filter(
161            build_env_filter(Some(config.default_directive.parse()?), &config.filters)?,
162        ));
163        Ok(())
164    }
165
166    /// Add OTLP spans layer to the layer collection
167    #[cfg(feature = "otlp")]
168    pub fn with_span_layer(
169        &mut self,
170        otlp_config: OtlpConfig,
171        filter: EnvFilter,
172    ) -> eyre::Result<()> {
173        // Create the span provider
174
175        let span_layer = span_layer(otlp_config)
176            .map_err(|e| eyre::eyre!("Failed to build OTLP span exporter {}", e))?
177            .with_filter(filter);
178
179        self.add_layer(span_layer);
180
181        Ok(())
182    }
183
184    /// Add OTLP logs layer to the layer collection
185    #[cfg(feature = "otlp-logs")]
186    pub fn with_log_layer(
187        &mut self,
188        otlp_config: OtlpLogsConfig,
189        filter: EnvFilter,
190    ) -> eyre::Result<()> {
191        let log_layer = log_layer(otlp_config)
192            .map_err(|e| eyre::eyre!("Failed to build OTLP log exporter {}", e))?
193            .with_filter(filter);
194
195        self.add_layer(log_layer);
196
197        Ok(())
198    }
199}
200
201/// Holds configuration information for file logging.
202///
203/// Contains details about the log file's path, name, size, and rotation strategy.
204#[derive(Debug, Clone)]
205pub struct FileInfo {
206    dir: PathBuf,
207    file_name: String,
208    max_size_bytes: u64,
209    max_files: usize,
210}
211
212impl FileInfo {
213    /// Creates a new `FileInfo` instance.
214    pub const fn new(
215        dir: PathBuf,
216        file_name: String,
217        max_size_bytes: u64,
218        max_files: usize,
219    ) -> Self {
220        Self { dir, file_name, max_size_bytes, max_files }
221    }
222
223    /// Creates the log directory if it doesn't exist.
224    fn create_log_dir(&self) -> eyre::Result<&Path> {
225        let log_dir: &Path = self.dir.as_ref();
226        if !log_dir.exists() {
227            std::fs::create_dir_all(log_dir)
228                .map_err(|err| eyre::eyre!("Could not create log directory {log_dir:?}: {err}"))?;
229        }
230        Ok(log_dir)
231    }
232
233    /// Creates a non-blocking writer for the log file.
234    fn create_log_writer(
235        &self,
236    ) -> eyre::Result<(tracing_appender::non_blocking::NonBlocking, WorkerGuard)> {
237        let log_dir = self.create_log_dir()?;
238        let (writer, guard) = tracing_appender::non_blocking(
239            RollingFileAppender::new(
240                log_dir.join(&self.file_name),
241                RollingConditionBasic::new().max_size(self.max_size_bytes),
242                self.max_files,
243            )
244            .map_err(|err| eyre::eyre!("Could not initialize file logging: {err}"))?,
245        );
246        Ok((writer, guard))
247    }
248}
249
250/// Builds an environment filter for logging.
251///
252/// The events are filtered by `default_directive`, unless overridden by `RUST_LOG`.
253///
254/// # Arguments
255/// * `default_directive` - An optional `Directive` that sets the default directive.
256/// * `directives` - Additional directives as a comma-separated string.
257///
258/// # Returns
259/// An `eyre::Result<EnvFilter>` that can be used to configure a tracing subscriber.
260fn build_env_filter(
261    default_directive: Option<Directive>,
262    directives: &str,
263) -> eyre::Result<EnvFilter> {
264    let env_filter = if let Some(default_directive) = default_directive {
265        EnvFilter::builder().with_default_directive(default_directive).from_env_lossy()
266    } else {
267        EnvFilter::builder().from_env_lossy()
268    };
269
270    DEFAULT_ENV_FILTER_DIRECTIVES
271        .into_iter()
272        .chain(directives.split(',').filter(|d| !d.is_empty()))
273        .try_fold(env_filter, |env_filter, directive| {
274            Ok(env_filter.add_directive(directive.parse()?))
275        })
276}