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}