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(
138 long = "tracing-otlp",
139 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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#[derive(Debug)]
370pub enum OtlpInitStatus {
371 Started(Url),
373 Disabled,
375 NoFeature,
377}
378
379#[derive(Debug)]
381pub enum OtlpLogsStatus {
382 Started(Url),
384 Disabled,
386 NoFeature,
388}
389
390fn 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}