Skip to main content

reth_node_core/args/
trace.rs

1//! Opentelemetry tracing and logging configuration through CLI args.
2
3use clap::Parser;
4use eyre::WrapErr;
5use reth_tracing::{tracing_subscriber::EnvFilter, Layers};
6use reth_tracing_otlp::OtlpProtocol;
7use std::sync::OnceLock;
8use url::Url;
9
10static TRACE_DEFAULTS: OnceLock<DefaultTraceValues> = OnceLock::new();
11
12/// Overridable defaults for OTLP trace configuration.
13///
14/// Downstream binaries that embed reth can call
15/// `DefaultTraceValues::default().with_service_name("myapp").try_init()` before CLI parsing to
16/// change the defaults that clap will use.
17#[derive(Debug, Clone)]
18pub struct DefaultTraceValues {
19    service_name: String,
20    service_version: Option<String>,
21}
22
23impl Default for DefaultTraceValues {
24    fn default() -> Self {
25        Self { service_name: "reth".to_string(), service_version: None }
26    }
27}
28
29impl DefaultTraceValues {
30    /// Initialize the global trace defaults with this configuration.
31    pub fn try_init(self) -> Result<(), Self> {
32        TRACE_DEFAULTS.set(self)
33    }
34
35    /// Get a reference to the global trace defaults.
36    pub fn get_global() -> &'static Self {
37        TRACE_DEFAULTS.get_or_init(Self::default)
38    }
39
40    /// Set the default service name.
41    pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
42        self.service_name = name.into();
43        self
44    }
45
46    /// Set the default service version.
47    pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
48        self.service_version = Some(version.into());
49        self
50    }
51}
52
53/// CLI arguments for configuring `Opentelemetry` trace and logs export.
54#[derive(Debug, Clone, Parser)]
55pub struct TraceArgs {
56    /// Enable `Opentelemetry` tracing export to an OTLP endpoint.
57    ///
58    /// If no value provided, defaults based on protocol:
59    /// - HTTP: `http://localhost:4318/v1/traces`
60    /// - gRPC: `http://localhost:4317`
61    ///
62    /// Example: --tracing-otlp=http://collector:4318/v1/traces
63    #[arg(
64        long = "tracing-otlp",
65        // Per specification.
66        env = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
67        global = true,
68        value_name = "URL",
69        num_args = 0..=1,
70        default_missing_value = "http://localhost:4318/v1/traces",
71        require_equals = true,
72        value_parser = parse_otlp_endpoint,
73        help_heading = "Tracing"
74    )]
75    pub otlp: Option<Url>,
76
77    /// Enable `Opentelemetry` logs export to an OTLP endpoint.
78    ///
79    /// If no value provided, defaults based on protocol:
80    /// - HTTP: `http://localhost:4318/v1/logs`
81    /// - gRPC: `http://localhost:4317`
82    ///
83    /// Example: --logs-otlp=http://collector:4318/v1/logs
84    #[arg(
85        long = "logs-otlp",
86        env = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
87        global = true,
88        value_name = "URL",
89        num_args = 0..=1,
90        default_missing_value = "http://localhost:4318/v1/logs",
91        require_equals = true,
92        value_parser = parse_otlp_endpoint,
93        help_heading = "Logging"
94    )]
95    pub logs_otlp: Option<Url>,
96
97    /// OTLP transport protocol to use for exporting traces and logs.
98    ///
99    /// - `http`: expects endpoint path to end with `/v1/traces` or `/v1/logs`
100    /// - `grpc`: expects endpoint without a path
101    ///
102    /// Defaults to HTTP if not specified.
103    #[arg(
104        long = "tracing-otlp-protocol",
105        env = "OTEL_EXPORTER_OTLP_PROTOCOL",
106        global = true,
107        value_name = "PROTOCOL",
108        default_value = "http",
109        help_heading = "Tracing"
110    )]
111    pub protocol: OtlpProtocol,
112
113    /// Set a filter directive for the OTLP tracer. This controls the verbosity
114    /// of spans and events sent to the OTLP endpoint. It follows the same
115    /// syntax as the `RUST_LOG` environment variable.
116    ///
117    /// Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off
118    ///
119    /// Defaults to TRACE if not specified.
120    #[arg(
121        long = "tracing-otlp.filter",
122        global = true,
123        value_name = "FILTER",
124        default_value = "debug",
125        help_heading = "Tracing"
126    )]
127    pub otlp_filter: EnvFilter,
128
129    /// Set a filter directive for the OTLP logs exporter. This controls the verbosity
130    /// of logs sent to the OTLP endpoint. It follows the same syntax as the
131    /// `RUST_LOG` environment variable.
132    ///
133    /// Example: --logs-otlp.filter=info,reth=debug
134    ///
135    /// Defaults to INFO if not specified.
136    #[arg(
137        long = "logs-otlp.filter",
138        global = true,
139        value_name = "FILTER",
140        default_value = "info",
141        help_heading = "Logging"
142    )]
143    pub logs_otlp_filter: EnvFilter,
144
145    /// Service name to use for OTLP tracing export.
146    ///
147    /// This name will be used to identify the service in distributed tracing systems
148    /// like Jaeger or Zipkin. Useful for differentiating between multiple reth instances.
149    ///
150    /// Set via `OTEL_SERVICE_NAME` environment variable. Defaults to "reth" if not specified.
151    #[arg(
152        long = "tracing-otlp.service-name",
153        env = "OTEL_SERVICE_NAME",
154        global = true,
155        value_name = "NAME",
156        default_value = DefaultTraceValues::get_global().service_name.as_str(),
157        hide = true,
158        help_heading = "Tracing"
159    )]
160    pub service_name: String,
161
162    /// Service version to use for OTLP tracing export.
163    ///
164    /// Overrides the default version reported in the `service.version` OTLP resource attribute.
165    /// Falls back to the crate's `CARGO_PKG_VERSION` if not specified.
166    #[arg(
167        long = "tracing-otlp.service-version",
168        env = "OTEL_SERVICE_VERSION",
169        global = true,
170        value_name = "VERSION",
171        hide = true,
172        help_heading = "Tracing"
173    )]
174    pub service_version: Option<String>,
175
176    /// Trace sampling ratio to control the percentage of traces to export.
177    ///
178    /// Valid range: 0.0 to 1.0
179    /// - 1.0, default: Sample all traces
180    /// - 0.01: Sample 1% of traces
181    /// - 0.0: Disable sampling
182    ///
183    /// Example: --tracing-otlp.sample-ratio=0.0.
184    #[arg(
185        long = "tracing-otlp.sample-ratio",
186        env = "OTEL_TRACES_SAMPLER_ARG",
187        global = true,
188        value_name = "RATIO",
189        help_heading = "Tracing"
190    )]
191    pub sample_ratio: Option<f64>,
192}
193
194impl Default for TraceArgs {
195    fn default() -> Self {
196        let defaults = DefaultTraceValues::get_global();
197        Self {
198            otlp: None,
199            logs_otlp: None,
200            protocol: OtlpProtocol::Http,
201            otlp_filter: EnvFilter::from_default_env(),
202            logs_otlp_filter: EnvFilter::try_new("info").expect("valid filter"),
203            sample_ratio: None,
204            service_name: defaults.service_name.clone(),
205            service_version: defaults.service_version.clone(),
206        }
207    }
208}
209
210impl TraceArgs {
211    /// Initialize OTLP tracing with the given layers and runner.
212    ///
213    /// This method handles OTLP tracing initialization based on the configured options,
214    /// including validation, protocol selection, and feature flag checking.
215    ///
216    /// Returns the initialization status to allow callers to log appropriate messages.
217    ///
218    /// Note: even though this function is async, it does not actually perform any async operations.
219    /// It's needed only to be able to initialize the gRPC transport of OTLP tracing that needs to
220    /// be called inside a tokio runtime context.
221    pub async fn init_otlp_tracing(
222        &mut self,
223        _layers: &mut Layers,
224    ) -> eyre::Result<OtlpInitStatus> {
225        if let Some(endpoint) = self.otlp.as_mut() {
226            self.protocol.validate_endpoint(endpoint)?;
227
228            #[cfg(feature = "otlp")]
229            {
230                {
231                    let mut config = reth_tracing_otlp::OtlpConfig::new(
232                        self.service_name.clone(),
233                        endpoint.clone(),
234                        self.protocol,
235                        self.sample_ratio,
236                    )?;
237                    if let Some(version) = &self.service_version {
238                        config = config.with_service_version(version.clone());
239                    }
240
241                    _layers.with_span_layer(config.clone(), self.otlp_filter.clone())?;
242
243                    Ok(OtlpInitStatus::Started(config.endpoint().clone()))
244                }
245            }
246            #[cfg(not(feature = "otlp"))]
247            {
248                Ok(OtlpInitStatus::NoFeature)
249            }
250        } else {
251            Ok(OtlpInitStatus::Disabled)
252        }
253    }
254
255    /// Initialize OTLP logs export with the given layers.
256    ///
257    /// This method handles OTLP logs initialization based on the configured options,
258    /// including validation and protocol selection.
259    ///
260    /// Returns the initialization status to allow callers to log appropriate messages.
261    pub async fn init_otlp_logs(&mut self, _layers: &mut Layers) -> eyre::Result<OtlpLogsStatus> {
262        if let Some(endpoint) = self.logs_otlp.as_mut() {
263            self.protocol.validate_logs_endpoint(endpoint)?;
264
265            #[cfg(feature = "otlp-logs")]
266            {
267                let mut config = reth_tracing_otlp::OtlpLogsConfig::new(
268                    self.service_name.clone(),
269                    endpoint.clone(),
270                    self.protocol,
271                )?;
272                if let Some(version) = &self.service_version {
273                    config = config.with_service_version(version.clone());
274                }
275
276                _layers.with_log_layer(config.clone(), self.logs_otlp_filter.clone())?;
277
278                Ok(OtlpLogsStatus::Started(config.endpoint().clone()))
279            }
280            #[cfg(not(feature = "otlp-logs"))]
281            {
282                Ok(OtlpLogsStatus::NoFeature)
283            }
284        } else {
285            Ok(OtlpLogsStatus::Disabled)
286        }
287    }
288}
289
290/// Status of OTLP tracing initialization.
291#[derive(Debug)]
292pub enum OtlpInitStatus {
293    /// OTLP tracing was successfully started with the given endpoint.
294    Started(Url),
295    /// OTLP tracing is disabled (no endpoint configured).
296    Disabled,
297    /// OTLP arguments provided but feature is not compiled.
298    NoFeature,
299}
300
301/// Status of OTLP logs initialization.
302#[derive(Debug)]
303pub enum OtlpLogsStatus {
304    /// OTLP logs export was successfully started with the given endpoint.
305    Started(Url),
306    /// OTLP logs export is disabled (no endpoint configured).
307    Disabled,
308    /// OTLP logs arguments provided but feature is not compiled.
309    NoFeature,
310}
311
312// Parses an OTLP endpoint url.
313fn parse_otlp_endpoint(arg: &str) -> eyre::Result<Url> {
314    Url::parse(arg).wrap_err("Invalid URL for OTLP trace output")
315}