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