Skip to main content

reth_node_core/args/
trace.rs

1//! Opentelemetry tracing and logging configuration through CLI args.
2
3use clap::{builder::Resettable, 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    otlp: Option<Url>,
20    logs_otlp: Option<Url>,
21    otlp_default_endpoint: String,
22    logs_otlp_default_endpoint: String,
23    protocol: OtlpProtocol,
24    service_name: String,
25    service_version: Option<String>,
26    otlp_filter: String,
27    logs_otlp_filter: String,
28    sample_ratio: Option<f64>,
29}
30
31impl Default for DefaultTraceValues {
32    fn default() -> Self {
33        Self {
34            otlp: None,
35            logs_otlp: None,
36            otlp_default_endpoint: "http://localhost:4318/v1/traces".to_string(),
37            logs_otlp_default_endpoint: "http://localhost:4318/v1/logs".to_string(),
38            protocol: OtlpProtocol::Http,
39            service_name: "reth".to_string(),
40            service_version: None,
41            otlp_filter: "debug".to_string(),
42            logs_otlp_filter: "info".to_string(),
43            sample_ratio: None,
44        }
45    }
46}
47
48impl DefaultTraceValues {
49    /// Initialize the global trace defaults with this configuration.
50    pub fn try_init(self) -> Result<(), Self> {
51        TRACE_DEFAULTS.set(self)
52    }
53
54    /// Get a reference to the global trace defaults.
55    pub fn get_global() -> &'static Self {
56        TRACE_DEFAULTS.get_or_init(Self::default)
57    }
58
59    /// Set the default OTLP tracing endpoint.
60    pub fn with_otlp(mut self, otlp: Option<Url>) -> Self {
61        self.otlp = otlp;
62        self
63    }
64
65    /// Set the default OTLP logs endpoint.
66    pub fn with_logs_otlp(mut self, logs_otlp: Option<Url>) -> Self {
67        self.logs_otlp = logs_otlp;
68        self
69    }
70
71    /// Set the endpoint used when `--tracing-otlp` is provided without a value.
72    pub fn with_otlp_default_endpoint(mut self, endpoint: impl Into<String>) -> Self {
73        self.otlp_default_endpoint = endpoint.into();
74        self
75    }
76
77    /// Set the endpoint used when `--logs-otlp` is provided without a value.
78    pub fn with_logs_otlp_default_endpoint(mut self, endpoint: impl Into<String>) -> Self {
79        self.logs_otlp_default_endpoint = endpoint.into();
80        self
81    }
82
83    /// Set the default OTLP transport protocol.
84    pub const fn with_protocol(mut self, protocol: OtlpProtocol) -> Self {
85        self.protocol = protocol;
86        self
87    }
88
89    /// Set the default service name.
90    pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
91        self.service_name = name.into();
92        self
93    }
94
95    /// Set the default service version.
96    pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
97        self.service_version = Some(version.into());
98        self
99    }
100
101    /// Set the default OTLP tracing filter.
102    pub fn with_otlp_filter(mut self, filter: impl Into<String>) -> Self {
103        self.otlp_filter = filter.into();
104        self
105    }
106
107    /// Set the default OTLP logs filter.
108    pub fn with_logs_otlp_filter(mut self, filter: impl Into<String>) -> Self {
109        self.logs_otlp_filter = filter.into();
110        self
111    }
112
113    /// Set the default OTLP trace sampling ratio.
114    pub const fn with_sample_ratio(mut self, sample_ratio: Option<f64>) -> Self {
115        self.sample_ratio = sample_ratio;
116        self
117    }
118
119    const fn protocol_as_str(&self) -> &'static str {
120        match self.protocol {
121            OtlpProtocol::Http => "http",
122            OtlpProtocol::Grpc => "grpc",
123        }
124    }
125}
126
127/// CLI arguments for configuring `Opentelemetry` trace and logs export.
128#[derive(Debug, Clone, Parser)]
129pub struct TraceArgs {
130    /// Enable `Opentelemetry` tracing export to an OTLP endpoint.
131    ///
132    /// If no value provided, defaults based on protocol:
133    /// - HTTP: `http://localhost:4318/v1/traces`
134    /// - gRPC: `http://localhost:4317`
135    ///
136    /// Example: --tracing-otlp=http://collector:4318/v1/traces
137    #[arg(
138        long = "tracing-otlp",
139        // Per specification.
140        env = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
141        global = true,
142        value_name = "URL",
143        num_args = 0..=1,
144        default_value = Resettable::from(DefaultTraceValues::get_global().otlp.as_ref().map(|url| url.as_str().into())),
145        default_missing_value = DefaultTraceValues::get_global().otlp_default_endpoint.as_str(),
146        require_equals = true,
147        value_parser = parse_otlp_endpoint,
148        help_heading = "Tracing"
149    )]
150    pub otlp: Option<Url>,
151
152    /// Enable `Opentelemetry` logs export to an OTLP endpoint.
153    ///
154    /// If no value provided, defaults based on protocol:
155    /// - HTTP: `http://localhost:4318/v1/logs`
156    /// - gRPC: `http://localhost:4317`
157    ///
158    /// Example: --logs-otlp=http://collector:4318/v1/logs
159    #[arg(
160        long = "logs-otlp",
161        env = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
162        global = true,
163        value_name = "URL",
164        num_args = 0..=1,
165        default_value = Resettable::from(DefaultTraceValues::get_global().logs_otlp.as_ref().map(|url| url.as_str().into())),
166        default_missing_value = DefaultTraceValues::get_global().logs_otlp_default_endpoint.as_str(),
167        require_equals = true,
168        value_parser = parse_otlp_endpoint,
169        help_heading = "Logging"
170    )]
171    pub logs_otlp: Option<Url>,
172
173    /// OTLP transport protocol to use for exporting traces and logs.
174    ///
175    /// - `http`: expects endpoint path to end with `/v1/traces` or `/v1/logs`
176    /// - `grpc`: expects endpoint without a path
177    ///
178    /// Defaults to HTTP if not specified.
179    #[arg(
180        long = "tracing-otlp-protocol",
181        env = "OTEL_EXPORTER_OTLP_PROTOCOL",
182        global = true,
183        value_name = "PROTOCOL",
184        default_value = DefaultTraceValues::get_global().protocol_as_str(),
185        help_heading = "Tracing"
186    )]
187    pub protocol: OtlpProtocol,
188
189    /// Set a filter directive for the OTLP tracer. This controls the verbosity
190    /// of spans and events sent to the OTLP endpoint. It follows the same
191    /// syntax as the `RUST_LOG` environment variable.
192    ///
193    /// Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off
194    ///
195    /// Defaults to TRACE if not specified.
196    #[arg(
197        long = "tracing-otlp.filter",
198        global = true,
199        value_name = "FILTER",
200        default_value = DefaultTraceValues::get_global().otlp_filter.as_str(),
201        help_heading = "Tracing"
202    )]
203    pub otlp_filter: EnvFilter,
204
205    /// Set a filter directive for the OTLP logs exporter. This controls the verbosity
206    /// of logs sent to the OTLP endpoint. It follows the same syntax as the
207    /// `RUST_LOG` environment variable.
208    ///
209    /// Example: --logs-otlp.filter=info,reth=debug
210    ///
211    /// Defaults to INFO if not specified.
212    #[arg(
213        long = "logs-otlp.filter",
214        global = true,
215        value_name = "FILTER",
216        default_value = DefaultTraceValues::get_global().logs_otlp_filter.as_str(),
217        help_heading = "Logging"
218    )]
219    pub logs_otlp_filter: EnvFilter,
220
221    /// Service name to use for OTLP tracing export.
222    ///
223    /// This name will be used to identify the service in distributed tracing systems
224    /// like Jaeger or Zipkin. Useful for differentiating between multiple reth instances.
225    ///
226    /// Set via `OTEL_SERVICE_NAME` environment variable. Defaults to "reth" if not specified.
227    #[arg(
228        long = "tracing-otlp.service-name",
229        env = "OTEL_SERVICE_NAME",
230        global = true,
231        value_name = "NAME",
232        default_value = DefaultTraceValues::get_global().service_name.as_str(),
233        hide = true,
234        help_heading = "Tracing"
235    )]
236    pub service_name: String,
237
238    /// Service version to use for OTLP tracing export.
239    ///
240    /// Overrides the default version reported in the `service.version` OTLP resource attribute.
241    /// Falls back to the crate's `CARGO_PKG_VERSION` if not specified.
242    #[arg(
243        long = "tracing-otlp.service-version",
244        env = "OTEL_SERVICE_VERSION",
245        global = true,
246        value_name = "VERSION",
247        default_value = Resettable::from(DefaultTraceValues::get_global().service_version.as_ref().map(|version| version.as_str().into())),
248        hide = true,
249        help_heading = "Tracing"
250    )]
251    pub service_version: Option<String>,
252
253    /// Trace sampling ratio to control the percentage of traces to export.
254    ///
255    /// Valid range: 0.0 to 1.0
256    /// - 1.0, default: Sample all traces
257    /// - 0.01: Sample 1% of traces
258    /// - 0.0: Disable sampling
259    ///
260    /// Example: --tracing-otlp.sample-ratio=0.0.
261    #[arg(
262        long = "tracing-otlp.sample-ratio",
263        env = "OTEL_TRACES_SAMPLER_ARG",
264        global = true,
265        value_name = "RATIO",
266        default_value = Resettable::from(DefaultTraceValues::get_global().sample_ratio.map(|ratio| ratio.to_string().into())),
267        help_heading = "Tracing"
268    )]
269    pub sample_ratio: Option<f64>,
270}
271
272impl Default for TraceArgs {
273    fn default() -> Self {
274        let defaults = DefaultTraceValues::get_global();
275        Self {
276            otlp: defaults.otlp.clone(),
277            logs_otlp: defaults.logs_otlp.clone(),
278            protocol: defaults.protocol,
279            otlp_filter: defaults.otlp_filter.parse().expect("valid filter"),
280            logs_otlp_filter: defaults.logs_otlp_filter.parse().expect("valid filter"),
281            sample_ratio: defaults.sample_ratio,
282            service_name: defaults.service_name.clone(),
283            service_version: defaults.service_version.clone(),
284        }
285    }
286}
287
288impl TraceArgs {
289    /// Initialize OTLP tracing with the given layers and runner.
290    ///
291    /// This method handles OTLP tracing initialization based on the configured options,
292    /// including validation, protocol selection, and feature flag checking.
293    ///
294    /// Returns the initialization status to allow callers to log appropriate messages.
295    ///
296    /// Note: even though this function is async, it does not actually perform any async operations.
297    /// It's needed only to be able to initialize the gRPC transport of OTLP tracing that needs to
298    /// be called inside a tokio runtime context.
299    pub async fn init_otlp_tracing(
300        &mut self,
301        _layers: &mut Layers,
302    ) -> eyre::Result<OtlpInitStatus> {
303        if let Some(endpoint) = self.otlp.as_mut() {
304            self.protocol.validate_endpoint(endpoint)?;
305
306            #[cfg(feature = "otlp")]
307            {
308                {
309                    let mut config = reth_tracing_otlp::OtlpConfig::new(
310                        self.service_name.clone(),
311                        endpoint.clone(),
312                        self.protocol,
313                        self.sample_ratio,
314                    )?;
315                    if let Some(version) = &self.service_version {
316                        config = config.with_service_version(version.clone());
317                    }
318
319                    _layers.with_span_layer(config.clone(), self.otlp_filter.clone())?;
320
321                    Ok(OtlpInitStatus::Started(config.endpoint().clone()))
322                }
323            }
324            #[cfg(not(feature = "otlp"))]
325            {
326                Ok(OtlpInitStatus::NoFeature)
327            }
328        } else {
329            Ok(OtlpInitStatus::Disabled)
330        }
331    }
332
333    /// Initialize OTLP logs export with the given layers.
334    ///
335    /// This method handles OTLP logs initialization based on the configured options,
336    /// including validation and protocol selection.
337    ///
338    /// Returns the initialization status to allow callers to log appropriate messages.
339    pub async fn init_otlp_logs(&mut self, _layers: &mut Layers) -> eyre::Result<OtlpLogsStatus> {
340        if let Some(endpoint) = self.logs_otlp.as_mut() {
341            self.protocol.validate_logs_endpoint(endpoint)?;
342
343            #[cfg(feature = "otlp-logs")]
344            {
345                let mut config = reth_tracing_otlp::OtlpLogsConfig::new(
346                    self.service_name.clone(),
347                    endpoint.clone(),
348                    self.protocol,
349                )?;
350                if let Some(version) = &self.service_version {
351                    config = config.with_service_version(version.clone());
352                }
353
354                _layers.with_log_layer(config.clone(), self.logs_otlp_filter.clone())?;
355
356                Ok(OtlpLogsStatus::Started(config.endpoint().clone()))
357            }
358            #[cfg(not(feature = "otlp-logs"))]
359            {
360                Ok(OtlpLogsStatus::NoFeature)
361            }
362        } else {
363            Ok(OtlpLogsStatus::Disabled)
364        }
365    }
366}
367
368/// Status of OTLP tracing initialization.
369#[derive(Debug)]
370pub enum OtlpInitStatus {
371    /// OTLP tracing was successfully started with the given endpoint.
372    Started(Url),
373    /// OTLP tracing is disabled (no endpoint configured).
374    Disabled,
375    /// OTLP arguments provided but feature is not compiled.
376    NoFeature,
377}
378
379/// Status of OTLP logs initialization.
380#[derive(Debug)]
381pub enum OtlpLogsStatus {
382    /// OTLP logs export was successfully started with the given endpoint.
383    Started(Url),
384    /// OTLP logs export is disabled (no endpoint configured).
385    Disabled,
386    /// OTLP logs arguments provided but feature is not compiled.
387    NoFeature,
388}
389
390// Parses an OTLP endpoint url.
391fn parse_otlp_endpoint(arg: &str) -> eyre::Result<Url> {
392    Url::parse(arg).wrap_err("Invalid URL for OTLP trace output")
393}
394
395#[cfg(test)]
396mod tests {
397    use super::{DefaultTraceValues, TraceArgs};
398    use reth_tracing_otlp::OtlpProtocol;
399
400    #[test]
401    fn default_trace_values_can_override_filters() {
402        let defaults =
403            DefaultTraceValues::default().with_otlp_filter("trace").with_logs_otlp_filter("debug");
404
405        assert_eq!(defaults.otlp_filter, "trace");
406        assert_eq!(defaults.logs_otlp_filter, "debug");
407    }
408
409    #[test]
410    fn default_trace_values_can_override_all_defaults() {
411        let otlp = "http://localhost:4318/v1/traces".parse().unwrap();
412        let logs_otlp = "http://localhost:4318/v1/logs".parse().unwrap();
413        let defaults = DefaultTraceValues::default()
414            .with_otlp(Some(otlp))
415            .with_logs_otlp(Some(logs_otlp))
416            .with_otlp_default_endpoint("http://collector:4318/v1/traces")
417            .with_logs_otlp_default_endpoint("http://collector:4318/v1/logs")
418            .with_protocol(OtlpProtocol::Grpc)
419            .with_service_name("custom")
420            .with_service_version("1.2.3")
421            .with_otlp_filter("info")
422            .with_logs_otlp_filter("debug")
423            .with_sample_ratio(Some(0.5));
424
425        let args = TraceArgs {
426            otlp: defaults.otlp.clone(),
427            logs_otlp: defaults.logs_otlp.clone(),
428            protocol: defaults.protocol,
429            otlp_filter: defaults.otlp_filter.parse().unwrap(),
430            logs_otlp_filter: defaults.logs_otlp_filter.parse().unwrap(),
431            service_name: defaults.service_name.clone(),
432            service_version: defaults.service_version.clone(),
433            sample_ratio: defaults.sample_ratio,
434        };
435
436        assert!(args.otlp.is_some());
437        assert!(args.logs_otlp.is_some());
438        assert_eq!(args.protocol, OtlpProtocol::Grpc);
439        assert_eq!(args.service_name, "custom");
440        assert_eq!(args.service_version.as_deref(), Some("1.2.3"));
441        assert_eq!(args.sample_ratio, Some(0.5));
442        assert_eq!(defaults.otlp_default_endpoint, "http://collector:4318/v1/traces");
443        assert_eq!(defaults.logs_otlp_default_endpoint, "http://collector:4318/v1/logs");
444    }
445}