1use 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#[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 pub fn try_init(self) -> Result<(), Self> {
51 TRACE_DEFAULTS.set(self)
52 }
53
54 pub fn get_global() -> &'static Self {
56 TRACE_DEFAULTS.get_or_init(Self::default)
57 }
58
59 pub fn with_otlp(mut self, otlp: Option<Url>) -> Self {
61 self.otlp = otlp;
62 self
63 }
64
65 pub fn with_logs_otlp(mut self, logs_otlp: Option<Url>) -> Self {
67 self.logs_otlp = logs_otlp;
68 self
69 }
70
71 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 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 pub const fn with_protocol(mut self, protocol: OtlpProtocol) -> Self {
85 self.protocol = protocol;
86 self
87 }
88
89 pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
91 self.service_name = name.into();
92 self
93 }
94
95 pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
97 self.service_version = Some(version.into());
98 self
99 }
100
101 pub fn with_otlp_filter(mut self, filter: impl Into<String>) -> Self {
103 self.otlp_filter = filter.into();
104 self
105 }
106
107 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 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#[derive(Debug, Clone, Parser)]
129pub struct TraceArgs {
130 #[arg(
141 long = "tracing-otlp",
142 env = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
144 global = true,
145 value_name = "URL",
146 num_args = 0..=1,
147 default_value = Resettable::from(DefaultTraceValues::get_global().otlp.as_ref().map(|url| url.as_str().into())),
148 default_missing_value = DefaultTraceValues::get_global().otlp_default_endpoint.as_str(),
149 require_equals = true,
150 value_parser = parse_otlp_endpoint,
151 help_heading = "Tracing"
152 )]
153 pub otlp: Option<Url>,
154
155 #[arg(
163 long = "logs-otlp",
164 env = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
165 global = true,
166 value_name = "URL",
167 num_args = 0..=1,
168 default_value = Resettable::from(DefaultTraceValues::get_global().logs_otlp.as_ref().map(|url| url.as_str().into())),
169 default_missing_value = DefaultTraceValues::get_global().logs_otlp_default_endpoint.as_str(),
170 require_equals = true,
171 value_parser = parse_otlp_endpoint,
172 help_heading = "Logging"
173 )]
174 pub logs_otlp: Option<Url>,
175
176 #[arg(
183 long = "tracing-otlp-protocol",
184 env = "OTEL_EXPORTER_OTLP_PROTOCOL",
185 global = true,
186 value_name = "PROTOCOL",
187 default_value = DefaultTraceValues::get_global().protocol_as_str(),
188 help_heading = "Tracing"
189 )]
190 pub protocol: OtlpProtocol,
191
192 #[arg(
200 long = "tracing-otlp.filter",
201 global = true,
202 value_name = "FILTER",
203 default_value = DefaultTraceValues::get_global().otlp_filter.as_str(),
204 help_heading = "Tracing"
205 )]
206 pub otlp_filter: EnvFilter,
207
208 #[arg(
216 long = "logs-otlp.filter",
217 global = true,
218 value_name = "FILTER",
219 default_value = DefaultTraceValues::get_global().logs_otlp_filter.as_str(),
220 help_heading = "Logging"
221 )]
222 pub logs_otlp_filter: EnvFilter,
223
224 #[arg(
231 long = "tracing-otlp.service-name",
232 env = "OTEL_SERVICE_NAME",
233 global = true,
234 value_name = "NAME",
235 default_value = DefaultTraceValues::get_global().service_name.as_str(),
236 hide = true,
237 help_heading = "Tracing"
238 )]
239 pub service_name: String,
240
241 #[arg(
246 long = "tracing-otlp.service-version",
247 env = "OTEL_SERVICE_VERSION",
248 global = true,
249 value_name = "VERSION",
250 default_value = Resettable::from(DefaultTraceValues::get_global().service_version.as_ref().map(|version| version.as_str().into())),
251 hide = true,
252 help_heading = "Tracing"
253 )]
254 pub service_version: Option<String>,
255
256 #[arg(
265 long = "tracing-otlp.sample-ratio",
266 env = "OTEL_TRACES_SAMPLER_ARG",
267 global = true,
268 value_name = "RATIO",
269 default_value = Resettable::from(DefaultTraceValues::get_global().sample_ratio.map(|ratio| ratio.to_string().into())),
270 help_heading = "Tracing"
271 )]
272 pub sample_ratio: Option<f64>,
273}
274
275impl Default for TraceArgs {
276 fn default() -> Self {
277 let defaults = DefaultTraceValues::get_global();
278 Self {
279 otlp: defaults.otlp.clone(),
280 logs_otlp: defaults.logs_otlp.clone(),
281 protocol: defaults.protocol,
282 otlp_filter: defaults.otlp_filter.parse().expect("valid filter"),
283 logs_otlp_filter: defaults.logs_otlp_filter.parse().expect("valid filter"),
284 sample_ratio: defaults.sample_ratio,
285 service_name: defaults.service_name.clone(),
286 service_version: defaults.service_version.clone(),
287 }
288 }
289}
290
291impl TraceArgs {
292 pub async fn init_otlp_tracing(
303 &mut self,
304 _layers: &mut Layers,
305 ) -> eyre::Result<OtlpInitStatus> {
306 if let Some(endpoint) = self.otlp.as_mut() {
307 self.protocol.validate_endpoint(endpoint)?;
308
309 #[cfg(feature = "otlp")]
310 {
311 {
312 let mut config = reth_tracing_otlp::OtlpConfig::new(
313 self.service_name.clone(),
314 endpoint.clone(),
315 self.protocol,
316 self.sample_ratio,
317 )?;
318 if let Some(version) = &self.service_version {
319 config = config.with_service_version(version.clone());
320 }
321
322 _layers.with_span_layer(config.clone(), self.otlp_filter.clone())?;
323
324 Ok(OtlpInitStatus::Started(config.endpoint().clone()))
325 }
326 }
327 #[cfg(not(feature = "otlp"))]
328 {
329 Ok(OtlpInitStatus::NoFeature)
330 }
331 } else {
332 Ok(OtlpInitStatus::Disabled)
333 }
334 }
335
336 pub async fn init_otlp_logs(&mut self, _layers: &mut Layers) -> eyre::Result<OtlpLogsStatus> {
343 if let Some(endpoint) = self.logs_otlp.as_mut() {
344 self.protocol.validate_logs_endpoint(endpoint)?;
345
346 #[cfg(feature = "otlp-logs")]
347 {
348 let mut config = reth_tracing_otlp::OtlpLogsConfig::new(
349 self.service_name.clone(),
350 endpoint.clone(),
351 self.protocol,
352 )?;
353 if let Some(version) = &self.service_version {
354 config = config.with_service_version(version.clone());
355 }
356
357 _layers.with_log_layer(config.clone(), self.logs_otlp_filter.clone())?;
358
359 Ok(OtlpLogsStatus::Started(config.endpoint().clone()))
360 }
361 #[cfg(not(feature = "otlp-logs"))]
362 {
363 Ok(OtlpLogsStatus::NoFeature)
364 }
365 } else {
366 Ok(OtlpLogsStatus::Disabled)
367 }
368 }
369}
370
371#[derive(Debug)]
373pub enum OtlpInitStatus {
374 Started(Url),
376 Disabled,
378 NoFeature,
380}
381
382#[derive(Debug)]
384pub enum OtlpLogsStatus {
385 Started(Url),
387 Disabled,
389 NoFeature,
391}
392
393fn parse_otlp_endpoint(arg: &str) -> eyre::Result<Url> {
395 Url::parse(arg).wrap_err("Invalid URL for OTLP trace output")
396}
397
398#[cfg(test)]
399mod tests {
400 use super::{DefaultTraceValues, TraceArgs};
401 use reth_tracing_otlp::OtlpProtocol;
402
403 #[test]
404 fn default_trace_values_can_override_filters() {
405 let defaults =
406 DefaultTraceValues::default().with_otlp_filter("trace").with_logs_otlp_filter("debug");
407
408 assert_eq!(defaults.otlp_filter, "trace");
409 assert_eq!(defaults.logs_otlp_filter, "debug");
410 }
411
412 #[test]
413 fn default_trace_values_can_override_all_defaults() {
414 let otlp = "http://localhost:4318/v1/traces".parse().unwrap();
415 let logs_otlp = "http://localhost:4318/v1/logs".parse().unwrap();
416 let defaults = DefaultTraceValues::default()
417 .with_otlp(Some(otlp))
418 .with_logs_otlp(Some(logs_otlp))
419 .with_otlp_default_endpoint("http://collector:4318/v1/traces")
420 .with_logs_otlp_default_endpoint("http://collector:4318/v1/logs")
421 .with_protocol(OtlpProtocol::Grpc)
422 .with_service_name("custom")
423 .with_service_version("1.2.3")
424 .with_otlp_filter("info")
425 .with_logs_otlp_filter("debug")
426 .with_sample_ratio(Some(0.5));
427
428 let args = TraceArgs {
429 otlp: defaults.otlp.clone(),
430 logs_otlp: defaults.logs_otlp.clone(),
431 protocol: defaults.protocol,
432 otlp_filter: defaults.otlp_filter.parse().unwrap(),
433 logs_otlp_filter: defaults.logs_otlp_filter.parse().unwrap(),
434 service_name: defaults.service_name.clone(),
435 service_version: defaults.service_version.clone(),
436 sample_ratio: defaults.sample_ratio,
437 };
438
439 assert!(args.otlp.is_some());
440 assert!(args.logs_otlp.is_some());
441 assert_eq!(args.protocol, OtlpProtocol::Grpc);
442 assert_eq!(args.service_name, "custom");
443 assert_eq!(args.service_version.as_deref(), Some("1.2.3"));
444 assert_eq!(args.sample_ratio, Some(0.5));
445 assert_eq!(defaults.otlp_default_endpoint, "http://collector:4318/v1/traces");
446 assert_eq!(defaults.logs_otlp_default_endpoint, "http://collector:4318/v1/logs");
447 }
448}