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::{Sampler, 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>(otlp_config: OtlpConfig) -> eyre::Result<OpenTelemetryLayer<S, SdkTracer>>
33where
34    for<'span> S: Subscriber + LookupSpan<'span>,
35{
36    global::set_text_map_propagator(TraceContextPropagator::new());
37
38    let resource = build_resource(otlp_config.service_name.clone());
39
40    let span_builder = SpanExporter::builder();
41
42    let span_exporter = match otlp_config.protocol {
43        OtlpProtocol::Http => {
44            span_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
45        }
46        OtlpProtocol::Grpc => {
47            span_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
48        }
49    };
50
51    let sampler = build_sampler(otlp_config.sample_ratio)?;
52
53    let tracer_provider = SdkTracerProvider::builder()
54        .with_resource(resource)
55        .with_sampler(sampler)
56        .with_batch_exporter(span_exporter)
57        .build();
58
59    global::set_tracer_provider(tracer_provider.clone());
60
61    let tracer = tracer_provider.tracer(otlp_config.service_name);
62    Ok(tracing_opentelemetry::layer().with_tracer(tracer))
63}
64
65/// Configuration for OTLP trace export.
66#[derive(Debug, Clone)]
67pub struct OtlpConfig {
68    /// Service name for trace identification
69    service_name: String,
70    /// Otlp endpoint URL
71    endpoint: Url,
72    /// Transport protocol, HTTP or gRPC
73    protocol: OtlpProtocol,
74    /// Optional sampling ratio, from 0.0 to 1.0
75    sample_ratio: Option<f64>,
76}
77
78impl OtlpConfig {
79    /// Creates a new OTLP configuration.
80    pub fn new(
81        service_name: impl Into<String>,
82        endpoint: Url,
83        protocol: OtlpProtocol,
84        sample_ratio: Option<f64>,
85    ) -> eyre::Result<Self> {
86        if let Some(ratio) = sample_ratio {
87            ensure!(
88                (0.0..=1.0).contains(&ratio),
89                "Sample ratio must be between 0.0 and 1.0, got: {}",
90                ratio
91            );
92        }
93
94        Ok(Self { service_name: service_name.into(), endpoint, protocol, sample_ratio })
95    }
96
97    /// Returns the service name.
98    pub fn service_name(&self) -> &str {
99        &self.service_name
100    }
101
102    /// Returns the OTLP endpoint URL.
103    pub const fn endpoint(&self) -> &Url {
104        &self.endpoint
105    }
106
107    /// Returns the transport protocol.
108    pub const fn protocol(&self) -> OtlpProtocol {
109        self.protocol
110    }
111
112    /// Returns the sampling ratio.
113    pub const fn sample_ratio(&self) -> Option<f64> {
114        self.sample_ratio
115    }
116}
117
118// Builds OTLP resource with service information.
119fn build_resource(service_name: impl Into<Value>) -> Resource {
120    Resource::builder()
121        .with_service_name(service_name)
122        .with_schema_url([KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))], SCHEMA_URL)
123        .build()
124}
125
126/// Builds the appropriate sampler based on the sample ratio.
127fn build_sampler(sample_ratio: Option<f64>) -> eyre::Result<Sampler> {
128    match sample_ratio {
129        // Default behavior: sample all traces
130        None | Some(1.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))),
131        // Don't sample anything
132        Some(0.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOff))),
133        // Sample based on trace ID ratio
134        Some(ratio) => Ok(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio)))),
135    }
136}
137
138/// OTLP transport protocol type
139#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
140pub enum OtlpProtocol {
141    /// HTTP/Protobuf transport, port 4318, requires `/v1/traces` path
142    Http,
143    /// gRPC transport, port 4317
144    Grpc,
145}
146
147impl OtlpProtocol {
148    /// Validate and correct the URL to match protocol requirements.
149    ///
150    /// For HTTP: Ensures the path ends with `/v1/traces`, appending it if necessary.
151    /// For gRPC: Ensures the path does NOT include `/v1/traces`.
152    pub fn validate_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
153        match self {
154            Self::Http => {
155                if !url.path().ends_with(HTTP_TRACE_ENDPOINT) {
156                    let path = url.path().trim_end_matches('/');
157                    url.set_path(&format!("{}{}", path, HTTP_TRACE_ENDPOINT));
158                }
159            }
160            Self::Grpc => {
161                ensure!(
162                    !url.path().ends_with(HTTP_TRACE_ENDPOINT),
163                    "OTLP gRPC endpoint should not include {} path, got: {}",
164                    HTTP_TRACE_ENDPOINT,
165                    url
166                );
167            }
168        }
169        Ok(())
170    }
171}