Skip to main content

zlayer_observability/
tracing_otel.rs

1//! OpenTelemetry distributed tracing
2//!
3//! Provides trace context propagation and OTLP export for distributed systems.
4
5use opentelemetry::{global, trace::TracerProvider as _, KeyValue};
6use opentelemetry_otlp::WithExportConfig;
7use opentelemetry_sdk::{
8    trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
9    Resource,
10};
11use tracing_opentelemetry::OpenTelemetryLayer;
12use tracing_subscriber::Registry;
13
14use crate::config::TracingConfig;
15use crate::error::{ObservabilityError, Result};
16
17/// Guard that shuts down the tracer provider when dropped
18pub struct TracingGuard {
19    provider: Option<SdkTracerProvider>,
20}
21
22impl Drop for TracingGuard {
23    fn drop(&mut self) {
24        if let Some(ref provider) = self.provider {
25            // Use the provider's shutdown method directly (0.31+ API)
26            if let Err(e) = provider.shutdown() {
27                tracing::warn!("Error shutting down tracer provider: {:?}", e);
28            }
29        }
30    }
31}
32
33/// Initialize OpenTelemetry tracing
34///
35/// Returns a guard that must be held for the lifetime of the application.
36/// When dropped, it will flush pending traces and shut down the provider.
37///
38/// Note: This function does NOT set a global tracing subscriber. It only
39/// initializes the OpenTelemetry provider. Use `create_otel_layer()` to get
40/// a layer that can be combined with other tracing-subscriber layers.
41///
42/// # Errors
43/// Returns an error if tracing is enabled but no OTLP endpoint is configured,
44/// or if the OTLP exporter or tracer provider fails to initialize.
45pub fn init_tracing(config: &TracingConfig) -> Result<TracingGuard> {
46    if !config.enabled {
47        tracing::info!("OpenTelemetry tracing disabled");
48        return Ok(TracingGuard { provider: None });
49    }
50
51    let endpoint = config.otlp_endpoint.as_ref().ok_or_else(|| {
52        ObservabilityError::TracingInit(
53            "OTLP endpoint required when tracing is enabled".to_string(),
54        )
55    })?;
56
57    // Build the OTLP exporter (0.31+ API: with_tonic() first, then configure)
58    let exporter = opentelemetry_otlp::SpanExporter::builder()
59        .with_tonic()
60        .with_endpoint(endpoint)
61        .build()
62        .map_err(|e| ObservabilityError::TracingInit(e.to_string()))?;
63
64    // Build the sampler
65    let sampler = if config.sampling_ratio >= 1.0 {
66        Sampler::AlwaysOn
67    } else if config.sampling_ratio <= 0.0 {
68        Sampler::AlwaysOff
69    } else {
70        Sampler::TraceIdRatioBased(config.sampling_ratio)
71    };
72
73    // Build the tracer provider with batch exporter (0.31+ API)
74    let provider = SdkTracerProvider::builder()
75        .with_batch_exporter(exporter)
76        .with_sampler(sampler)
77        .with_id_generator(RandomIdGenerator::default())
78        .with_resource(
79            Resource::builder_empty()
80                .with_service_name(config.service_name.clone())
81                .with_attribute(KeyValue::new("service.version", env!("CARGO_PKG_VERSION")))
82                .build(),
83        )
84        .build();
85
86    // Set the global tracer provider
87    global::set_tracer_provider(provider.clone());
88
89    tracing::info!(
90        endpoint = %endpoint,
91        service_name = %config.service_name,
92        sampling_ratio = config.sampling_ratio,
93        "OpenTelemetry tracing initialized"
94    );
95
96    Ok(TracingGuard {
97        provider: Some(provider),
98    })
99}
100
101/// Create an OpenTelemetry layer that can be combined with other layers
102///
103/// Use this when you want to integrate with an existing tracing-subscriber setup.
104/// The returned layer can be combined with other layers using `Registry::with()`.
105///
106/// Returns `None` if tracing is disabled in the configuration.
107///
108/// # Errors
109/// Returns an error if tracing is enabled but no OTLP endpoint is configured,
110/// or if the OTLP exporter fails to build.
111pub fn create_otel_layer(
112    config: &TracingConfig,
113) -> Result<Option<OpenTelemetryLayer<Registry, opentelemetry_sdk::trace::Tracer>>> {
114    if !config.enabled {
115        return Ok(None);
116    }
117
118    let endpoint = config.otlp_endpoint.as_ref().ok_or_else(|| {
119        ObservabilityError::TracingInit(
120            "OTLP endpoint required when tracing is enabled".to_string(),
121        )
122    })?;
123
124    let exporter = opentelemetry_otlp::SpanExporter::builder()
125        .with_tonic()
126        .with_endpoint(endpoint)
127        .build()
128        .map_err(|e| ObservabilityError::TracingInit(e.to_string()))?;
129
130    let sampler = if config.sampling_ratio >= 1.0 {
131        Sampler::AlwaysOn
132    } else if config.sampling_ratio <= 0.0 {
133        Sampler::AlwaysOff
134    } else {
135        Sampler::TraceIdRatioBased(config.sampling_ratio)
136    };
137
138    let provider = SdkTracerProvider::builder()
139        .with_batch_exporter(exporter)
140        .with_sampler(sampler)
141        .with_id_generator(RandomIdGenerator::default())
142        .with_resource(
143            Resource::builder_empty()
144                .with_service_name(config.service_name.clone())
145                .build(),
146        )
147        .build();
148
149    let tracer = provider.tracer("zlayer");
150    global::set_tracer_provider(provider);
151
152    Ok(Some(OpenTelemetryLayer::new(tracer)))
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_disabled_tracing() {
161        let config = TracingConfig {
162            enabled: false,
163            ..Default::default()
164        };
165
166        let guard = init_tracing(&config).unwrap();
167        assert!(guard.provider.is_none());
168    }
169
170    #[test]
171    fn test_enabled_without_endpoint_fails() {
172        let config = TracingConfig {
173            enabled: true,
174            otlp_endpoint: None,
175            ..Default::default()
176        };
177
178        let result = init_tracing(&config);
179        assert!(result.is_err());
180    }
181
182    #[test]
183    fn test_create_layer_disabled() {
184        let config = TracingConfig {
185            enabled: false,
186            ..Default::default()
187        };
188
189        let layer = create_otel_layer(&config).unwrap();
190        assert!(layer.is_none());
191    }
192
193    #[test]
194    fn test_create_layer_without_endpoint_fails() {
195        let config = TracingConfig {
196            enabled: true,
197            otlp_endpoint: None,
198            ..Default::default()
199        };
200
201        let result = create_otel_layer(&config);
202        assert!(result.is_err());
203    }
204}