Skip to main content

adk_telemetry/
init.rs

1//! Telemetry initialization and configuration
2
3use 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/// Error returned by telemetry initialization functions.
21#[derive(Debug, thiserror::Error)]
22pub enum TelemetryError {
23    /// Failed to build the tracing/OTLP pipeline.
24    #[error("telemetry init failed: {0}")]
25    Init(String),
26}
27
28/// Initialize basic telemetry with console logging.
29///
30/// # Arguments
31/// * `service_name` - Name of the service for trace identification
32///
33/// # Example
34/// ```
35/// use adk_telemetry::init_telemetry;
36/// init_telemetry("my-agent-service").expect("Failed to initialize telemetry");
37/// ```
38pub 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/// Initialize telemetry with OpenTelemetry OTLP export.
61///
62/// Enables distributed tracing by exporting spans to an OTLP collector.
63///
64/// # Arguments
65/// * `service_name` - Name of the service for trace identification
66/// * `endpoint` - OTLP collector endpoint (e.g., "http://localhost:4317")
67///
68/// # Example
69/// ```no_run
70/// use adk_telemetry::init_with_otlp;
71/// init_with_otlp("my-agent", "http://localhost:4317")
72///     .expect("Failed to initialize telemetry");
73/// ```
74#[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        // Build OTLP span exporter
91        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        // Build tracer provider with batch exporter
105        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        // Initialize metrics
114        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/// The tonic OTLP pipeline with a configuration hook on each exporter builder.
166///
167/// The hook is the seam that lets the `gcp` module inject TLS settings and a
168/// per-request auth interceptor without duplicating the exporter/provider
169/// plumbing shared with [`build_otlp_layer`].
170#[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    /// Configures a tonic exporter builder before it is built.
177    pub(crate) trait ExporterHook {
178        /// Returns the builder with hook-specific configuration applied.
179        fn configure<B: WithTonicConfig>(&self, builder: B) -> B;
180    }
181
182    /// Hook that leaves the builder unchanged — the plain-collector path.
183    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    /// Builds the OTLP span pipeline (exporter → batch tracer provider →
192    /// global registration) and returns a tracer for layer construction.
193    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/// Build an OTLP tracing layer without initializing a global subscriber.
219///
220/// Returns a boxed [`tracing_subscriber::Layer`] that can be composed with any
221/// subscriber via `.with()`. Also configures the global OpenTelemetry tracer
222/// and meter providers.
223///
224/// The layer is returned as `Box<dyn Layer<S>>` rather than `impl Layer` so it
225/// can be stored, composed across crate boundaries, and used in `Layered<...>`
226/// chains without running into opaque-type limitations.
227///
228/// Unlike [`init_with_otlp`], this function does **not** call `.init()` on a
229/// subscriber and does **not** use the `INIT` [`Once`] guard. The caller is
230/// responsible for composing the returned layer into their own subscriber stack.
231///
232/// # Arguments
233/// * `service_name` - Name of the service for trace identification
234/// * `endpoint` - OTLP collector endpoint (e.g., `"http://localhost:4317"`)
235///
236/// # Errors
237/// Returns [`TelemetryError::Init`] if the OTLP span or metric exporter fails to build.
238///
239/// # Example
240/// ```no_run
241/// use adk_telemetry::build_otlp_layer;
242/// use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
243///
244/// let otlp_layer = build_otlp_layer("my-agent", "http://localhost:4317")
245///     .expect("Failed to build OTLP layer");
246///
247/// tracing_subscriber::registry()
248///     .with(otlp_layer)
249///     .with(tracing_subscriber::fmt::layer())
250///     .init();
251/// ```
252#[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    // Build OTLP metric exporter
273    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
289/// Shutdown telemetry and flush any pending spans.
290///
291/// Should be called before application exit to ensure all telemetry data is sent.
292/// In OTel 0.28+, the tracer provider is shut down when the last reference is dropped.
293/// This function is kept for backward compatibility and explicitly drops the global provider.
294pub fn shutdown_telemetry() {
295    #[cfg(feature = "otlp")]
296    {
297        // In OTel 0.28, shutdown_tracer_provider() was removed.
298        // The SdkTracerProvider shuts down automatically when the last reference is dropped.
299        // We trigger this by replacing the global provider with a no-op, which drops the old one.
300        opentelemetry::global::set_tracer_provider(
301            opentelemetry::trace::noop::NoopTracerProvider::new(),
302        );
303    }
304}
305
306/// Initialize telemetry with ADK span exporter.
307///
308/// Creates a shared span exporter that can be used by both telemetry and the debug API.
309/// Returns the exporter so it can be passed to the debug controller.
310pub 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/// Initialize telemetry with direct SQLite span export — zero-infrastructure
362/// tracing with no collector or backend to deploy.
363///
364/// Spans are persisted to the database file at `db_path` (created if needed)
365/// by a background writer thread; the traced code path never blocks on I/O.
366/// Read them back with [`SqliteTraceReader`](crate::sqlite::SqliteTraceReader)
367/// or any SQLite client.
368///
369/// Returns the exporter so callers can [`flush`](crate::sqlite::SqliteSpanExporter::flush)
370/// before exiting (the subscriber keeps it alive for the process lifetime, so
371/// drop-based flushing never fires for globally installed subscribers).
372///
373/// # Example
374/// ```no_run
375/// use adk_telemetry::init_with_sqlite;
376///
377/// let exporter = init_with_sqlite("my-agent", "traces.db")
378///     .expect("Failed to initialize telemetry");
379/// // ... run the agent ...
380/// exporter.flush().ok();
381/// ```
382#[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    // Create the exporter (and surface db errors) before the irreversible
388    // global subscriber installation.
389    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}