Skip to main content

reth_tracing_otlp/
lib.rs

1#![cfg(feature = "otlp")]
2
3//! Provides tracing layers for `OpenTelemetry` that export spans, logs, and metrics to an OTLP
4//! endpoint.
5//!
6//! This module simplifies the integration of `OpenTelemetry` with OTLP export in Rust
7//! applications. It allows for easily capturing and exporting distributed traces, logs,
8//! and metrics to compatible backends like Jaeger, Zipkin, or any other
9//! OpenTelemetry-compatible system.
10
11use clap::ValueEnum;
12use eyre::ensure;
13use opentelemetry::{global, trace::TracerProvider, KeyValue, Value};
14use opentelemetry_otlp::{SpanExporter, WithExportConfig};
15use opentelemetry_sdk::{
16    propagation::TraceContextPropagator,
17    trace::{Sampler, SdkTracer, SdkTracerProvider},
18    Resource,
19};
20use opentelemetry_semantic_conventions::{attribute::SERVICE_VERSION, SCHEMA_URL};
21use tracing::Subscriber;
22use tracing_opentelemetry::OpenTelemetryLayer;
23use tracing_subscriber::registry::LookupSpan;
24use url::Url;
25
26// Otlp http endpoint is expected to end with this path.
27// See also <https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_traces_endpoint>.
28const HTTP_TRACE_ENDPOINT: &str = "/v1/traces";
29const HTTP_LOGS_ENDPOINT: &str = "/v1/logs";
30
31/// Creates a tracing [`OpenTelemetryLayer`] that exports spans to an OTLP endpoint.
32///
33/// This layer can be added to a [`tracing_subscriber::Registry`] to enable `OpenTelemetry` tracing
34/// with OTLP export to an url.
35pub fn span_layer<S>(otlp_config: OtlpConfig) -> eyre::Result<OpenTelemetryLayer<S, SdkTracer>>
36where
37    for<'span> S: Subscriber + LookupSpan<'span>,
38{
39    global::set_text_map_propagator(TraceContextPropagator::new());
40
41    let resource =
42        build_resource(otlp_config.service_name.clone(), otlp_config.service_version.as_deref());
43
44    let span_builder = SpanExporter::builder();
45
46    let span_exporter = match otlp_config.protocol {
47        OtlpProtocol::Http => {
48            span_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
49        }
50        OtlpProtocol::Grpc => {
51            span_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
52        }
53    };
54
55    let sampler = build_sampler(otlp_config.sample_ratio)?;
56
57    let tracer_provider = SdkTracerProvider::builder()
58        .with_resource(resource)
59        .with_sampler(sampler)
60        .with_batch_exporter(span_exporter)
61        .build();
62
63    global::set_tracer_provider(tracer_provider.clone());
64
65    let tracer = tracer_provider.tracer(otlp_config.service_name);
66    Ok(tracing_opentelemetry::layer()
67        .with_tracer(tracer)
68        .with_location(false)
69        .with_tracked_inactivity(false)
70        .with_target(false)
71        .with_threads(false))
72}
73
74/// Creates a tracing layer that exports logs to an OTLP endpoint.
75///
76/// This layer bridges logs emitted via the `tracing` crate to `OpenTelemetry` logs.
77#[cfg(feature = "otlp-logs")]
78pub fn log_layer(
79    otlp_config: OtlpLogsConfig,
80) -> eyre::Result<
81    opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge<
82        opentelemetry_sdk::logs::SdkLoggerProvider,
83        opentelemetry_sdk::logs::SdkLogger,
84    >,
85> {
86    use opentelemetry_otlp::LogExporter;
87    use opentelemetry_sdk::logs::SdkLoggerProvider;
88
89    let resource =
90        build_resource(otlp_config.service_name.clone(), otlp_config.service_version.as_deref());
91
92    let log_builder = LogExporter::builder();
93
94    let log_exporter = match otlp_config.protocol {
95        OtlpProtocol::Http => {
96            log_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
97        }
98        OtlpProtocol::Grpc => {
99            log_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
100        }
101    };
102
103    let logger_provider = SdkLoggerProvider::builder()
104        .with_resource(resource)
105        .with_batch_exporter(log_exporter)
106        .build();
107
108    Ok(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(&logger_provider))
109}
110
111/// Configuration for OTLP trace export.
112#[derive(Debug, Clone)]
113pub struct OtlpConfig {
114    /// Service name for trace identification
115    service_name: String,
116    /// Optional service version override. Falls back to `CARGO_PKG_VERSION` if `None`.
117    service_version: Option<String>,
118    /// Otlp endpoint URL
119    endpoint: Url,
120    /// Transport protocol, HTTP or gRPC
121    protocol: OtlpProtocol,
122    /// Optional sampling ratio, from 0.0 to 1.0
123    sample_ratio: Option<f64>,
124}
125
126impl OtlpConfig {
127    /// Creates a new OTLP configuration.
128    pub fn new(
129        service_name: impl Into<String>,
130        endpoint: Url,
131        protocol: OtlpProtocol,
132        sample_ratio: Option<f64>,
133    ) -> eyre::Result<Self> {
134        if let Some(ratio) = sample_ratio {
135            ensure!(
136                (0.0..=1.0).contains(&ratio),
137                "Sample ratio must be between 0.0 and 1.0, got: {}",
138                ratio
139            );
140        }
141
142        Ok(Self {
143            service_name: service_name.into(),
144            service_version: None,
145            endpoint,
146            protocol,
147            sample_ratio,
148        })
149    }
150
151    /// Sets the service version for OTLP resource identification.
152    pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
153        self.service_version = Some(version.into());
154        self
155    }
156
157    /// Returns the service name.
158    pub fn service_name(&self) -> &str {
159        &self.service_name
160    }
161
162    /// Returns the OTLP endpoint URL.
163    pub const fn endpoint(&self) -> &Url {
164        &self.endpoint
165    }
166
167    /// Returns the transport protocol.
168    pub const fn protocol(&self) -> OtlpProtocol {
169        self.protocol
170    }
171
172    /// Returns the sampling ratio.
173    pub const fn sample_ratio(&self) -> Option<f64> {
174        self.sample_ratio
175    }
176}
177
178/// Configuration for OTLP logs export.
179#[derive(Debug, Clone)]
180pub struct OtlpLogsConfig {
181    /// Service name for log identification
182    service_name: String,
183    /// Optional service version override. Falls back to `CARGO_PKG_VERSION` if `None`.
184    service_version: Option<String>,
185    /// Otlp endpoint URL
186    endpoint: Url,
187    /// Transport protocol, HTTP or gRPC
188    protocol: OtlpProtocol,
189}
190
191impl OtlpLogsConfig {
192    /// Creates a new OTLP logs configuration.
193    pub fn new(
194        service_name: impl Into<String>,
195        endpoint: Url,
196        protocol: OtlpProtocol,
197    ) -> eyre::Result<Self> {
198        Ok(Self { service_name: service_name.into(), service_version: None, endpoint, protocol })
199    }
200
201    /// Sets the service version for OTLP resource identification.
202    pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
203        self.service_version = Some(version.into());
204        self
205    }
206
207    /// Returns the service name.
208    pub fn service_name(&self) -> &str {
209        &self.service_name
210    }
211
212    /// Returns the OTLP endpoint URL.
213    pub const fn endpoint(&self) -> &Url {
214        &self.endpoint
215    }
216
217    /// Returns the transport protocol.
218    pub const fn protocol(&self) -> OtlpProtocol {
219        self.protocol
220    }
221}
222
223// Builds OTLP resource with service information.
224fn build_resource(service_name: impl Into<Value>, service_version: Option<&str>) -> Resource {
225    let version = service_version.unwrap_or(env!("CARGO_PKG_VERSION"));
226    Resource::builder()
227        .with_service_name(service_name)
228        .with_schema_url([KeyValue::new(SERVICE_VERSION, version.to_string())], SCHEMA_URL)
229        .build()
230}
231
232/// Builds the appropriate sampler based on the sample ratio.
233fn build_sampler(sample_ratio: Option<f64>) -> eyre::Result<Sampler> {
234    match sample_ratio {
235        // Default behavior: sample all traces
236        None | Some(1.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))),
237        // Don't sample anything
238        Some(0.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOff))),
239        // Sample based on trace ID ratio
240        Some(ratio) => Ok(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio)))),
241    }
242}
243
244/// OTLP transport protocol type
245#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
246pub enum OtlpProtocol {
247    /// HTTP/Protobuf transport, port 4318, requires `/v1/traces` path
248    Http,
249    /// gRPC transport, port 4317
250    Grpc,
251}
252
253impl OtlpProtocol {
254    /// Validate and correct the URL to match protocol requirements for traces.
255    ///
256    /// For HTTP: Ensures the path ends with `/v1/traces`, appending it if necessary.
257    /// For gRPC: Ensures the path does NOT include `/v1/traces`.
258    pub fn validate_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
259        self.validate_endpoint_with_path(url, HTTP_TRACE_ENDPOINT)
260    }
261
262    /// Validate and correct the URL to match protocol requirements for logs.
263    ///
264    /// For HTTP: Ensures the path ends with `/v1/logs`, appending it if necessary.
265    /// For gRPC: Ensures the path does NOT include `/v1/logs`.
266    pub fn validate_logs_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
267        self.validate_endpoint_with_path(url, HTTP_LOGS_ENDPOINT)
268    }
269
270    fn validate_endpoint_with_path(&self, url: &mut Url, http_path: &str) -> eyre::Result<()> {
271        match self {
272            Self::Http => {
273                if !url.path().ends_with(http_path) {
274                    let path = url.path().trim_end_matches('/');
275                    url.set_path(&format!("{}{}", path, http_path));
276                }
277            }
278            Self::Grpc => {
279                ensure!(
280                    !url.path().ends_with(http_path),
281                    "OTLP gRPC endpoint should not include {} path, got: {}",
282                    http_path,
283                    url
284                );
285            }
286        }
287        Ok(())
288    }
289}