1use std::sync::{
4 Arc, Once, OnceLock,
5 atomic::{AtomicBool, Ordering},
6};
7use tracing_subscriber::{
8 EnvFilter,
9 filter::filter_fn,
10 layer::{Layer, SubscriberExt},
11 util::SubscriberInitExt,
12};
13
14use crate::span_exporter::{AdkSpanExporter, AdkSpanLayer, is_runtime_span};
15
16pub(crate) static INIT: Once = Once::new();
17static ADK_EXPORTER: OnceLock<Arc<AdkSpanExporter>> = OnceLock::new();
18static ADK_EXPORTER_INSTALLED: AtomicBool = AtomicBool::new(false);
19
20#[derive(Debug, thiserror::Error)]
22pub enum TelemetryError {
23 #[error("telemetry init failed: {0}")]
25 Init(String),
26}
27
28pub fn init_telemetry(service_name: &str) -> Result<(), TelemetryError> {
39 INIT.call_once(|| {
40 let filter = EnvFilter::try_from_default_env()
41 .or_else(|_| EnvFilter::try_new("info"))
42 .unwrap_or_else(|_| EnvFilter::new("info"));
43
44 tracing_subscriber::registry()
45 .with(filter)
46 .with(
47 tracing_subscriber::fmt::layer()
48 .with_target(true)
49 .with_thread_ids(true)
50 .with_line_number(true),
51 )
52 .init();
53
54 tracing::info!(service.name = service_name, "telemetry initialized");
55 });
56
57 Ok(())
58}
59
60#[cfg(feature = "otlp")]
75pub fn init_with_otlp(service_name: &str, endpoint: &str) -> Result<(), TelemetryError> {
76 use opentelemetry::trace::TracerProvider;
77 use opentelemetry_otlp::WithExportConfig;
78 use tracing_opentelemetry::OpenTelemetryLayer;
79
80 let endpoint = endpoint.to_string();
81 let service_name = service_name.to_string();
82
83 let init_error: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
84
85 INIT.call_once(|| {
86 let resource = opentelemetry_sdk::Resource::builder_empty()
87 .with_attributes([opentelemetry::KeyValue::new("service.name", service_name.clone())])
88 .build();
89
90 let span_exporter = match opentelemetry_otlp::SpanExporter::builder()
92 .with_tonic()
93 .with_endpoint(&endpoint)
94 .build()
95 {
96 Ok(e) => e,
97 Err(e) => {
98 *init_error.lock().unwrap_or_else(|p| p.into_inner()) =
99 Some(format!("failed to build OTLP span exporter: {e}"));
100 return;
101 }
102 };
103
104 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
106 .with_batch_exporter(span_exporter)
107 .with_resource(resource.clone())
108 .build();
109
110 let tracer = tracer_provider.tracer("adk-telemetry");
111 opentelemetry::global::set_tracer_provider(tracer_provider);
112
113 let metric_exporter = match opentelemetry_otlp::MetricExporter::builder()
115 .with_tonic()
116 .with_endpoint(&endpoint)
117 .build()
118 {
119 Ok(e) => e,
120 Err(e) => {
121 *init_error.lock().unwrap_or_else(|p| p.into_inner()) =
122 Some(format!("failed to build OTLP metric exporter: {e}"));
123 return;
124 }
125 };
126
127 let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
128 .with_periodic_exporter(metric_exporter)
129 .with_resource(resource)
130 .build();
131
132 opentelemetry::global::set_meter_provider(meter_provider);
133
134 let telemetry_layer = OpenTelemetryLayer::new(tracer);
135
136 let filter = EnvFilter::try_from_default_env()
137 .or_else(|_| EnvFilter::try_new("info"))
138 .unwrap_or_else(|_| EnvFilter::new("info"));
139
140 tracing_subscriber::registry()
141 .with(
142 tracing_subscriber::fmt::layer()
143 .with_target(true)
144 .with_thread_ids(true)
145 .with_line_number(true)
146 .with_filter(filter),
147 )
148 .with(telemetry_layer)
149 .init();
150
151 tracing::info!(
152 service.name = service_name,
153 otlp.endpoint = %endpoint,
154 "telemetry initialized with OpenTelemetry"
155 );
156 });
157
158 if let Some(err) = init_error.lock().unwrap_or_else(|p| p.into_inner()).take() {
159 return Err(TelemetryError::Init(err));
160 }
161
162 Ok(())
163}
164
165#[cfg(feature = "otlp")]
171pub(crate) mod otlp_pipeline {
172 use super::TelemetryError;
173 use opentelemetry::trace::TracerProvider;
174 use opentelemetry_otlp::{WithExportConfig, WithTonicConfig};
175
176 pub(crate) trait ExporterHook {
178 fn configure<B: WithTonicConfig>(&self, builder: B) -> B;
180 }
181
182 pub(crate) struct NoopHook;
184
185 impl ExporterHook for NoopHook {
186 fn configure<B: WithTonicConfig>(&self, builder: B) -> B {
187 builder
188 }
189 }
190
191 pub(crate) fn build_tracer<H: ExporterHook>(
194 resource: opentelemetry_sdk::Resource,
195 endpoint: &str,
196 hook: &H,
197 ) -> Result<opentelemetry_sdk::trace::SdkTracer, TelemetryError> {
198 let span_exporter = hook
199 .configure(
200 opentelemetry_otlp::SpanExporter::builder().with_tonic().with_endpoint(endpoint),
201 )
202 .build()
203 .map_err(|e| {
204 TelemetryError::Init(format!("failed to build OTLP span exporter: {e}"))
205 })?;
206
207 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
208 .with_batch_exporter(span_exporter)
209 .with_resource(resource)
210 .build();
211
212 let tracer = tracer_provider.tracer("adk-telemetry");
213 opentelemetry::global::set_tracer_provider(tracer_provider);
214 Ok(tracer)
215 }
216}
217
218#[cfg(feature = "otlp")]
253pub fn build_otlp_layer<S>(
254 service_name: &str,
255 endpoint: &str,
256) -> Result<Box<dyn tracing_subscriber::Layer<S> + Send + Sync>, TelemetryError>
257where
258 S: tracing::Subscriber
259 + for<'span> tracing_subscriber::registry::LookupSpan<'span>
260 + Send
261 + Sync,
262{
263 use opentelemetry_otlp::WithExportConfig;
264 use tracing_opentelemetry::OpenTelemetryLayer;
265
266 let resource = opentelemetry_sdk::Resource::builder_empty()
267 .with_attributes([opentelemetry::KeyValue::new("service.name", service_name.to_string())])
268 .build();
269
270 let tracer = otlp_pipeline::build_tracer(resource.clone(), endpoint, &otlp_pipeline::NoopHook)?;
271
272 let metric_exporter = opentelemetry_otlp::MetricExporter::builder()
274 .with_tonic()
275 .with_endpoint(endpoint)
276 .build()
277 .map_err(|e| TelemetryError::Init(format!("failed to build OTLP metric exporter: {e}")))?;
278
279 let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
280 .with_periodic_exporter(metric_exporter)
281 .with_resource(resource)
282 .build();
283
284 opentelemetry::global::set_meter_provider(meter_provider);
285
286 Ok(Box::new(OpenTelemetryLayer::new(tracer)))
287}
288
289pub fn shutdown_telemetry() {
295 #[cfg(feature = "otlp")]
296 {
297 opentelemetry::global::set_tracer_provider(
301 opentelemetry::trace::noop::NoopTracerProvider::new(),
302 );
303 }
304}
305
306pub fn init_with_adk_exporter(service_name: &str) -> Result<Arc<AdkSpanExporter>, TelemetryError> {
311 initialize_adk_exporter_with(&INIT, &ADK_EXPORTER, &ADK_EXPORTER_INSTALLED, |exporter| {
312 let filter = EnvFilter::try_from_default_env()
313 .or_else(|_| EnvFilter::try_new("info"))
314 .unwrap_or_else(|_| EnvFilter::new("info"));
315
316 let adk_layer = AdkSpanLayer::new(exporter).with_filter(filter_fn(|metadata| {
317 metadata.is_span() && is_runtime_span(metadata.name())
318 }));
319
320 tracing_subscriber::registry()
321 .with(
322 tracing_subscriber::fmt::layer()
323 .with_target(true)
324 .with_thread_ids(true)
325 .with_line_number(true)
326 .with_filter(filter),
327 )
328 .with(adk_layer)
329 .init();
330
331 tracing::info!(service.name = service_name, "telemetry initialized with ADK span exporter");
332 })
333}
334
335fn initialize_adk_exporter_with<F>(
336 init: &Once,
337 exporter_cell: &OnceLock<Arc<AdkSpanExporter>>,
338 installed: &AtomicBool,
339 install: F,
340) -> Result<Arc<AdkSpanExporter>, TelemetryError>
341where
342 F: FnOnce(Arc<AdkSpanExporter>),
343{
344 let exporter = exporter_cell.get_or_init(|| Arc::new(AdkSpanExporter::new())).clone();
345 init.call_once(|| {
346 install(exporter.clone());
347 installed.store(true, Ordering::Release);
348 });
349
350 if installed.load(Ordering::Acquire) {
351 Ok(exporter)
352 } else {
353 Err(TelemetryError::Init(
354 "global telemetry was already initialized without the ADK in-process exporter; \
355 initialize the ADK exporter before other global telemetry modes"
356 .to_string(),
357 ))
358 }
359}
360
361#[cfg(feature = "sqlite")]
383pub fn init_with_sqlite(
384 service_name: &str,
385 db_path: impl AsRef<std::path::Path>,
386) -> Result<Arc<crate::sqlite::SqliteSpanExporter>, TelemetryError> {
387 let exporter = Arc::new(crate::sqlite::SqliteSpanExporter::new(db_path)?);
390 let exporter_clone = exporter.clone();
391
392 INIT.call_once(|| {
393 let filter = EnvFilter::try_from_default_env()
394 .or_else(|_| EnvFilter::try_new("info"))
395 .unwrap_or_else(|_| EnvFilter::new("info"));
396
397 let adk_layer = AdkSpanLayer::new(exporter_clone).with_filter(filter_fn(|metadata| {
398 metadata.is_span() && is_runtime_span(metadata.name())
399 }));
400
401 tracing_subscriber::registry()
402 .with(
403 tracing_subscriber::fmt::layer()
404 .with_target(true)
405 .with_thread_ids(true)
406 .with_line_number(true)
407 .with_filter(filter),
408 )
409 .with(adk_layer)
410 .init();
411
412 tracing::info!(
413 service.name = service_name,
414 "telemetry initialized with SQLite span exporter"
415 );
416 });
417
418 Ok(exporter)
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use std::sync::atomic::AtomicUsize;
425
426 #[test]
427 fn repeated_adk_initialization_reuses_the_registered_exporter() {
428 let init = Once::new();
429 let exporter = OnceLock::new();
430 let installed = AtomicBool::new(false);
431 let installations = AtomicUsize::new(0);
432
433 let first = initialize_adk_exporter_with(&init, &exporter, &installed, |_| {
434 installations.fetch_add(1, Ordering::Relaxed);
435 })
436 .unwrap();
437 let second = initialize_adk_exporter_with(&init, &exporter, &installed, |_| {
438 installations.fetch_add(1, Ordering::Relaxed);
439 })
440 .unwrap();
441
442 assert!(Arc::ptr_eq(&first, &second));
443 assert_eq!(installations.load(Ordering::Relaxed), 1);
444 }
445
446 #[test]
447 fn adk_initialization_rejects_an_incompatible_existing_global_mode() {
448 let init = Once::new();
449 init.call_once(|| {});
450 let exporter = OnceLock::new();
451 let installed = AtomicBool::new(false);
452
453 let error = initialize_adk_exporter_with(&init, &exporter, &installed, |_| {})
454 .expect_err("an earlier telemetry mode must not return a disconnected exporter");
455
456 assert!(error.to_string().contains("already initialized without the ADK"));
457 }
458}