Skip to main content

apollo_opentelemetry/
telemetry.rs

1//! Core telemetry management.
2//!
3//! This module provides the [`Telemetry`] struct for unified management of
4//! OpenTelemetry traces, metrics, and logs providers.
5//!
6//! # Getting Started
7//!
8//! Use [`Telemetry::builder()`] to create a telemetry instance with custom
9//! configuration, or [`Telemetry::new()`] for configuration-only setup.
10//!
11//! # Example
12//!
13//! ```no_run
14//! use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
15//!
16//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! // Create from configuration
18//! let telemetry = Telemetry::new(OpenTelemetryConfig::default())?;
19//!
20//! // Or use builder for custom setup with all global registrations
21//! let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
22//!     .with_globals()
23//!     .build()?;
24//! # Ok(())
25//! # }
26//! ```
27
28use opentelemetry::global;
29use opentelemetry_appender_log::OpenTelemetryLogBridge;
30use opentelemetry_sdk::Resource;
31use opentelemetry_sdk::logs::{LoggerProviderBuilder, SdkLoggerProvider};
32use opentelemetry_sdk::metrics::{Instrument, MeterProviderBuilder, SdkMeterProvider, Stream};
33use opentelemetry_sdk::trace::{
34    SdkTracerProvider, SpanExporter, SpanProcessor, TracerProviderBuilder,
35};
36use tracing_subscriber::layer::SubscriberExt;
37use tracing_subscriber::util::SubscriberInitExt;
38
39use crate::config::OpenTelemetryConfig;
40use crate::{InitError, providers};
41use tokio::runtime::RuntimeFlavor;
42
43/// Builder for configuring and creating a [`Telemetry`] instance.
44///
45/// Use this builder to combine configuration-based setup with programmatic
46/// customization. Exporters and processors added via the builder are used
47/// in addition to those specified in the configuration.
48///
49/// You can register telemetry providers globally using:
50/// - [`with_global_tracer_provider()`](TelemetryBuilder::with_global_tracer_provider)
51/// - [`with_global_meter_provider()`](TelemetryBuilder::with_global_meter_provider)
52/// - [`with_log_bridge()`](TelemetryBuilder::with_log_bridge)
53/// - [`with_tracing_bridge()`](TelemetryBuilder::with_tracing_bridge)
54/// - [`with_global_propagator()`](TelemetryBuilder::with_global_propagator)
55///
56/// Or use [`with_globals()`](TelemetryBuilder::with_globals) to enable all of them at once.
57///
58/// # Example
59///
60/// ```no_run
61/// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
62///
63/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
64/// let config = OpenTelemetryConfig::default();
65/// let telemetry = Telemetry::builder(config).with_globals().build()?;
66/// # Ok(())
67/// # }
68/// ```
69pub struct TelemetryBuilder {
70    config: OpenTelemetryConfig,
71    set_global_tracer: bool,
72    set_global_meter: bool,
73    set_global_propagator: bool,
74    enable_log_bridge: bool,
75    enable_tracing_bridge: bool,
76    resource_builder: Option<opentelemetry_sdk::resource::ResourceBuilder>,
77    tracer_builder: Option<TracerProviderBuilder>,
78    meter_builder: Option<MeterProviderBuilder>,
79    logger_builder: Option<LoggerProviderBuilder>,
80}
81
82impl TelemetryBuilder {
83    /// Create a new builder with the given configuration.
84    ///
85    /// By default, nothing is registered globally. Use [`with_globals()`](Self::with_globals)
86    /// to opt in to all global registrations, or individual methods to opt in selectively.
87    pub fn new(config: OpenTelemetryConfig) -> Self {
88        Self {
89            config,
90            set_global_tracer: false,
91            set_global_meter: false,
92            set_global_propagator: false,
93            enable_log_bridge: false,
94            enable_tracing_bridge: false,
95            resource_builder: None,
96            tracer_builder: None,
97            meter_builder: None,
98            logger_builder: None,
99        }
100    }
101
102    fn tracer_builder(&mut self) -> TracerProviderBuilder {
103        self.tracer_builder
104            .take()
105            .unwrap_or_else(SdkTracerProvider::builder)
106    }
107
108    fn meter_builder(&mut self) -> MeterProviderBuilder {
109        self.meter_builder
110            .take()
111            .unwrap_or_else(SdkMeterProvider::builder)
112    }
113
114    fn logger_builder(&mut self) -> LoggerProviderBuilder {
115        self.logger_builder
116            .take()
117            .unwrap_or_else(SdkLoggerProvider::builder)
118    }
119
120    /// Register the tracer provider as the global tracer provider.
121    ///
122    /// When enabled, you can use `opentelemetry::global::tracer()` to obtain
123    /// tracers anywhere in your application.
124    ///
125    /// # Example
126    ///
127    /// ```no_run
128    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
129    /// use opentelemetry::global;
130    ///
131    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
132    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
133    ///     .with_global_tracer_provider()
134    ///     .build()?;
135    ///
136    /// // Now you can get tracers from anywhere
137    /// let tracer = global::tracer("my-component");
138    /// # Ok(())
139    /// # }
140    /// ```
141    pub fn with_global_tracer_provider(mut self) -> Self {
142        self.set_global_tracer = true;
143        self
144    }
145
146    /// Register the meter provider as the global meter provider.
147    ///
148    /// When enabled, you can use `opentelemetry::global::meter()` to obtain
149    /// meters anywhere in your application.
150    ///
151    /// # Example
152    ///
153    /// ```no_run
154    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
155    /// use opentelemetry::global;
156    ///
157    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
158    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
159    ///     .with_global_meter_provider()
160    ///     .build()?;
161    ///
162    /// // Now you can get meters from anywhere
163    /// let meter = global::meter("my-component");
164    /// # Ok(())
165    /// # }
166    /// ```
167    pub fn with_global_meter_provider(mut self) -> Self {
168        self.set_global_meter = true;
169        self
170    }
171
172    /// Register the propagator configuration as the global text map propagator.
173    ///
174    /// When enabled, tower middleware such as `http_server_propagation()` and
175    /// `http_client_propagation()` automatically use the configured propagator.
176    pub fn with_global_propagator(mut self) -> Self {
177        self.set_global_propagator = true;
178        self
179    }
180
181    /// Enable all global registrations at once.
182    ///
183    /// Equivalent to calling [`with_global_tracer_provider()`](Self::with_global_tracer_provider),
184    /// [`with_global_meter_provider()`](Self::with_global_meter_provider),
185    /// [`with_log_bridge()`](Self::with_log_bridge),
186    /// [`with_tracing_bridge()`](Self::with_tracing_bridge), and
187    /// [`with_global_propagator()`](Self::with_global_propagator).
188    ///
189    /// # Example
190    ///
191    /// ```no_run
192    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
193    ///
194    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
195    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
196    ///     .with_globals()
197    ///     .build()?;
198    /// # Ok(())
199    /// # }
200    /// ```
201    pub fn with_globals(self) -> Self {
202        self.with_global_tracer_provider()
203            .with_global_meter_provider()
204            .with_log_bridge()
205            .with_tracing_bridge()
206            .with_global_propagator()
207    }
208
209    /// Enable the log bridge to route `log` crate macros to OpenTelemetry.
210    ///
211    /// When enabled, calls to `log::info!()`, `log::error!()`, etc. will be
212    /// exported as OpenTelemetry log records through the configured logger provider.
213    ///
214    /// The bridge respects the `RUST_LOG` environment variable for filtering.
215    /// If `RUST_LOG` is not set, defaults to `info` level.
216    ///
217    /// **Note**: This sets the global logger, so it can only be called once per process.
218    /// Calling `build()` will fail if another logger is already installed.
219    ///
220    /// # Example
221    ///
222    /// ```no_run
223    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
224    ///
225    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
226    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
227    ///     .with_log_bridge()
228    ///     .build()?;
229    ///
230    /// // Now log macros are routed to OpenTelemetry
231    /// log::info!("This goes to OpenTelemetry");
232    /// # Ok(())
233    /// # }
234    /// ```
235    pub fn with_log_bridge(mut self) -> Self {
236        self.enable_log_bridge = true;
237        self
238    }
239
240    /// Enable the tracing bridge to route `tracing` crate spans and events to OpenTelemetry.
241    ///
242    /// When enabled, `tracing::info!()`, `tracing::span!()`, etc. will be exported
243    /// as OpenTelemetry log records and traces through the configured providers.
244    ///
245    /// The bridge respects the `RUST_LOG` environment variable for filtering.
246    /// If `RUST_LOG` is not set, defaults to `info` level.
247    ///
248    /// **Note**: This sets the global tracing subscriber, so it can only be called once
249    /// per process. Calling `build()` will fail if another subscriber is already installed.
250    ///
251    /// # Example
252    ///
253    /// ```ignore
254    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
255    ///
256    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::logs_to_stderr())
257    ///     .with_tracing_bridge()
258    ///     .build()?;
259    ///
260    /// // Now tracing macros are routed to OpenTelemetry
261    /// tracing::info!("This goes to OpenTelemetry");
262    /// ```
263    pub fn with_tracing_bridge(mut self) -> Self {
264        self.enable_tracing_bridge = true;
265        self
266    }
267
268    // --- Resource configuration ---
269
270    /// Set the resource builder for configuring resource attributes.
271    ///
272    /// Resource attributes provide identifying information about the entity
273    /// producing telemetry (e.g., service name, version, deployment environment).
274    ///
275    /// Attributes from the builder are merged with any resource configuration
276    /// from the config file. On conflicts, builder values take precedence,
277    /// allowing you to override specific attributes like `service.name` while
278    /// keeping other config-defined attributes.
279    ///
280    /// # Example
281    ///
282    /// ```no_run
283    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
284    /// use opentelemetry_sdk::Resource;
285    /// use opentelemetry::KeyValue;
286    ///
287    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
288    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
289    ///     .with_resource_builder(
290    ///         Resource::builder()
291    ///             .with_service_name("my-service")
292    ///             .with_attributes([
293    ///                 KeyValue::new("service.version", "1.0.0"),
294    ///                 KeyValue::new("deployment.environment", "production"),
295    ///             ])
296    ///     )
297    ///     .build()?;
298    /// # Ok(())
299    /// # }
300    /// ```
301    pub fn with_resource_builder(
302        mut self,
303        builder: opentelemetry_sdk::resource::ResourceBuilder,
304    ) -> Self {
305        self.resource_builder = Some(builder);
306        self
307    }
308
309    // --- Tracer configuration ---
310
311    /// Add a custom span processor to the tracer provider.
312    ///
313    /// Span processors receive spans as they are started and ended, allowing
314    /// for custom processing logic such as filtering, enrichment, or routing
315    /// to multiple exporters.
316    ///
317    /// This is added in addition to any processors configured via the
318    /// configuration file.
319    pub fn with_span_processor<T: SpanProcessor + 'static>(mut self, processor: T) -> Self {
320        self.tracer_builder = Some(self.tracer_builder().with_span_processor(processor));
321        self
322    }
323
324    /// Add a span exporter with batch processing.
325    ///
326    /// Batch processing buffers spans and exports them in batches, which is
327    /// more efficient for production use. This is the recommended approach
328    /// for most applications.
329    ///
330    /// This is added in addition to any exporters configured via the
331    /// configuration file.
332    ///
333    /// # Example
334    ///
335    /// ```ignore
336    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
337    /// use opentelemetry_otlp::SpanExporter;
338    ///
339    /// let exporter = SpanExporter::builder()
340    ///     .with_http()
341    ///     .with_endpoint("http://localhost:4318")
342    ///     .build()
343    ///     .unwrap();
344    ///
345    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
346    ///     .with_batch_span_exporter(exporter)
347    ///     .build()
348    ///     .unwrap();
349    /// ```
350    pub fn with_batch_span_exporter<T: SpanExporter + 'static>(mut self, exporter: T) -> Self {
351        self.tracer_builder = Some(self.tracer_builder().with_batch_exporter(exporter));
352        self
353    }
354
355    /// Add a span exporter with simple (synchronous) processing.
356    ///
357    /// Simple processing exports spans immediately as they complete. This is
358    /// useful for development and debugging but may impact performance in
359    /// production.
360    ///
361    /// This is added in addition to any exporters configured via the
362    /// configuration file.
363    pub fn with_simple_span_exporter<T: SpanExporter + 'static>(mut self, exporter: T) -> Self {
364        self.tracer_builder = Some(self.tracer_builder().with_simple_exporter(exporter));
365        self
366    }
367
368    // --- Meter configuration ---
369
370    /// Add a metric view for customizing metric aggregation.
371    ///
372    /// Views allow you to customize how metrics are collected and reported.
373    /// The view function receives instrument metadata and can return a
374    /// [`Stream`] configuration to modify the instrument's behavior.
375    ///
376    /// Return `Some(Stream)` to apply customizations, or `None` to use
377    /// the default behavior.
378    ///
379    /// # Example
380    ///
381    /// ```no_run
382    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
383    /// use opentelemetry_sdk::metrics::{Instrument, Stream};
384    ///
385    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
386    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
387    ///     .with_view(|instrument: &Instrument| {
388    ///         // Customize high-cardinality metrics
389    ///         if instrument.name().contains("debug") {
390    ///             Some(Stream::default())
391    ///         } else {
392    ///             None // Use default behavior
393    ///         }
394    ///     })
395    ///     .build()?;
396    /// # Ok(())
397    /// # }
398    /// ```
399    pub fn with_view<F>(mut self, view: F) -> Self
400    where
401        F: Fn(&Instrument) -> Option<Stream> + Send + Sync + 'static,
402    {
403        self.meter_builder = Some(self.meter_builder().with_view(view));
404        self
405    }
406
407    /// Add a periodic reader for push-based metric export.
408    ///
409    /// A [`PeriodicReader`] collects metrics at regular intervals and pushes
410    /// them to the configured exporter. Use [`PeriodicReaderBuilder`] for
411    /// custom interval and timeout settings.
412    ///
413    /// This is added in addition to any readers configured via the
414    /// configuration file.
415    ///
416    /// # Example
417    ///
418    /// ```ignore
419    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
420    /// use opentelemetry_sdk::metrics::PeriodicReader;
421    /// use opentelemetry_otlp::MetricExporter;
422    /// use std::time::Duration;
423    ///
424    /// let exporter = MetricExporter::builder().with_http().build()?;
425    /// let reader = PeriodicReader::builder(exporter)
426    ///     .with_interval(Duration::from_secs(30))
427    ///     .build();
428    ///
429    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
430    ///     .with_periodic_reader(reader)
431    ///     .build()?;
432    /// ```
433    ///
434    /// [`PeriodicReader`]: opentelemetry_sdk::metrics::PeriodicReader
435    /// [`PeriodicReaderBuilder`]: opentelemetry_sdk::metrics::PeriodicReaderBuilder
436    pub fn with_periodic_reader<E>(
437        mut self,
438        reader: opentelemetry_sdk::metrics::PeriodicReader<E>,
439    ) -> Self
440    where
441        E: opentelemetry_sdk::metrics::exporter::PushMetricExporter,
442    {
443        self.meter_builder = Some(self.meter_builder().with_reader(reader));
444        self
445    }
446
447    // --- Logger configuration ---
448
449    /// Add a log exporter with batch processing.
450    ///
451    /// Batch processing buffers log records and exports them in batches,
452    /// which is more efficient for production use.
453    ///
454    /// This is added in addition to any exporters configured via the
455    /// configuration file.
456    pub fn with_batch_log_exporter<T>(mut self, exporter: T) -> Self
457    where
458        T: opentelemetry_sdk::logs::LogExporter + 'static,
459    {
460        self.logger_builder = Some(self.logger_builder().with_batch_exporter(exporter));
461        self
462    }
463
464    /// Add a log exporter with simple (synchronous) processing.
465    ///
466    /// Simple processing exports log records immediately. This is useful
467    /// for development and debugging but may impact performance in production.
468    ///
469    /// This is added in addition to any exporters configured via the
470    /// configuration file.
471    pub fn with_simple_log_exporter<T>(mut self, exporter: T) -> Self
472    where
473        T: opentelemetry_sdk::logs::LogExporter + 'static,
474    {
475        self.logger_builder = Some(self.logger_builder().with_simple_exporter(exporter));
476        self
477    }
478
479    /// Build the [`Telemetry`] instance.
480    ///
481    /// This initializes the OpenTelemetry providers based on the configuration
482    /// and any programmatic customizations. Providers are only created if
483    /// they are configured or have programmatic additions.
484    ///
485    /// # Errors
486    ///
487    /// Returns an error if exporter initialization fails (e.g., invalid
488    /// endpoint, missing credentials, or required feature not enabled).
489    pub fn build(self) -> Result<Telemetry, InitError> {
490        // If disabled, return empty telemetry
491        if self.config.disabled == Some(true) {
492            return Ok(Telemetry {
493                tracer_provider: None,
494                meter_provider: None,
495                logger_provider: None,
496            });
497        }
498
499        // Build resource: start with config, then add builder values (builder wins on conflicts)
500        let config_resource: Resource = (&self.config.resource).into();
501        let resource = match self.resource_builder {
502            Some(builder) => {
503                // Start with an empty builder, add config attrs, then builder attrs (last wins)
504                let config_attrs: Vec<_> = config_resource
505                    .iter()
506                    .map(|(k, v)| opentelemetry::KeyValue::new(k.clone(), v.clone()))
507                    .collect();
508                Resource::builder_empty()
509                    .with_attributes(config_attrs)
510                    .with_attributes(
511                        builder
512                            .build()
513                            .iter()
514                            .map(|(k, v)| opentelemetry::KeyValue::new(k.clone(), v.clone())),
515                    )
516                    .build()
517            }
518            None => config_resource,
519        };
520
521        // Build tracer provider
522        let builder = self
523            .tracer_builder
524            .unwrap_or_else(SdkTracerProvider::builder);
525        let tracer_provider =
526            providers::traces::build_tracer_provider(&self.config, resource.clone(), builder)?;
527
528        // Build meter provider
529        let builder = self.meter_builder.unwrap_or_else(SdkMeterProvider::builder);
530        let meter_provider =
531            providers::metrics::build_meter_provider(&self.config, resource.clone(), builder)?;
532
533        // Build logger provider
534        let builder = self
535            .logger_builder
536            .unwrap_or_else(SdkLoggerProvider::builder);
537        let logger_provider =
538            providers::logs::build_logger_provider(&self.config, resource, builder)?;
539
540        // In tests we do not set the global providers to avoid polluting the global state
541        #[cfg(not(test))]
542        {
543            // Set global providers if requested and track ownership
544            if self.set_global_tracer {
545                global::set_tracer_provider(tracer_provider.clone());
546            }
547
548            if self.set_global_meter {
549                global::set_meter_provider(meter_provider.clone());
550            }
551
552            if self.set_global_propagator {
553                global::set_text_map_propagator(
554                    opentelemetry::propagation::TextMapCompositePropagator::from(
555                        &self.config.propagator,
556                    ),
557                );
558            }
559
560            // Set up log bridge if requested
561            if self.enable_log_bridge {
562                setup_log_bridge(&logger_provider)?;
563            }
564
565            // Set up tracing bridge if requested
566            if self.enable_tracing_bridge {
567                setup_tracing_bridge(&logger_provider)?;
568            }
569        }
570
571        Ok(Telemetry {
572            tracer_provider: Some(tracer_provider),
573            meter_provider: Some(meter_provider),
574            logger_provider: Some(logger_provider),
575        })
576    }
577}
578
579/// Set up the log crate bridge to route logs to OpenTelemetry.
580fn setup_log_bridge(logger_provider: &SdkLoggerProvider) -> Result<(), InitError> {
581    let otel_log_bridge = OpenTelemetryLogBridge::new(logger_provider);
582
583    // Build filter from RUST_LOG, defaulting to info level
584    let mut builder = env_filter::Builder::new();
585    builder.filter_level(log::LevelFilter::Info);
586    // Filter out OTel internal logs to prevent infinite recursion
587    builder.filter_module("opentelemetry", log::LevelFilter::Off);
588    builder.filter_module("opentelemetry_sdk", log::LevelFilter::Off);
589    if let Ok(rust_log) = std::env::var("RUST_LOG") {
590        builder.parse(&rust_log);
591    }
592    let filter = builder.build();
593    let max_level = filter.filter();
594
595    let logger = env_filter::FilteredLog::new(otel_log_bridge, filter);
596    log::set_logger(Box::leak(Box::new(logger))).map_err(|e| InitError::Bridge {
597        bridge: "log".to_string(),
598        reason: e.to_string(),
599    })?;
600    log::set_max_level(max_level);
601
602    Ok(())
603}
604
605/// Set up the tracing crate bridge to route events to OpenTelemetry logs.
606fn setup_tracing_bridge(logger_provider: &SdkLoggerProvider) -> Result<(), InitError> {
607    let otel_layer =
608        opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(logger_provider);
609
610    // Build filter from RUST_LOG, defaulting to info level
611    let filter = tracing_subscriber::EnvFilter::try_from_default_env()
612        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
613        // Filter out OTel internal logs to prevent infinite recursion
614        .add_directive("opentelemetry=off".parse().unwrap())
615        .add_directive("opentelemetry_sdk=off".parse().unwrap());
616
617    tracing_subscriber::registry()
618        .with(filter)
619        .with(otel_layer)
620        .try_init()
621        .map_err(|e| InitError::Bridge {
622            bridge: "tracing".to_string(),
623            reason: e.to_string(),
624        })?;
625
626    Ok(())
627}
628
629/// Unified telemetry management for traces, metrics, and logs.
630///
631/// This struct holds the OpenTelemetry providers and ensures proper shutdown
632/// when dropped. Create an instance at application startup and keep it alive
633/// for the duration of your application.
634///
635/// # Lifecycle
636///
637/// - Create at application startup using [`Telemetry::new()`] or [`Telemetry::builder()`]
638/// - Keep the instance alive (e.g., in your main function or as application state)
639/// - Providers automatically shut down when `Telemetry` is dropped
640/// - For explicit shutdown control, call [`Telemetry::shutdown()`]
641///
642/// # Example
643///
644/// ```no_run
645/// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
646///
647/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
648/// // Initialize at startup
649/// let telemetry = Telemetry::new(OpenTelemetryConfig::default())?;
650///
651/// // Access providers if needed
652/// if let Some(tracer_provider) = telemetry.tracer_provider() {
653///     // Use the tracer provider directly
654/// }
655///
656/// // Telemetry shuts down automatically when dropped
657/// # Ok(())
658/// # }
659/// ```
660pub struct Telemetry {
661    tracer_provider: Option<SdkTracerProvider>,
662    meter_provider: Option<SdkMeterProvider>,
663    logger_provider: Option<SdkLoggerProvider>,
664}
665
666impl Telemetry {
667    /// Create a no-op telemetry instance with no providers or output.
668    ///
669    /// This returns a `Telemetry` instance that does nothing: no providers
670    /// are created, no global providers are set, and no data is exported.
671    /// This is useful for:
672    ///
673    /// - **Tests**: When you need a `Telemetry` instance but don't want actual
674    ///   telemetry overhead or output.
675    /// - **Disabled telemetry**: When you want to completely disable telemetry
676    ///   without going through configuration parsing.
677    pub fn none() -> Self {
678        Self {
679            tracer_provider: None,
680            meter_provider: None,
681            logger_provider: None,
682        }
683    }
684
685    /// Create a builder for configuring telemetry.
686    ///
687    /// Use the builder when you need to add custom exporters, processors,
688    /// or views programmatically.
689    ///
690    /// # Example
691    ///
692    /// ```no_run
693    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
694    ///
695    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
696    /// let telemetry = Telemetry::builder(OpenTelemetryConfig::default())
697    ///     .with_global_tracer_provider()
698    ///     .build()?;
699    /// # Ok(())
700    /// # }
701    /// ```
702    pub fn builder(config: OpenTelemetryConfig) -> TelemetryBuilder {
703        TelemetryBuilder::new(config)
704    }
705
706    /// Create telemetry with full integration from configuration.
707    ///
708    /// This method sets up OpenTelemetry with all integrations enabled:
709    /// - **Global tracer provider**: Enables `opentelemetry::global::tracer()` anywhere
710    /// - **Global meter provider**: Enables `opentelemetry::global::meter()` anywhere
711    /// - **Log bridge**: Routes `log::info!()` etc. to OpenTelemetry logs
712    /// - **Tracing bridge**: Routes `tracing::info!()` etc. to OpenTelemetry logs
713    ///
714    /// This is the recommended way to initialize telemetry for most applications,
715    /// as it provides seamless integration with the Rust logging ecosystem.
716    ///
717    /// # When to use `builder()` instead
718    ///
719    /// Use [`Telemetry::builder()`] if you need:
720    /// - Custom exporters or processors
721    /// - To disable specific bridges (e.g., if you have your own tracing subscriber)
722    /// - Fine-grained control over which integrations are enabled
723    ///
724    /// # Effects on your application
725    ///
726    /// - **Sets the global logger**: Only one logger can be set per process. This will
727    ///   fail if another logger (e.g., `env_logger`) is already installed.
728    /// - **Sets the global tracing subscriber**: Only one subscriber can be set per
729    ///   process. This will fail if another subscriber is already installed.
730    /// - **Filters apply**: Both bridges respect `RUST_LOG` for filtering (defaults to `info`).
731    ///
732    /// # Example
733    ///
734    /// ```no_run
735    /// use apollo_opentelemetry::{OpenTelemetryConfig, Telemetry};
736    ///
737    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
738    /// let config = OpenTelemetryConfig::default();
739    /// let telemetry = Telemetry::new(config)?;
740    ///
741    /// // Now all logging goes through OpenTelemetry
742    /// log::info!("This becomes an OpenTelemetry log record");
743    /// # Ok(())
744    /// # }
745    /// ```
746    pub fn new(config: OpenTelemetryConfig) -> Result<Self, InitError> {
747        Self::builder(config).with_globals().build()
748    }
749
750    /// Get a reference to the tracer provider, if tracing is enabled.
751    ///
752    /// Returns `None` if no tracer configuration was provided and no
753    /// span exporters were added programmatically.
754    pub fn tracer_provider(&self) -> Option<&SdkTracerProvider> {
755        self.tracer_provider.as_ref()
756    }
757
758    /// Get a reference to the meter provider, if metrics are enabled.
759    ///
760    /// Returns `None` if no meter configuration was provided and no
761    /// metric views were added programmatically.
762    pub fn meter_provider(&self) -> Option<&SdkMeterProvider> {
763        self.meter_provider.as_ref()
764    }
765
766    /// Get a reference to the logger provider, if logging is enabled.
767    ///
768    /// Returns `None` if no logger configuration was provided and no
769    /// log exporters were added programmatically.
770    pub fn logger_provider(&self) -> Option<&SdkLoggerProvider> {
771        self.logger_provider.as_ref()
772    }
773
774    /// Explicitly shut down all telemetry providers.
775    ///
776    /// This flushes any buffered telemetry data and releases resources.
777    /// It is called automatically when the `Telemetry` instance is dropped,
778    /// but can be called explicitly for more control over shutdown timing.
779    ///
780    /// If the providers were registered as global providers, the global
781    /// provider will continue to reference the shut-down provider, which
782    /// will act as a no-op for any subsequent operations.
783    ///
784    /// After calling `shutdown()`, the providers are no longer available
785    /// and subsequent calls to `tracer_provider()`, `meter_provider()`,
786    /// and `logger_provider()` will return `None`.
787    pub fn shutdown(&mut self) {
788        // Take providers before any closure to avoid borrowing issues
789        let tracer_provider = self.tracer_provider.take();
790        let meter_provider = self.meter_provider.take();
791        let logger_provider = self.logger_provider.take();
792
793        // Shutdown order: traces first, then metrics, then logs
794        let shutdown = move || {
795            if let Some(provider) = tracer_provider {
796                let _ = provider.shutdown();
797            }
798            if let Some(provider) = meter_provider {
799                let _ = provider.shutdown();
800            }
801            if let Some(provider) = logger_provider {
802                let _ = provider.shutdown();
803            }
804        };
805
806        // In a multi-thread tokio runtime, use block_in_place to move blocking work off the
807        // async worker thread. This prevents blocking the executor while waiting for shutdown.
808        //
809        // For current_thread runtimes or non-tokio contexts, call shutdown directly. The non-async
810        // BatchSpanProcessor we use runs on a dedicated OS thread, so blocking here is safe.
811        //
812        // TODO: When switching to the async BatchSpanProcessor (span_processor_with_async_runtime),
813        // detect runtime flavor at construction time and pass `TokioCurrentThread` for current_thread
814        // or `Tokio` for multi_thread. This handles threading correctly from the start.
815        // See: https://github.com/open-telemetry/opentelemetry-rust/blob/v0.31.0/opentelemetry-sdk/src/runtime.rs
816        if let Ok(rt) = tokio::runtime::Handle::try_current()
817            && rt.runtime_flavor() == RuntimeFlavor::MultiThread
818        {
819            tokio::task::block_in_place(shutdown);
820        } else {
821            shutdown();
822        }
823    }
824}
825
826impl Drop for Telemetry {
827    fn drop(&mut self) {
828        self.shutdown();
829    }
830}
831
832#[cfg(test)]
833mod tests {
834    use apollo_configuration::parse_yaml;
835    use opentelemetry::trace::TracerProvider;
836    use opentelemetry_sdk::error::OTelSdkResult;
837    use std::time::Duration;
838
839    use super::*;
840
841    #[test]
842    fn none_has_no_providers() {
843        let telemetry = Telemetry::none();
844
845        assert!(telemetry.tracer_provider().is_none());
846        assert!(telemetry.meter_provider().is_none());
847        assert!(telemetry.logger_provider().is_none());
848    }
849
850    #[test]
851    fn build_disabled_config() {
852        let config: OpenTelemetryConfig = parse_yaml(
853            indoc::indoc! {"
854                disabled: true
855                tracer_provider:
856                  processors:
857                    - batch:
858                        exporter:
859                          console: {}
860            "},
861            &Default::default(),
862        )
863        .unwrap();
864
865        let telemetry = Telemetry::new(config).unwrap();
866
867        // Disabled config should have no providers
868        assert!(telemetry.tracer_provider().is_none());
869        assert!(telemetry.meter_provider().is_none());
870        assert!(telemetry.logger_provider().is_none());
871    }
872
873    #[test]
874    fn builder_does_not_register_globally_by_default() {
875        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
876        let builder = TelemetryBuilder::new(config);
877        assert!(!builder.set_global_tracer);
878        assert!(!builder.set_global_meter);
879        assert!(!builder.set_global_propagator);
880    }
881
882    #[test]
883    fn builder_with_global_propagator_flag() {
884        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
885        let builder = Telemetry::builder(config).with_global_propagator();
886        assert!(builder.set_global_propagator);
887        let _telemetry = builder.build().unwrap();
888    }
889
890    #[test]
891    fn builder_with_global_tracer_flag() {
892        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
893        let builder = Telemetry::builder(config).with_global_tracer_provider();
894        assert!(builder.set_global_tracer);
895        let _telemetry = builder.build().unwrap();
896    }
897
898    #[test]
899    fn builder_with_global_meter_flag() {
900        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
901        let builder = Telemetry::builder(config).with_global_meter_provider();
902        assert!(builder.set_global_meter);
903        let _telemetry = builder.build().unwrap();
904    }
905
906    #[test]
907    fn builder_with_resource_builder() {
908        use opentelemetry::KeyValue;
909        use opentelemetry_sdk::Resource;
910
911        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
912        let _telemetry = Telemetry::builder(config)
913            .with_resource_builder(
914                Resource::builder()
915                    .with_service_name("test-service")
916                    .with_attributes([KeyValue::new("test.attr", "test-value")]),
917            )
918            .build()
919            .unwrap();
920
921        // Telemetry builds successfully with custom resource
922        assert!(_telemetry.tracer_provider().is_some());
923        assert!(_telemetry.meter_provider().is_some());
924        assert!(_telemetry.logger_provider().is_some());
925    }
926
927    #[test]
928    fn builder_with_resource_builder_overrides_config() {
929        use opentelemetry::KeyValue;
930        use opentelemetry_sdk::Resource;
931
932        // Config has a resource attribute
933        let config: OpenTelemetryConfig = parse_yaml(
934            indoc::indoc! {"
935                resource:
936                  attributes:
937                    - name: from.config
938                      value: config-value
939            "},
940            &Default::default(),
941        )
942        .unwrap();
943
944        // Builder resource should override config resource
945        let _telemetry = Telemetry::builder(config)
946            .with_resource_builder(
947                Resource::builder()
948                    .with_service_name("override-service")
949                    .with_attributes([KeyValue::new("from.builder", "builder-value")]),
950            )
951            .build()
952            .unwrap();
953
954        assert!(_telemetry.tracer_provider().is_some());
955    }
956
957    #[test]
958    fn shutdown_clears_providers() {
959        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
960        let mut telemetry = Telemetry::builder(config).build().unwrap();
961
962        telemetry.shutdown();
963
964        assert!(telemetry.tracer_provider().is_none());
965        assert!(telemetry.meter_provider().is_none());
966        assert!(telemetry.logger_provider().is_none());
967    }
968
969    #[test]
970    fn shutdown_is_idempotent() {
971        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
972        let mut telemetry = Telemetry::builder(config).build().unwrap();
973
974        telemetry.shutdown();
975        telemetry.shutdown(); // Should not panic
976
977        assert!(telemetry.tracer_provider().is_none());
978    }
979
980    #[cfg(feature = "otlp")]
981    #[test]
982    fn global_tracer_set_and_shutdown() {
983        use opentelemetry::trace::Tracer;
984
985        let config: OpenTelemetryConfig = parse_yaml(
986            indoc::indoc! {"
987                tracer_provider:
988                  processors:
989                    - batch:
990                        exporter:
991                          console: {}
992            "},
993            &Default::default(),
994        )
995        .unwrap();
996
997        // Create telemetry with global tracer provider
998        let telemetry = Telemetry::builder(config)
999            .with_global_tracer_provider()
1000            .build()
1001            .unwrap();
1002
1003        // Verify the global tracer is working (can create a tracer and span)
1004        let tracer = global::tracer("test");
1005        let _span = tracer.start("test-span");
1006
1007        // Drop the telemetry - this should shutdown the global provider
1008        drop(telemetry);
1009
1010        // After shutdown, the global still references the provider but operations are noops
1011        let tracer = global::tracer("test-after-shutdown");
1012        let _span = tracer.start("noop-span");
1013    }
1014
1015    #[cfg(feature = "otlp")]
1016    #[test]
1017    fn global_meter_set_and_shutdown() {
1018        let config: OpenTelemetryConfig = parse_yaml(
1019            indoc::indoc! {"
1020                meter_provider:
1021                  readers:
1022                    - periodic:
1023                        exporter:
1024                          otlp_http:
1025                            endpoint: http://localhost:4318
1026            "},
1027            &Default::default(),
1028        )
1029        .unwrap();
1030
1031        // Create telemetry with global meter provider
1032        let telemetry = Telemetry::builder(config)
1033            .with_global_meter_provider()
1034            .build()
1035            .unwrap();
1036
1037        // Verify the global meter is working (can create a meter and instrument)
1038        let meter = global::meter("test");
1039        let counter = meter.u64_counter("test_counter").build();
1040        counter.add(1, &[]);
1041
1042        // Drop the telemetry - this should shutdown the global provider
1043        drop(telemetry);
1044
1045        // After shutdown, the global still references the provider but operations are noops
1046        let meter = global::meter("test-after-shutdown");
1047        let counter = meter.u64_counter("noop_counter").build();
1048        counter.add(1, &[]);
1049    }
1050
1051    #[cfg(feature = "otlp")]
1052    #[test]
1053    fn explicit_shutdown() {
1054        let config: OpenTelemetryConfig = parse_yaml(
1055            indoc::indoc! {"
1056                tracer_provider:
1057                  processors:
1058                    - batch:
1059                        exporter:
1060                          otlp_http:
1061                            endpoint: http://localhost:4318
1062            "},
1063            &Default::default(),
1064        )
1065        .unwrap();
1066
1067        let mut telemetry = Telemetry::builder(config)
1068            .with_global_tracer_provider()
1069            .build()
1070            .unwrap();
1071
1072        // Call shutdown explicitly
1073        telemetry.shutdown();
1074
1075        assert!(telemetry.tracer_provider().is_none());
1076    }
1077
1078    // Test that shutdown() uses block_in_place to avoid blocking the async runtime.
1079    //
1080    // Problem: OTel exporters may perform blocking I/O during shutdown (e.g., flushing
1081    // spans over the network). Without block_in_place, this blocks the worker thread.
1082    //
1083    // Setup:
1084    // - Tokio runtime with 1 worker thread
1085    // - Custom exporter that blocks for 1 second during shutdown
1086    // - Timer task that increments a counter every 10ms
1087    //
1088    // How it works:
1089    // 1. Spawn a timer task incrementing a counter every 10ms
1090    // 2. Spawn a task that drops Telemetry (triggers blocking shutdown)
1091    // 3. Wait for shutdown to complete, then check the counter
1092    //
1093    // Expected:
1094    // - WITH block_in_place: Timer runs during shutdown, counter reaches ~100
1095    // - WITHOUT block_in_place: Worker blocked, counter stays at ~0
1096    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1097    async fn shutdown_in_async_context() {
1098        use std::sync::Arc;
1099        use std::sync::atomic::{AtomicUsize, Ordering};
1100
1101        // Test exporter that simulates slow network I/O during shutdown
1102        #[derive(Debug)]
1103        struct BlockingExporter;
1104
1105        impl opentelemetry_sdk::trace::SpanExporter for BlockingExporter {
1106            async fn export(
1107                &self,
1108                _batch: Vec<opentelemetry_sdk::trace::SpanData>,
1109            ) -> opentelemetry_sdk::error::OTelSdkResult {
1110                Ok(())
1111            }
1112
1113            fn shutdown_with_timeout(&mut self, _timeout: Duration) -> OTelSdkResult {
1114                // Simulate slow network flush - blocks thread for 1 second
1115                std::thread::sleep(Duration::from_millis(1000));
1116                Ok(())
1117            }
1118        }
1119
1120        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
1121        let telemetry = Telemetry::builder(config)
1122            .with_simple_span_exporter(BlockingExporter)
1123            .build()
1124            .unwrap();
1125
1126        // Counter incremented by timer task running concurrently with shutdown
1127        let counter = Arc::new(AtomicUsize::new(0));
1128        let counter_clone = counter.clone();
1129
1130        // Timer task: increments counter every 10ms (needs worker thread to run)
1131        let _timer_task = tokio::spawn(async move {
1132            let mut interval = tokio::time::interval(Duration::from_millis(10));
1133            loop {
1134                interval.tick().await;
1135                counter_clone.fetch_add(1, Ordering::SeqCst);
1136            }
1137        });
1138
1139        // Drop task: triggers blocking shutdown on worker thread
1140        let drop_task = tokio::spawn(async move {
1141            drop(telemetry);
1142        });
1143
1144        drop_task.await.unwrap();
1145
1146        // Verify timer ran during 1-second shutdown
1147        // With block_in_place: ~100 ticks. Without: ~0 ticks.
1148        let final_count = counter.load(Ordering::SeqCst);
1149        assert!(
1150            final_count >= 10,
1151            "Timer should have run during shutdown (got {} ticks)",
1152            final_count
1153        );
1154    }
1155
1156    // Verify shutdown works in a single-threaded tokio runtime using spawn_blocking.
1157    #[tokio::test(flavor = "current_thread")]
1158    async fn shutdown_in_single_threaded_runtime() {
1159        let config = OpenTelemetryConfig::default();
1160        let mut telemetry = Telemetry::builder(config).build().unwrap();
1161
1162        // This should not panic
1163        telemetry.shutdown();
1164    }
1165
1166    #[test]
1167    fn builder_with_simple_span_exporter() {
1168        use opentelemetry::trace::Tracer;
1169        use opentelemetry_sdk::trace::InMemorySpanExporter;
1170
1171        let exporter = InMemorySpanExporter::default();
1172        let exporter_clone = exporter.clone();
1173
1174        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
1175        let telemetry = Telemetry::builder(config)
1176            .with_simple_span_exporter(exporter)
1177            .build()
1178            .unwrap();
1179
1180        // Create a span
1181        let tracer = telemetry.tracer_provider().unwrap().tracer("test");
1182        let _span = tracer.start("test-span");
1183        drop(_span);
1184
1185        // Simple exporter exports synchronously
1186        let spans = exporter_clone.get_finished_spans().unwrap();
1187        assert_eq!(spans.len(), 1);
1188        assert_eq!(spans[0].name.as_ref(), "test-span");
1189    }
1190
1191    #[test]
1192    fn builder_with_batch_span_exporter() {
1193        use opentelemetry::trace::Tracer;
1194        use opentelemetry_sdk::trace::InMemorySpanExporter;
1195
1196        let exporter = InMemorySpanExporter::default();
1197        let exporter_clone = exporter.clone();
1198
1199        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
1200        let mut telemetry = Telemetry::builder(config)
1201            .with_batch_span_exporter(exporter)
1202            .build()
1203            .unwrap();
1204
1205        // Create a span
1206        let tracer = telemetry.tracer_provider().unwrap().tracer("test");
1207        let _span = tracer.start("batch-test-span");
1208        drop(_span);
1209
1210        // Force flush to export batched spans
1211        telemetry.tracer_provider().unwrap().force_flush().unwrap();
1212
1213        let spans = exporter_clone.get_finished_spans().unwrap();
1214        assert_eq!(spans.len(), 1);
1215        assert_eq!(spans[0].name.as_ref(), "batch-test-span");
1216
1217        telemetry.shutdown();
1218    }
1219
1220    #[test]
1221    fn builder_with_span_processor() {
1222        use opentelemetry::trace::Tracer;
1223        use opentelemetry_sdk::trace::{InMemorySpanExporter, SimpleSpanProcessor};
1224
1225        let exporter = InMemorySpanExporter::default();
1226        let exporter_clone = exporter.clone();
1227        let processor = SimpleSpanProcessor::new(exporter);
1228
1229        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
1230        let telemetry = Telemetry::builder(config)
1231            .with_span_processor(processor)
1232            .build()
1233            .unwrap();
1234
1235        // Create a span
1236        let tracer = telemetry.tracer_provider().unwrap().tracer("test");
1237        let _span = tracer.start("processor-test-span");
1238        drop(_span);
1239
1240        let spans = exporter_clone.get_finished_spans().unwrap();
1241        assert_eq!(spans.len(), 1);
1242        assert_eq!(spans[0].name.as_ref(), "processor-test-span");
1243    }
1244
1245    #[test]
1246    fn builder_with_view() {
1247        use opentelemetry::metrics::MeterProvider;
1248
1249        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
1250        let telemetry = Telemetry::builder(config)
1251            .with_view(|_instrument: &Instrument| None)
1252            .build()
1253            .unwrap();
1254
1255        // Verify we can create instruments through the meter provider
1256        let meter = telemetry.meter_provider().unwrap().meter("test");
1257        let counter = meter.u64_counter("test_counter").build();
1258        counter.add(1, &[]);
1259    }
1260
1261    #[test]
1262    fn builder_with_periodic_reader() {
1263        use opentelemetry::metrics::MeterProvider;
1264        use opentelemetry_sdk::metrics::{InMemoryMetricExporter, PeriodicReader};
1265
1266        let exporter = InMemoryMetricExporter::default();
1267        let reader = PeriodicReader::builder(exporter.clone()).build();
1268
1269        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
1270        let telemetry = Telemetry::builder(config)
1271            .with_periodic_reader(reader)
1272            .build()
1273            .unwrap();
1274
1275        let meter_provider = telemetry.meter_provider().unwrap();
1276        let meter = meter_provider.meter("test");
1277        let counter = meter.u64_counter("test_counter").build();
1278        counter.add(42, &[]);
1279
1280        meter_provider.force_flush().unwrap();
1281
1282        let metrics = exporter.get_finished_metrics().unwrap();
1283        assert!(!metrics.is_empty(), "expected metrics to be exported");
1284    }
1285
1286    #[test]
1287    fn builder_with_simple_log_exporter() {
1288        use opentelemetry_sdk::logs::InMemoryLogExporter;
1289
1290        let exporter = InMemoryLogExporter::default();
1291
1292        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
1293        let telemetry = Telemetry::builder(config)
1294            .with_simple_log_exporter(exporter)
1295            .build()
1296            .unwrap();
1297
1298        assert!(telemetry.logger_provider().is_some());
1299    }
1300
1301    #[test]
1302    fn builder_with_batch_log_exporter() {
1303        use opentelemetry_sdk::logs::InMemoryLogExporter;
1304
1305        let exporter = InMemoryLogExporter::default();
1306
1307        let config: OpenTelemetryConfig = parse_yaml("{}", &Default::default()).unwrap();
1308        let telemetry = Telemetry::builder(config)
1309            .with_batch_log_exporter(exporter)
1310            .build()
1311            .unwrap();
1312
1313        assert!(telemetry.logger_provider().is_some());
1314    }
1315}