reth_tracing_otlp/
lib.rs

1#![cfg(feature = "otlp")]
2
3//! Provides a tracing layer for `OpenTelemetry` that exports spans to an OTLP endpoint.
4//!
5//! This module simplifies the integration of `OpenTelemetry` tracing with OTLP export in Rust
6//! applications. It allows for easily capturing and exporting distributed traces to compatible
7//! backends like Jaeger, Zipkin, or any other OpenTelemetry-compatible tracing system.
8
9use clap::ValueEnum;
10use eyre::ensure;
11use opentelemetry::{global, trace::TracerProvider, KeyValue, Value};
12use opentelemetry_otlp::{SpanExporter, WithExportConfig};
13use opentelemetry_sdk::{
14    propagation::TraceContextPropagator,
15    trace::{SdkTracer, SdkTracerProvider},
16    Resource,
17};
18use opentelemetry_semantic_conventions::{attribute::SERVICE_VERSION, SCHEMA_URL};
19use tracing::Subscriber;
20use tracing_opentelemetry::OpenTelemetryLayer;
21use tracing_subscriber::registry::LookupSpan;
22use url::Url;
23
24// Otlp http endpoint is expected to end with this path.
25// See also <https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_traces_endpoint>.
26const HTTP_TRACE_ENDPOINT: &str = "/v1/traces";
27
28/// Creates a tracing [`OpenTelemetryLayer`] that exports spans to an OTLP endpoint.
29///
30/// This layer can be added to a [`tracing_subscriber::Registry`] to enable `OpenTelemetry` tracing
31/// with OTLP export to an url.
32pub fn span_layer<S>(
33    service_name: impl Into<Value>,
34    endpoint: &Url,
35    protocol: OtlpProtocol,
36) -> eyre::Result<OpenTelemetryLayer<S, SdkTracer>>
37where
38    for<'span> S: Subscriber + LookupSpan<'span>,
39{
40    global::set_text_map_propagator(TraceContextPropagator::new());
41
42    let resource = build_resource(service_name);
43
44    let span_builder = SpanExporter::builder();
45
46    let span_exporter = match protocol {
47        OtlpProtocol::Http => span_builder.with_http().with_endpoint(endpoint.as_str()).build()?,
48        OtlpProtocol::Grpc => span_builder.with_tonic().with_endpoint(endpoint.as_str()).build()?,
49    };
50
51    let tracer_provider = SdkTracerProvider::builder()
52        .with_resource(resource)
53        .with_batch_exporter(span_exporter)
54        .build();
55
56    global::set_tracer_provider(tracer_provider.clone());
57
58    let tracer = tracer_provider.tracer("reth");
59    Ok(tracing_opentelemetry::layer().with_tracer(tracer))
60}
61
62// Builds OTLP resource with service information.
63fn build_resource(service_name: impl Into<Value>) -> Resource {
64    Resource::builder()
65        .with_service_name(service_name)
66        .with_schema_url([KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))], SCHEMA_URL)
67        .build()
68}
69
70/// OTLP transport protocol type
71#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
72pub enum OtlpProtocol {
73    /// HTTP/Protobuf transport, port 4318, requires `/v1/traces` path
74    Http,
75    /// gRPC transport, port 4317
76    Grpc,
77}
78
79impl OtlpProtocol {
80    /// Validate and correct the URL to match protocol requirements.
81    ///
82    /// For HTTP: Ensures the path ends with `/v1/traces`, appending it if necessary.
83    /// For gRPC: Ensures the path does NOT include `/v1/traces`.
84    pub fn validate_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
85        match self {
86            Self::Http => {
87                if !url.path().ends_with(HTTP_TRACE_ENDPOINT) {
88                    let path = url.path().trim_end_matches('/');
89                    url.set_path(&format!("{}{}", path, HTTP_TRACE_ENDPOINT));
90                }
91            }
92            Self::Grpc => {
93                ensure!(
94                    !url.path().ends_with(HTTP_TRACE_ENDPOINT),
95                    "OTLP gRPC endpoint should not include {} path, got: {}",
96                    HTTP_TRACE_ENDPOINT,
97                    url
98                );
99            }
100        }
101        Ok(())
102    }
103}