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        println!("Completing telemetry");
175        match self.mode {
176            ProcessorMode::Sync => {
177                if let Err(e) = self.provider.force_flush() {
178                    tracing::warn!(error = ?e, "Error flushing telemetry");
179                }
180            }
181            ProcessorMode::Async => {
182                if let Some(sender) = &self.sender {
183                    if let Err(e) = sender.send(()) {
184                        tracing::warn!(error = ?e, "Failed to send completion signal to extension");
185                    }
186                }
187            }
188            ProcessorMode::Finalize => {
189                // Do nothing, handled by drop
190            }
191        }
192    }
193}
194
195/// Configuration for OpenTelemetry initialization.
196///
197/// Provides configuration options for telemetry setup. Use `TelemetryConfig::default()`
198/// for standard Lambda configuration, or the builder pattern for customization.
199///
200/// # Fields
201///
202/// * `enable_fmt_layer` - Enable console output for debugging (default: false)
203/// * `set_global_provider` - Set as global tracer provider (default: true)
204/// * `resource` - Custom resource attributes (default: auto-detected from Lambda)
205/// * `env_var_name` - Environment variable name for log level configuration
206///
207/// # Examples
208///
209/// Basic usage with default configuration:
210///
211/// ```no_run
212/// use lambda_otel_lite::telemetry::TelemetryConfig;
213///
214/// let config = TelemetryConfig::default();
215/// ```
216///
217/// Custom configuration with resource attributes:
218///
219/// ```no_run
220/// use lambda_otel_lite::telemetry::TelemetryConfig;
221/// use opentelemetry::KeyValue;
222/// use opentelemetry_sdk::Resource;
223///
224/// let config = TelemetryConfig::builder()
225///     .resource(Resource::builder()
226///         .with_attributes(vec![KeyValue::new("version", "1.0.0")])
227///         .build())
228///     .build();
229/// ```
230///
231/// Custom configuration with logging options:
232///
233/// ```no_run
234/// use lambda_otel_lite::telemetry::TelemetryConfig;
235///
236/// let config = TelemetryConfig::builder()
237///     .enable_fmt_layer(true)  // Enable console output for debugging
238///     .env_var_name("MY_CUSTOM_LOG_LEVEL".to_string())  // Custom env var for log level
239///     .build();
240/// ```
241#[derive(Builder, Debug)]
242pub struct TelemetryConfig {
243    // Custom fields for internal state
244    #[builder(field)]
245    provider_builder: TracerProviderBuilder,
246
247    #[builder(field)]
248    has_processor: bool,
249
250    #[builder(field)]
251    propagators: Vec<Box<dyn TextMapPropagator + Send + Sync>>,
252
253    /// Enable console output for debugging.
254    ///
255    /// When enabled, spans and events will be printed to the console in addition
256    /// to being exported through the configured span processors. This is useful
257    /// for debugging but adds overhead and should be disabled in production.
258    ///
259    /// Default: `false`
260    #[builder(default = false)]
261    pub enable_fmt_layer: bool,
262
263    /// Set this provider as the global OpenTelemetry provider.
264    ///
265    /// When enabled, the provider will be registered as the global provider
266    /// for the OpenTelemetry API. This allows using the global tracer API
267    /// without explicitly passing around the provider.
268    ///
269    /// Default: `true`
270    #[builder(default = true)]
271    pub set_global_provider: bool,
272
273    /// Custom resource attributes for all spans.
274    ///
275    /// If not provided, resource attributes will be automatically detected
276    /// from the Lambda environment. Custom resources will override any
277    /// automatically detected attributes with the same keys.
278    ///
279    /// Default: `None` (auto-detected from Lambda environment)
280    pub resource: Option<Resource>,
281
282    /// Environment variable name to use for log level configuration.
283    ///
284    /// This field specifies which environment variable should be used to configure
285    /// the tracing subscriber's log level filter. If not specified, the system will
286    /// first check for `RUST_LOG` and then fall back to `AWS_LAMBDA_LOG_LEVEL`.
287    ///
288    /// Default: `None` (uses `RUST_LOG` or `AWS_LAMBDA_LOG_LEVEL`)
289    pub env_var_name: Option<String>,
290}
291
292impl Default for TelemetryConfig {
293    fn default() -> Self {
294        Self::builder().build()
295    }
296}
297
298/// Builder methods for adding span processors and other configuration
299impl<S: telemetry_config_builder::State> TelemetryConfigBuilder<S> {
300    /// Add a span processor to the tracer provider.
301    ///
302    /// This method allows adding custom span processors for trace data processing.
303    /// Multiple processors can be added by calling this method multiple times.
304    ///
305    /// # Arguments
306    ///
307    /// * `processor` - A span processor implementing the [`SpanProcessor`] trait
308    ///
309    /// # Examples
310    ///
311    /// ```no_run
312    /// use lambda_otel_lite::TelemetryConfig;
313    /// use opentelemetry_sdk::trace::SimpleSpanProcessor;
314    /// use otlp_stdout_span_exporter::OtlpStdoutSpanExporter;
315    ///
316    /// // Only use builder when adding custom processors
317    /// let config = TelemetryConfig::builder()
318    ///     .with_span_processor(SimpleSpanProcessor::new(
319    ///         Box::new(OtlpStdoutSpanExporter::default())
320    ///     ))
321    ///     .build();
322    /// ```
323    pub fn with_span_processor<T>(mut self, processor: T) -> Self
324    where
325        T: SpanProcessor + 'static,
326    {
327        self.provider_builder = self.provider_builder.with_span_processor(processor);
328        self.has_processor = true;
329        self
330    }
331
332    /// Add a propagator to the list of propagators.
333    ///
334    /// Multiple propagators can be added and will be combined into a composite propagator.
335    /// The default propagator is TraceContextPropagator.
336    ///
337    /// # Arguments
338    ///
339    /// * `propagator` - A propagator implementing the [`TextMapPropagator`] trait
340    ///
341    /// # Examples
342    ///
343    /// ```no_run
344    /// use lambda_otel_lite::TelemetryConfig;
345    /// use opentelemetry_sdk::propagation::BaggagePropagator;
346    ///
347    /// let config = TelemetryConfig::builder()
348    ///     .with_propagator(BaggagePropagator::new())
349    ///     .build();
350    /// ```
351    pub fn with_propagator<T>(mut self, propagator: T) -> Self
352    where
353        T: TextMapPropagator + Send + Sync + 'static,
354    {
355        self.propagators.push(Box::new(propagator));
356        self
357    }
358}
359
360/// Initialize OpenTelemetry for AWS Lambda with the provided configuration.
361///
362/// # Arguments
363///
364/// * `config` - Configuration for telemetry initialization
365///
366/// # Returns
367///
368/// Returns a tuple containing:
369/// - A tracer instance for manual instrumentation
370/// - A completion handler for managing span export timing
371///
372/// # Errors
373///
374/// Returns error if:
375/// - Extension registration fails (async/finalize modes)
376/// - Tracer provider initialization fails
377/// - Environment variable parsing fails
378///
379/// # Examples
380///
381/// Basic usage with default configuration:
382///
383/// ```no_run
384/// use lambda_otel_lite::telemetry::{init_telemetry, TelemetryConfig};
385///
386/// # async fn example() -> Result<(), lambda_runtime::Error> {
387/// // Initialize with default configuration
388/// let (_, telemetry) = init_telemetry(TelemetryConfig::default()).await?;
389/// # Ok(())
390/// # }
391/// ```
392///
393/// Custom configuration:
394///
395/// ```no_run
396/// use lambda_otel_lite::telemetry::{init_telemetry, TelemetryConfig};
397/// use opentelemetry::KeyValue;
398/// use opentelemetry_sdk::Resource;
399///
400/// # async fn example() -> Result<(), lambda_runtime::Error> {
401/// // Create custom resource
402/// let resource = Resource::builder()
403///     .with_attributes(vec![
404///         KeyValue::new("service.name", "payment-api"),
405///         KeyValue::new("service.version", "1.2.3"),
406///     ])
407///     .build();
408///
409/// // Initialize with custom configuration
410/// let (_, telemetry) = init_telemetry(
411///     TelemetryConfig::builder()
412///         .resource(resource)
413///         .build()
414/// ).await?;
415/// # Ok(())
416/// # }
417/// ```
418///
419/// Advanced usage with BatchSpanProcessor (required for async exporters):
420///
421/// ```no_run
422/// use lambda_otel_lite::{init_telemetry, TelemetryConfig};
423/// use opentelemetry_otlp::{WithExportConfig, WithHttpConfig, Protocol};
424/// use opentelemetry_sdk::trace::BatchSpanProcessor;
425/// use lambda_runtime::Error;
426///
427/// # async fn example() -> Result<(), Error> {
428/// let batch_exporter = opentelemetry_otlp::SpanExporter::builder()
429///     .with_http()
430///     .with_http_client(reqwest::Client::new())
431///     .with_protocol(Protocol::HttpBinary)
432///     .build()?;
433///
434/// let (provider, completion) = init_telemetry(
435///     TelemetryConfig::builder()
436///         .with_span_processor(BatchSpanProcessor::builder(batch_exporter).build())
437///         .build()
438/// ).await?;
439/// # Ok(())
440/// # }
441/// ```
442///
443/// Using LambdaSpanProcessor with blocking http client:
444///
445/// ```no_run
446/// use lambda_otel_lite::{init_telemetry, TelemetryConfig, LambdaSpanProcessor};
447/// use opentelemetry_otlp::{WithExportConfig, WithHttpConfig, Protocol};
448/// use lambda_runtime::Error;
449///
450/// # async fn example() -> Result<(), Error> {
451/// let lambda_exporter = opentelemetry_otlp::SpanExporter::builder()
452///     .with_http()
453///     .with_http_client(reqwest::blocking::Client::new())
454///     .with_protocol(Protocol::HttpBinary)
455///     .build()?;
456///
457/// let (provider, completion) = init_telemetry(
458///     TelemetryConfig::builder()
459///         .with_span_processor(
460///             LambdaSpanProcessor::builder()
461///                 .exporter(lambda_exporter)
462///                 .max_batch_size(512)
463///                 .max_queue_size(2048)
464///                 .build()
465///         )
466///         .build()
467/// ).await?;
468/// # Ok(())
469/// # }
470/// ```
471///
472pub async fn init_telemetry(
473    mut config: TelemetryConfig,
474) -> Result<(opentelemetry_sdk::trace::Tracer, TelemetryCompletionHandler), Error> {
475    let mode = ProcessorMode::from_env();
476
477    // Set up the propagator(s)
478    if config.propagators.is_empty() {
479        config
480            .propagators
481            .push(Box::new(TraceContextPropagator::new()));
482    }
483
484    let composite_propagator = TextMapCompositePropagator::new(config.propagators);
485    global::set_text_map_propagator(composite_propagator);
486
487    // Add default span processor if none was added
488    if !config.has_processor {
489        let processor = LambdaSpanProcessor::builder()
490            .exporter(OtlpStdoutSpanExporter::default())
491            .build();
492        config.provider_builder = config.provider_builder.with_span_processor(processor);
493    }
494
495    // Apply defaults and build the provider
496    let resource = config.resource.unwrap_or_else(get_lambda_resource);
497    let provider = Arc::new(config.provider_builder.with_resource(resource).build());
498
499    // Register the extension if in async or finalize mode
500    let sender = match mode {
501        ProcessorMode::Async | ProcessorMode::Finalize => {
502            Some(register_extension(provider.clone(), mode.clone()).await?)
503        }
504        _ => None,
505    };
506
507    if config.set_global_provider {
508        // Set the provider as global
509        set_tracer_provider(provider.as_ref().clone());
510    }
511
512    // Initialize tracing subscriber with smart env var selection
513    let env_var_name = config.env_var_name.as_deref().unwrap_or_else(|| {
514        if env::var("RUST_LOG").is_ok() {
515            "RUST_LOG"
516        } else {
517            "AWS_LAMBDA_LOG_LEVEL"
518        }
519    });
520
521    let env_filter = tracing_subscriber::EnvFilter::builder()
522        .with_env_var(env_var_name)
523        .from_env_lossy();
524
525    let completion_handler = TelemetryCompletionHandler::new(provider.clone(), sender, mode);
526    let tracer = completion_handler.get_tracer().clone();
527
528    let subscriber = tracing_subscriber::registry::Registry::default()
529        .with(tracing_opentelemetry::OpenTelemetryLayer::new(
530            tracer.clone(),
531        ))
532        .with(env_filter);
533
534    // Always initialize the subscriber, with or without fmt layer
535    if config.enable_fmt_layer {
536        let is_json = env::var("AWS_LAMBDA_LOG_FORMAT")
537            .unwrap_or_default()
538            .to_uppercase()
539            == "JSON";
540
541        if is_json {
542            tracing::subscriber::set_global_default(
543                subscriber.with(
544                    tracing_subscriber::fmt::layer()
545                        .with_target(false)
546                        .without_time()
547                        .json(),
548                ),
549            )?;
550        } else {
551            tracing::subscriber::set_global_default(
552                subscriber.with(
553                    tracing_subscriber::fmt::layer()
554                        .with_target(false)
555                        .without_time()
556                        .with_ansi(false),
557                ),
558            )?;
559        }
560    } else {
561        tracing::subscriber::set_global_default(subscriber)?;
562    }
563
564    Ok((tracer, completion_handler))
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use opentelemetry_sdk::trace::SimpleSpanProcessor;
571    use sealed_test::prelude::*;
572    use tokio::sync::mpsc;
573
574    #[tokio::test]
575    #[sealed_test]
576    async fn test_init_telemetry_defaults() {
577        let (_, completion_handler) = init_telemetry(TelemetryConfig::default()).await.unwrap();
578        assert!(completion_handler.sender.is_none()); // Default mode is Sync
579    }
580
581    #[tokio::test]
582    #[sealed_test]
583    async fn test_init_telemetry_custom() {
584        let resource = Resource::builder().build();
585        let config = TelemetryConfig::builder()
586            .resource(resource)
587            .enable_fmt_layer(true)
588            .set_global_provider(false)
589            .build();
590
591        let (_, completion_handler) = init_telemetry(config).await.unwrap();
592        assert!(completion_handler.sender.is_none());
593    }
594
595    #[test]
596    fn test_telemetry_config_defaults() {
597        let config = TelemetryConfig::builder().build();
598        assert!(config.set_global_provider); // Should be true by default
599        assert!(!config.has_processor);
600        assert!(!config.enable_fmt_layer);
601    }
602
603    #[test]
604    fn test_completion_handler_sync_mode() {
605        let provider = Arc::new(
606            SdkTracerProvider::builder()
607                .with_span_processor(SimpleSpanProcessor::new(Box::new(
608                    OtlpStdoutSpanExporter::default(),
609                )))
610                .build(),
611        );
612
613        let handler = TelemetryCompletionHandler::new(provider, None, ProcessorMode::Sync);
614
615        // In sync mode, complete() should call force_flush
616        handler.complete();
617        // Note: We can't easily verify the flush was called since TracerProvider
618        // doesn't expose this information, but we can verify it doesn't panic
619    }
620
621    #[tokio::test]
622    async fn test_completion_handler_async_mode() {
623        let provider = Arc::new(
624            SdkTracerProvider::builder()
625                .with_span_processor(SimpleSpanProcessor::new(Box::new(
626                    OtlpStdoutSpanExporter::default(),
627                )))
628                .build(),
629        );
630
631        let (tx, mut rx) = mpsc::unbounded_channel();
632
633        let completion_handler =
634            TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Async);
635
636        // In async mode, complete() should send a message through the channel
637        completion_handler.complete();
638
639        // Verify that we received the completion signal
640        assert!(rx.try_recv().is_ok());
641        // Verify channel is now empty
642        assert!(rx.try_recv().is_err());
643    }
644
645    #[test]
646    fn test_completion_handler_finalize_mode() {
647        let provider = Arc::new(
648            SdkTracerProvider::builder()
649                .with_span_processor(SimpleSpanProcessor::new(Box::new(
650                    OtlpStdoutSpanExporter::default(),
651                )))
652                .build(),
653        );
654
655        let (tx, _rx) = mpsc::unbounded_channel();
656
657        let completion_handler =
658            TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Finalize);
659
660        // In finalize mode, complete() should do nothing
661        completion_handler.complete();
662        // Verify it doesn't panic or cause issues
663    }
664
665    #[test]
666    fn test_completion_handler_clone() {
667        let provider = Arc::new(
668            SdkTracerProvider::builder()
669                .with_span_processor(SimpleSpanProcessor::new(Box::new(
670                    OtlpStdoutSpanExporter::default(),
671                )))
672                .build(),
673        );
674
675        let (tx, _rx) = mpsc::unbounded_channel();
676
677        let completion_handler =
678            TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Async);
679
680        // Test that Clone is implemented correctly
681        let cloned = completion_handler.clone();
682
683        // Verify both handlers have the same mode
684        assert!(matches!(cloned.mode, ProcessorMode::Async));
685        assert!(cloned.sender.is_some());
686    }
687
688    #[test]
689    fn test_completion_handler_sync_mode_error_handling() {
690        let provider = Arc::new(
691            SdkTracerProvider::builder()
692                .with_span_processor(SimpleSpanProcessor::new(Box::new(
693                    OtlpStdoutSpanExporter::default(),
694                )))
695                .build(),
696        );
697
698        let completion_handler =
699            TelemetryCompletionHandler::new(provider, None, ProcessorMode::Sync);
700
701        // Test that complete() doesn't panic
702        completion_handler.complete();
703    }
704
705    #[tokio::test]
706    async fn test_completion_handler_async_mode_error_handling() {
707        let provider = Arc::new(
708            SdkTracerProvider::builder()
709                .with_span_processor(SimpleSpanProcessor::new(Box::new(
710                    OtlpStdoutSpanExporter::default(),
711                )))
712                .build(),
713        );
714
715        // Use UnboundedSender instead of Sender
716        let (tx, _rx) = mpsc::unbounded_channel();
717        // Fill the channel by dropping the receiver
718        drop(_rx);
719
720        let completion_handler =
721            TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Async);
722
723        // Test that complete() doesn't panic when receiver is dropped
724        completion_handler.complete();
725    }
726}