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 = build_resource(otlp_config.service_name.clone());
42
43    let span_builder = SpanExporter::builder();
44
45    let span_exporter = match otlp_config.protocol {
46        OtlpProtocol::Http => {
47            span_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
48        }
49        OtlpProtocol::Grpc => {
50            span_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
51        }
52    };
53
54    let sampler = build_sampler(otlp_config.sample_ratio)?;
55
56    let tracer_provider = SdkTracerProvider::builder()
57        .with_resource(resource)
58        .with_sampler(sampler)
59        .with_batch_exporter(span_exporter)
60        .build();
61
62    global::set_tracer_provider(tracer_provider.clone());
63
64    let tracer = tracer_provider.tracer(otlp_config.service_name);
65    Ok(tracing_opentelemetry::layer().with_tracer(tracer))
66}
67
68/// Creates a tracing layer that exports logs to an OTLP endpoint.
69///
70/// This layer bridges logs emitted via the `tracing` crate to `OpenTelemetry` logs.
71#[cfg(feature = "otlp-logs")]
72pub fn log_layer(
73    otlp_config: OtlpLogsConfig,
74) -> eyre::Result<
75    opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge<
76        opentelemetry_sdk::logs::SdkLoggerProvider,
77        opentelemetry_sdk::logs::SdkLogger,
78    >,
79> {
80    use opentelemetry_otlp::LogExporter;
81    use opentelemetry_sdk::logs::SdkLoggerProvider;
82
83    let resource = build_resource(otlp_config.service_name.clone());
84
85    let log_builder = LogExporter::builder();
86
87    let log_exporter = match otlp_config.protocol {
88        OtlpProtocol::Http => {
89            log_builder.with_http().with_endpoint(otlp_config.endpoint.as_str()).build()?
90        }
91        OtlpProtocol::Grpc => {
92            log_builder.with_tonic().with_endpoint(otlp_config.endpoint.as_str()).build()?
93        }
94    };
95
96    let logger_provider = SdkLoggerProvider::builder()
97        .with_resource(resource)
98        .with_batch_exporter(log_exporter)
99        .build();
100
101    Ok(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(&logger_provider))
102}
103
104/// Configuration for OTLP trace export.
105#[derive(Debug, Clone)]
106pub struct OtlpConfig {
107    /// Service name for trace identification
108    service_name: String,
109    /// Otlp endpoint URL
110    endpoint: Url,
111    /// Transport protocol, HTTP or gRPC
112    protocol: OtlpProtocol,
113    /// Optional sampling ratio, from 0.0 to 1.0
114    sample_ratio: Option<f64>,
115}
116
117impl OtlpConfig {
118    /// Creates a new OTLP configuration.
119    pub fn new(
120        service_name: impl Into<String>,
121        endpoint: Url,
122        protocol: OtlpProtocol,
123        sample_ratio: Option<f64>,
124    ) -> eyre::Result<Self> {
125        if let Some(ratio) = sample_ratio {
126            ensure!(
127                (0.0..=1.0).contains(&ratio),
128                "Sample ratio must be between 0.0 and 1.0, got: {}",
129                ratio
130            );
131        }
132
133        Ok(Self { service_name: service_name.into(), endpoint, protocol, sample_ratio })
134    }
135
136    /// Returns the service name.
137    pub fn service_name(&self) -> &str {
138        &self.service_name
139    }
140
141    /// Returns the OTLP endpoint URL.
142    pub const fn endpoint(&self) -> &Url {
143        &self.endpoint
144    }
145
146    /// Returns the transport protocol.
147    pub const fn protocol(&self) -> OtlpProtocol {
148        self.protocol
149    }
150
151    /// Returns the sampling ratio.
152    pub const fn sample_ratio(&self) -> Option<f64> {
153        self.sample_ratio
154    }
155}
156
157/// Configuration for OTLP logs export.
158#[derive(Debug, Clone)]
159pub struct OtlpLogsConfig {
160    /// Service name for log identification
161    service_name: String,
162    /// Otlp endpoint URL
163    endpoint: Url,
164    /// Transport protocol, HTTP or gRPC
165    protocol: OtlpProtocol,
166}
167
168impl OtlpLogsConfig {
169    /// Creates a new OTLP logs configuration.
170    pub fn new(
171        service_name: impl Into<String>,
172        endpoint: Url,
173        protocol: OtlpProtocol,
174    ) -> eyre::Result<Self> {
175        Ok(Self { service_name: service_name.into(), endpoint, protocol })
176    }
177
178    /// Returns the service name.
179    pub fn service_name(&self) -> &str {
180        &self.service_name
181    }
182
183    /// Returns the OTLP endpoint URL.
184    pub const fn endpoint(&self) -> &Url {
185        &self.endpoint
186    }
187
188    /// Returns the transport protocol.
189    pub const fn protocol(&self) -> OtlpProtocol {
190        self.protocol
191    }
192}
193
194// Builds OTLP resource with service information.
195fn build_resource(service_name: impl Into<Value>) -> Resource {
196    Resource::builder()
197        .with_service_name(service_name)
198        .with_schema_url([KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))], SCHEMA_URL)
199        .build()
200}
201
202/// Builds the appropriate sampler based on the sample ratio.
203fn build_sampler(sample_ratio: Option<f64>) -> eyre::Result<Sampler> {
204    match sample_ratio {
205        // Default behavior: sample all traces
206        None | Some(1.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))),
207        // Don't sample anything
208        Some(0.0) => Ok(Sampler::ParentBased(Box::new(Sampler::AlwaysOff))),
209        // Sample based on trace ID ratio
210        Some(ratio) => Ok(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio)))),
211    }
212}
213
214/// OTLP transport protocol type
215#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
216pub enum OtlpProtocol {
217    /// HTTP/Protobuf transport, port 4318, requires `/v1/traces` path
218    Http,
219    /// gRPC transport, port 4317
220    Grpc,
221}
222
223impl OtlpProtocol {
224    /// Validate and correct the URL to match protocol requirements for traces.
225    ///
226    /// For HTTP: Ensures the path ends with `/v1/traces`, appending it if necessary.
227    /// For gRPC: Ensures the path does NOT include `/v1/traces`.
228    pub fn validate_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
229        self.validate_endpoint_with_path(url, HTTP_TRACE_ENDPOINT)
230    }
231
232    /// Validate and correct the URL to match protocol requirements for logs.
233    ///
234    /// For HTTP: Ensures the path ends with `/v1/logs`, appending it if necessary.
235    /// For gRPC: Ensures the path does NOT include `/v1/logs`.
236    pub fn validate_logs_endpoint(&self, url: &mut Url) -> eyre::Result<()> {
237        self.validate_endpoint_with_path(url, HTTP_LOGS_ENDPOINT)
238    }
239
240    fn validate_endpoint_with_path(&self, url: &mut Url, http_path: &str) -> eyre::Result<()> {
241        match self {
242            Self::Http => {
243                if !url.path().ends_with(http_path) {
244                    let path = url.path().trim_end_matches('/');
245                    url.set_path(&format!("{}{}", path, http_path));
246                }
247            }
248            Self::Grpc => {
249                ensure!(
250                    !url.path().ends_with(http_path),
251                    "OTLP gRPC endpoint should not include {} path, got: {}",
252                    http_path,
253                    url
254                );
255            }
256        }
257        Ok(())
258    }
259}