zlayer_observability/
tracing_otel.rs1use 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
17pub 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 if let Err(e) = provider.shutdown() {
27 tracing::warn!("Error shutting down tracer provider: {:?}", e);
28 }
29 }
30 }
31}
32
33pub 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 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 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 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 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
101pub 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}