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}