lambda_otel_lite/telemetry.rs
1//! Core functionality for OpenTelemetry initialization and configuration in Lambda functions.
2//!
3//! This module provides the initialization and configuration components for OpenTelemetry in Lambda:
4//! - `init_telemetry`: Main entry point for telemetry setup
5//! - `TelemetryConfig`: Configuration builder with environment-based defaults
6//! - `TelemetryCompletionHandler`: Controls span export timing based on processing mode
7//!
8//! # Architecture
9//!
10//! The initialization flow:
11//! 1. Configuration is built from environment and/or builder options
12//! 2. Span processor is created based on processing mode
13//! 3. Resource attributes are detected from Lambda environment
14//! 4. Tracer provider is initialized with the configuration
15//! 5. Completion handler is returned for managing span export
16//!
17//! # Environment Configuration
18//!
19//! Core environment variables:
20//! - `LAMBDA_EXTENSION_SPAN_PROCESSOR_MODE`: "sync" (default), "async", or "finalize"
21//! - `LAMBDA_SPAN_PROCESSOR_QUEUE_SIZE`: Maximum spans in buffer (default: 2048)
22//! - `OTEL_SERVICE_NAME`: Override auto-detected service name
23//!
24//! # Basic Usage
25//!
26//! ```no_run
27//! use lambda_otel_lite::telemetry::{init_telemetry, TelemetryConfig};
28//! use lambda_runtime::Error;
29//!
30//! #[tokio::main]
31//! async fn main() -> Result<(), Error> {
32//! let (_, completion_handler) = init_telemetry(TelemetryConfig::default()).await?;
33//! Ok(())
34//! }
35//! ```
36//!
37//! Custom configuration with custom resource attributes:
38//! ```no_run
39//! use lambda_otel_lite::telemetry::{init_telemetry, TelemetryConfig};
40//! use opentelemetry::KeyValue;
41//! use opentelemetry_sdk::Resource;
42//! use lambda_runtime::Error;
43//!
44//! #[tokio::main]
45//! async fn main() -> Result<(), Error> {
46//! let resource = Resource::builder()
47//! .with_attributes(vec![
48//! KeyValue::new("service.version", "1.0.0"),
49//! KeyValue::new("deployment.environment", "production"),
50//! ])
51//! .build();
52//!
53//! let config = TelemetryConfig::builder()
54//! .resource(resource)
55//! .build();
56//!
57//! let (_, completion_handler) = init_telemetry(config).await?;
58//! Ok(())
59//! }
60//! ```
61//!
62//! Custom configuration with custom span processor:
63//! ```no_run
64//! use lambda_otel_lite::{init_telemetry, TelemetryConfig};
65//! use opentelemetry_sdk::trace::SimpleSpanProcessor;
66//! use otlp_stdout_span_exporter::OtlpStdoutSpanExporter;
67//! use lambda_runtime::Error;
68//!
69//! #[tokio::main]
70//! async fn main() -> Result<(), Error> {
71//! let config = TelemetryConfig::builder()
72//! .with_span_processor(SimpleSpanProcessor::new(
73//! Box::new(OtlpStdoutSpanExporter::default())
74//! ))
75//! .enable_fmt_layer(true)
76//! .build();
77//!
78//! let (_, completion_handler) = init_telemetry(config).await?;
79//! Ok(())
80//! }
81//! ```
82//!
83//! # Environment Variables
84//!
85//! The following environment variables affect the configuration:
86//! - `OTEL_SERVICE_NAME`: Service name for spans
87//! - `OTEL_RESOURCE_ATTRIBUTES`: Additional resource attributes
88//! - `LAMBDA_SPAN_PROCESSOR_QUEUE_SIZE`: Span buffer size (default: 2048)
89//! - `OTLP_STDOUT_SPAN_EXPORTER_COMPRESSION_LEVEL`: Export compression (default: 6)
90//! - `LAMBDA_TRACING_ENABLE_FMT_LAYER`: Enable formatting layer (default: false)
91//! - `LAMBDA_EXTENSION_SPAN_PROCESSOR_MODE`: Processing mode (sync/async/finalize)
92//! - `RUST_LOG` or `AWS_LAMBDA_LOG_LEVEL`: Log level configuration
93
94use crate::{
95 extension::register_extension, mode::ProcessorMode, processor::LambdaSpanProcessor,
96 resource::get_lambda_resource,
97};
98use bon::Builder;
99use lambda_runtime::Error;
100use opentelemetry::propagation::{TextMapCompositePropagator, TextMapPropagator};
101use opentelemetry::{global, global::set_tracer_provider, trace::TracerProvider as _, KeyValue};
102use opentelemetry_sdk::{
103 propagation::TraceContextPropagator,
104 trace::{SdkTracerProvider, SpanProcessor, TracerProviderBuilder},
105 Resource,
106};
107use otlp_stdout_span_exporter::OtlpStdoutSpanExporter;
108use std::{borrow::Cow, env, sync::Arc};
109use tokio::sync::mpsc::UnboundedSender;
110use tracing_subscriber::layer::SubscriberExt;
111
112/// Manages the lifecycle of span export based on the processing mode.
113///
114/// This handler must be used to signal when spans should be exported. Its behavior
115/// varies by processing mode:
116/// - Sync: Forces immediate export
117/// - Async: Signals the extension to export
118/// - Finalize: Defers to span processor
119///
120/// # Thread Safety
121///
122/// This type is `Clone` and can be safely shared between threads.
123#[derive(Clone)]
124pub struct TelemetryCompletionHandler {
125 provider: Arc<SdkTracerProvider>,
126 sender: Option<UnboundedSender<()>>,
127 mode: ProcessorMode,
128 tracer: opentelemetry_sdk::trace::Tracer,
129}
130
131impl TelemetryCompletionHandler {
132 pub fn new(
133 provider: Arc<SdkTracerProvider>,
134 sender: Option<UnboundedSender<()>>,
135 mode: ProcessorMode,
136 ) -> Self {
137 // Create instrumentation scope with attributes
138 let scope = opentelemetry::InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
139 .with_version(Cow::Borrowed(env!("CARGO_PKG_VERSION")))
140 .with_schema_url(Cow::Borrowed("https://opentelemetry.io/schemas/1.30.0"))
141 .with_attributes(vec![
142 KeyValue::new("library.language", "rust"),
143 KeyValue::new("library.type", "instrumentation"),
144 KeyValue::new("library.runtime", "aws_lambda"),
145 ])
146 .build();
147
148 // Create tracer with instrumentation scope
149 let tracer = provider.tracer_with_scope(scope);
150
151 Self {
152 provider,
153 sender,
154 mode,
155 tracer,
156 }
157 }
158
159 /// Get the tracer instance for creating spans.
160 ///
161 /// Returns the cached tracer instance configured with this package's instrumentation scope.
162 /// The tracer is configured with the provider's settings and will automatically use
163 /// the correct span processor based on the processing mode.
164 pub fn get_tracer(&self) -> &opentelemetry_sdk::trace::Tracer {
165 &self.tracer
166 }
167
168 /// Complete telemetry processing for the current invocation
169 ///
170 /// In Sync mode, this will force flush the provider and log any errors that occur.
171 /// In Async mode, this will send a completion signal to the extension.
172 /// In Finalize mode, this will do nothing (handled by drop).
173 pub fn complete(&self) {
174 match self.mode {
175 ProcessorMode::Sync => {
176 if let Err(e) = self.provider.force_flush() {
177 tracing::warn!(error = ?e, "Error flushing telemetry");
178 }
179 }
180 ProcessorMode::Async => {
181 if let Some(sender) = &self.sender {
182 if let Err(e) = sender.send(()) {
183 tracing::warn!(error = ?e, "Failed to send completion signal to extension");
184 }
185 }
186 }
187 ProcessorMode::Finalize => {
188 // Do nothing, handled by drop
189 }
190 }
191 }
192}
193
194/// Configuration for OpenTelemetry initialization.
195///
196/// Provides configuration options for telemetry setup. Use `TelemetryConfig::default()`
197/// for standard Lambda configuration, or the builder pattern for customization.
198///
199/// # Fields
200///
201/// * `enable_fmt_layer` - Enable console output for debugging (default: false)
202/// * `set_global_provider` - Set as global tracer provider (default: true)
203/// * `resource` - Custom resource attributes (default: auto-detected from Lambda)
204/// * `env_var_name` - Environment variable name for log level configuration
205///
206/// # Examples
207///
208/// Basic usage with default configuration:
209///
210/// ```no_run
211/// use lambda_otel_lite::telemetry::TelemetryConfig;
212///
213/// let config = TelemetryConfig::default();
214/// ```
215///
216/// Custom configuration with resource attributes:
217///
218/// ```no_run
219/// use lambda_otel_lite::telemetry::TelemetryConfig;
220/// use opentelemetry::KeyValue;
221/// use opentelemetry_sdk::Resource;
222///
223/// let config = TelemetryConfig::builder()
224/// .resource(Resource::builder()
225/// .with_attributes(vec![KeyValue::new("version", "1.0.0")])
226/// .build())
227/// .build();
228/// ```
229///
230/// Custom configuration with logging options:
231///
232/// ```no_run
233/// use lambda_otel_lite::telemetry::TelemetryConfig;
234///
235/// let config = TelemetryConfig::builder()
236/// .enable_fmt_layer(true) // Enable console output for debugging
237/// .env_var_name("MY_CUSTOM_LOG_LEVEL".to_string()) // Custom env var for log level
238/// .build();
239/// ```
240#[derive(Builder, Debug)]
241pub struct TelemetryConfig {
242 // Custom fields for internal state
243 #[builder(field)]
244 provider_builder: TracerProviderBuilder,
245
246 #[builder(field)]
247 has_processor: bool,
248
249 #[builder(field)]
250 propagators: Vec<Box<dyn TextMapPropagator + Send + Sync>>,
251
252 /// Enable console output for debugging.
253 ///
254 /// When enabled, spans and events will be printed to the console in addition
255 /// to being exported through the configured span processors. This is useful
256 /// for debugging but adds overhead and should be disabled in production.
257 ///
258 /// Default: `false`
259 #[builder(default = false)]
260 pub enable_fmt_layer: bool,
261
262 /// Set this provider as the global OpenTelemetry provider.
263 ///
264 /// When enabled, the provider will be registered as the global provider
265 /// for the OpenTelemetry API. This allows using the global tracer API
266 /// without explicitly passing around the provider.
267 ///
268 /// Default: `true`
269 #[builder(default = true)]
270 pub set_global_provider: bool,
271
272 /// Custom resource attributes for all spans.
273 ///
274 /// If not provided, resource attributes will be automatically detected
275 /// from the Lambda environment. Custom resources will override any
276 /// automatically detected attributes with the same keys.
277 ///
278 /// Default: `None` (auto-detected from Lambda environment)
279 pub resource: Option<Resource>,
280
281 /// Environment variable name to use for log level configuration.
282 ///
283 /// This field specifies which environment variable should be used to configure
284 /// the tracing subscriber's log level filter. If not specified, the system will
285 /// first check for `RUST_LOG` and then fall back to `AWS_LAMBDA_LOG_LEVEL`.
286 ///
287 /// Default: `None` (uses `RUST_LOG` or `AWS_LAMBDA_LOG_LEVEL`)
288 pub env_var_name: Option<String>,
289}
290
291impl Default for TelemetryConfig {
292 fn default() -> Self {
293 Self::builder().build()
294 }
295}
296
297/// Builder methods for adding span processors and other configuration
298impl<S: telemetry_config_builder::State> TelemetryConfigBuilder<S> {
299 /// Add a span processor to the tracer provider.
300 ///
301 /// This method allows adding custom span processors for trace data processing.
302 /// Multiple processors can be added by calling this method multiple times.
303 ///
304 /// # Arguments
305 ///
306 /// * `processor` - A span processor implementing the [`SpanProcessor`] trait
307 ///
308 /// # Examples
309 ///
310 /// ```no_run
311 /// use lambda_otel_lite::TelemetryConfig;
312 /// use opentelemetry_sdk::trace::SimpleSpanProcessor;
313 /// use otlp_stdout_span_exporter::OtlpStdoutSpanExporter;
314 ///
315 /// // Only use builder when adding custom processors
316 /// let config = TelemetryConfig::builder()
317 /// .with_span_processor(SimpleSpanProcessor::new(
318 /// Box::new(OtlpStdoutSpanExporter::default())
319 /// ))
320 /// .build();
321 /// ```
322 pub fn with_span_processor<T>(mut self, processor: T) -> Self
323 where
324 T: SpanProcessor + 'static,
325 {
326 self.provider_builder = self.provider_builder.with_span_processor(processor);
327 self.has_processor = true;
328 self
329 }
330
331 /// Add a propagator to the list of propagators.
332 ///
333 /// Multiple propagators can be added and will be combined into a composite propagator.
334 /// The default propagator is TraceContextPropagator.
335 ///
336 /// # Arguments
337 ///
338 /// * `propagator` - A propagator implementing the [`TextMapPropagator`] trait
339 ///
340 /// # Examples
341 ///
342 /// ```no_run
343 /// use lambda_otel_lite::TelemetryConfig;
344 /// use opentelemetry_sdk::propagation::BaggagePropagator;
345 ///
346 /// let config = TelemetryConfig::builder()
347 /// .with_propagator(BaggagePropagator::new())
348 /// .build();
349 /// ```
350 pub fn with_propagator<T>(mut self, propagator: T) -> Self
351 where
352 T: TextMapPropagator + Send + Sync + 'static,
353 {
354 self.propagators.push(Box::new(propagator));
355 self
356 }
357}
358
359/// Initialize OpenTelemetry for AWS Lambda with the provided configuration.
360///
361/// # Arguments
362///
363/// * `config` - Configuration for telemetry initialization
364///
365/// # Returns
366///
367/// Returns a tuple containing:
368/// - A tracer instance for manual instrumentation
369/// - A completion handler for managing span export timing
370///
371/// # Errors
372///
373/// Returns error if:
374/// - Extension registration fails (async/finalize modes)
375/// - Tracer provider initialization fails
376/// - Environment variable parsing fails
377///
378/// # Examples
379///
380/// Basic usage with default configuration:
381///
382/// ```no_run
383/// use lambda_otel_lite::telemetry::{init_telemetry, TelemetryConfig};
384///
385/// # async fn example() -> Result<(), lambda_runtime::Error> {
386/// // Initialize with default configuration
387/// let (_, telemetry) = init_telemetry(TelemetryConfig::default()).await?;
388/// # Ok(())
389/// # }
390/// ```
391///
392/// Custom configuration:
393///
394/// ```no_run
395/// use lambda_otel_lite::telemetry::{init_telemetry, TelemetryConfig};
396/// use opentelemetry::KeyValue;
397/// use opentelemetry_sdk::Resource;
398///
399/// # async fn example() -> Result<(), lambda_runtime::Error> {
400/// // Create custom resource
401/// let resource = Resource::builder()
402/// .with_attributes(vec![
403/// KeyValue::new("service.name", "payment-api"),
404/// KeyValue::new("service.version", "1.2.3"),
405/// ])
406/// .build();
407///
408/// // Initialize with custom configuration
409/// let (_, telemetry) = init_telemetry(
410/// TelemetryConfig::builder()
411/// .resource(resource)
412/// .build()
413/// ).await?;
414/// # Ok(())
415/// # }
416/// ```
417///
418/// Advanced usage with BatchSpanProcessor (required for async exporters):
419///
420/// ```no_run
421/// use lambda_otel_lite::{init_telemetry, TelemetryConfig};
422/// use opentelemetry_otlp::{WithExportConfig, WithHttpConfig, Protocol};
423/// use opentelemetry_sdk::trace::BatchSpanProcessor;
424/// use lambda_runtime::Error;
425///
426/// # async fn example() -> Result<(), Error> {
427/// let batch_exporter = opentelemetry_otlp::SpanExporter::builder()
428/// .with_http()
429/// .with_http_client(reqwest::Client::new())
430/// .with_protocol(Protocol::HttpBinary)
431/// .build()?;
432///
433/// let (provider, completion) = init_telemetry(
434/// TelemetryConfig::builder()
435/// .with_span_processor(BatchSpanProcessor::builder(batch_exporter).build())
436/// .build()
437/// ).await?;
438/// # Ok(())
439/// # }
440/// ```
441///
442/// Using LambdaSpanProcessor with blocking http client:
443///
444/// ```no_run
445/// use lambda_otel_lite::{init_telemetry, TelemetryConfig, LambdaSpanProcessor};
446/// use opentelemetry_otlp::{WithExportConfig, WithHttpConfig, Protocol};
447/// use lambda_runtime::Error;
448///
449/// # async fn example() -> Result<(), Error> {
450/// let lambda_exporter = opentelemetry_otlp::SpanExporter::builder()
451/// .with_http()
452/// .with_http_client(reqwest::blocking::Client::new())
453/// .with_protocol(Protocol::HttpBinary)
454/// .build()?;
455///
456/// let (provider, completion) = init_telemetry(
457/// TelemetryConfig::builder()
458/// .with_span_processor(
459/// LambdaSpanProcessor::builder()
460/// .exporter(lambda_exporter)
461/// .max_batch_size(512)
462/// .max_queue_size(2048)
463/// .build()
464/// )
465/// .build()
466/// ).await?;
467/// # Ok(())
468/// # }
469/// ```
470///
471pub async fn init_telemetry(
472 mut config: TelemetryConfig,
473) -> Result<(opentelemetry_sdk::trace::Tracer, TelemetryCompletionHandler), Error> {
474 let mode = ProcessorMode::from_env();
475
476 // Set up the propagator(s)
477 if config.propagators.is_empty() {
478 config
479 .propagators
480 .push(Box::new(TraceContextPropagator::new()));
481 }
482
483 let composite_propagator = TextMapCompositePropagator::new(config.propagators);
484 global::set_text_map_propagator(composite_propagator);
485
486 // Add default span processor if none was added
487 if !config.has_processor {
488 let processor = LambdaSpanProcessor::builder()
489 .exporter(OtlpStdoutSpanExporter::default())
490 .build();
491 config.provider_builder = config.provider_builder.with_span_processor(processor);
492 }
493
494 // Apply defaults and build the provider
495 let resource = config.resource.unwrap_or_else(get_lambda_resource);
496 let provider = Arc::new(config.provider_builder.with_resource(resource).build());
497
498 // Register the extension if in async or finalize mode
499 let sender = match mode {
500 ProcessorMode::Async | ProcessorMode::Finalize => {
501 Some(register_extension(provider.clone(), mode.clone()).await?)
502 }
503 _ => None,
504 };
505
506 if config.set_global_provider {
507 // Set the provider as global
508 set_tracer_provider(provider.as_ref().clone());
509 }
510
511 // Initialize tracing subscriber with smart env var selection
512 let env_var_name = config.env_var_name.as_deref().unwrap_or_else(|| {
513 if env::var("RUST_LOG").is_ok() {
514 "RUST_LOG"
515 } else {
516 "AWS_LAMBDA_LOG_LEVEL"
517 }
518 });
519
520 let env_filter = tracing_subscriber::EnvFilter::builder()
521 .with_env_var(env_var_name)
522 .from_env_lossy();
523
524 let completion_handler = TelemetryCompletionHandler::new(provider.clone(), sender, mode);
525 let tracer = completion_handler.get_tracer().clone();
526
527 let subscriber = tracing_subscriber::registry::Registry::default()
528 .with(tracing_opentelemetry::OpenTelemetryLayer::new(
529 tracer.clone(),
530 ))
531 .with(env_filter);
532
533 // Always initialize the subscriber, with or without fmt layer
534 if config.enable_fmt_layer {
535 let is_json = env::var("AWS_LAMBDA_LOG_FORMAT")
536 .unwrap_or_default()
537 .to_uppercase()
538 == "JSON";
539
540 if is_json {
541 tracing::subscriber::set_global_default(
542 subscriber.with(
543 tracing_subscriber::fmt::layer()
544 .with_target(false)
545 .without_time()
546 .json(),
547 ),
548 )?;
549 } else {
550 tracing::subscriber::set_global_default(
551 subscriber.with(
552 tracing_subscriber::fmt::layer()
553 .with_target(false)
554 .without_time()
555 .with_ansi(false),
556 ),
557 )?;
558 }
559 } else {
560 tracing::subscriber::set_global_default(subscriber)?;
561 }
562
563 Ok((tracer, completion_handler))
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use opentelemetry_sdk::trace::SimpleSpanProcessor;
570 use sealed_test::prelude::*;
571 use tokio::sync::mpsc;
572
573 #[tokio::test]
574 #[sealed_test]
575 async fn test_init_telemetry_defaults() {
576 let (_, completion_handler) = init_telemetry(TelemetryConfig::default()).await.unwrap();
577 assert!(completion_handler.sender.is_none()); // Default mode is Sync
578 }
579
580 #[tokio::test]
581 #[sealed_test]
582 async fn test_init_telemetry_custom() {
583 let resource = Resource::builder().build();
584 let config = TelemetryConfig::builder()
585 .resource(resource)
586 .enable_fmt_layer(true)
587 .set_global_provider(false)
588 .build();
589
590 let (_, completion_handler) = init_telemetry(config).await.unwrap();
591 assert!(completion_handler.sender.is_none());
592 }
593
594 #[test]
595 fn test_telemetry_config_defaults() {
596 let config = TelemetryConfig::builder().build();
597 assert!(config.set_global_provider); // Should be true by default
598 assert!(!config.has_processor);
599 assert!(!config.enable_fmt_layer);
600 }
601
602 #[test]
603 fn test_completion_handler_sync_mode() {
604 let provider = Arc::new(
605 SdkTracerProvider::builder()
606 .with_span_processor(SimpleSpanProcessor::new(Box::new(
607 OtlpStdoutSpanExporter::default(),
608 )))
609 .build(),
610 );
611
612 let handler = TelemetryCompletionHandler::new(provider, None, ProcessorMode::Sync);
613
614 // In sync mode, complete() should call force_flush
615 handler.complete();
616 // Note: We can't easily verify the flush was called since TracerProvider
617 // doesn't expose this information, but we can verify it doesn't panic
618 }
619
620 #[tokio::test]
621 async fn test_completion_handler_async_mode() {
622 let provider = Arc::new(
623 SdkTracerProvider::builder()
624 .with_span_processor(SimpleSpanProcessor::new(Box::new(
625 OtlpStdoutSpanExporter::default(),
626 )))
627 .build(),
628 );
629
630 let (tx, mut rx) = mpsc::unbounded_channel();
631
632 let completion_handler =
633 TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Async);
634
635 // In async mode, complete() should send a message through the channel
636 completion_handler.complete();
637
638 // Verify that we received the completion signal
639 assert!(rx.try_recv().is_ok());
640 // Verify channel is now empty
641 assert!(rx.try_recv().is_err());
642 }
643
644 #[test]
645 fn test_completion_handler_finalize_mode() {
646 let provider = Arc::new(
647 SdkTracerProvider::builder()
648 .with_span_processor(SimpleSpanProcessor::new(Box::new(
649 OtlpStdoutSpanExporter::default(),
650 )))
651 .build(),
652 );
653
654 let (tx, _rx) = mpsc::unbounded_channel();
655
656 let completion_handler =
657 TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Finalize);
658
659 // In finalize mode, complete() should do nothing
660 completion_handler.complete();
661 // Verify it doesn't panic or cause issues
662 }
663
664 #[test]
665 fn test_completion_handler_clone() {
666 let provider = Arc::new(
667 SdkTracerProvider::builder()
668 .with_span_processor(SimpleSpanProcessor::new(Box::new(
669 OtlpStdoutSpanExporter::default(),
670 )))
671 .build(),
672 );
673
674 let (tx, _rx) = mpsc::unbounded_channel();
675
676 let completion_handler =
677 TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Async);
678
679 // Test that Clone is implemented correctly
680 let cloned = completion_handler.clone();
681
682 // Verify both handlers have the same mode
683 assert!(matches!(cloned.mode, ProcessorMode::Async));
684 assert!(cloned.sender.is_some());
685 }
686
687 #[test]
688 fn test_completion_handler_sync_mode_error_handling() {
689 let provider = Arc::new(
690 SdkTracerProvider::builder()
691 .with_span_processor(SimpleSpanProcessor::new(Box::new(
692 OtlpStdoutSpanExporter::default(),
693 )))
694 .build(),
695 );
696
697 let completion_handler =
698 TelemetryCompletionHandler::new(provider, None, ProcessorMode::Sync);
699
700 // Test that complete() doesn't panic
701 completion_handler.complete();
702 }
703
704 #[tokio::test]
705 async fn test_completion_handler_async_mode_error_handling() {
706 let provider = Arc::new(
707 SdkTracerProvider::builder()
708 .with_span_processor(SimpleSpanProcessor::new(Box::new(
709 OtlpStdoutSpanExporter::default(),
710 )))
711 .build(),
712 );
713
714 // Use UnboundedSender instead of Sender
715 let (tx, _rx) = mpsc::unbounded_channel();
716 // Fill the channel by dropping the receiver
717 drop(_rx);
718
719 let completion_handler =
720 TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Async);
721
722 // Test that complete() doesn't panic when receiver is dropped
723 completion_handler.complete();
724 }
725}