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        let enable_fmt_layer = env::var("LAMBDA_TRACING_ENABLE_FMT_LAYER")
294            .map(|val| val.to_lowercase() == "true" || val == "1")
295            .unwrap_or(false);
296
297        Self::builder().enable_fmt_layer(enable_fmt_layer).build()
298    }
299}
300
301/// Builder methods for adding span processors and other configuration
302impl<S: telemetry_config_builder::State> TelemetryConfigBuilder<S> {
303    /// Add a span processor to the tracer provider.
304    ///
305    /// This method allows adding custom span processors for trace data processing.
306    /// Multiple processors can be added by calling this method multiple times.
307    ///
308    /// # Arguments
309    ///
310    /// * `processor` - A span processor implementing the [`SpanProcessor`] trait
311    ///
312    /// # Examples
313    ///
314    /// ```no_run
315    /// use lambda_otel_lite::TelemetryConfig;
316    /// use opentelemetry_sdk::trace::SimpleSpanProcessor;
317    /// use otlp_stdout_span_exporter::OtlpStdoutSpanExporter;
318    ///
319    /// // Only use builder when adding custom processors
320    /// let config = TelemetryConfig::builder()
321    ///     .with_span_processor(SimpleSpanProcessor::new(
322    ///         Box::new(OtlpStdoutSpanExporter::default())
323    ///     ))
324    ///     .build();
325    /// ```
326    pub fn with_span_processor<T>(mut self, processor: T) -> Self
327    where
328        T: SpanProcessor + 'static,
329    {
330        self.provider_builder = self.provider_builder.with_span_processor(processor);
331        self.has_processor = true;
332        self
333    }
334
335    /// Add a propagator to the list of propagators.
336    ///
337    /// Multiple propagators can be added and will be combined into a composite propagator.
338    /// The default propagator is TraceContextPropagator.
339    ///
340    /// # Arguments
341    ///
342    /// * `propagator` - A propagator implementing the [`TextMapPropagator`] trait
343    ///
344    /// # Examples
345    ///
346    /// ```no_run
347    /// use lambda_otel_lite::TelemetryConfig;
348    /// use opentelemetry_sdk::propagation::BaggagePropagator;
349    ///
350    /// let config = TelemetryConfig::builder()
351    ///     .with_propagator(BaggagePropagator::new())
352    ///     .build();
353    /// ```
354    pub fn with_propagator<T>(mut self, propagator: T) -> Self
355    where
356        T: TextMapPropagator + Send + Sync + 'static,
357    {
358        self.propagators.push(Box::new(propagator));
359        self
360    }
361}
362
363/// Initialize OpenTelemetry for AWS Lambda with the provided configuration.
364///
365/// # Arguments
366///
367/// * `config` - Configuration for telemetry initialization
368///
369/// # Returns
370///
371/// Returns a tuple containing:
372/// - A tracer instance for manual instrumentation
373/// - A completion handler for managing span export timing
374///
375/// # Errors
376///
377/// Returns error if:
378/// - Extension registration fails (async/finalize modes)
379/// - Tracer provider initialization fails
380/// - Environment variable parsing fails
381///
382/// # Examples
383///
384/// Basic usage with default configuration:
385///
386/// ```no_run
387/// use lambda_otel_lite::telemetry::{init_telemetry, TelemetryConfig};
388///
389/// # async fn example() -> Result<(), lambda_runtime::Error> {
390/// // Initialize with default configuration
391/// let (_, telemetry) = init_telemetry(TelemetryConfig::default()).await?;
392/// # Ok(())
393/// # }
394/// ```
395///
396/// Custom configuration:
397///
398/// ```no_run
399/// use lambda_otel_lite::telemetry::{init_telemetry, TelemetryConfig};
400/// use opentelemetry::KeyValue;
401/// use opentelemetry_sdk::Resource;
402///
403/// # async fn example() -> Result<(), lambda_runtime::Error> {
404/// // Create custom resource
405/// let resource = Resource::builder()
406///     .with_attributes(vec![
407///         KeyValue::new("service.name", "payment-api"),
408///         KeyValue::new("service.version", "1.2.3"),
409///     ])
410///     .build();
411///
412/// // Initialize with custom configuration
413/// let (_, telemetry) = init_telemetry(
414///     TelemetryConfig::builder()
415///         .resource(resource)
416///         .build()
417/// ).await?;
418/// # Ok(())
419/// # }
420/// ```
421///
422/// Advanced usage with BatchSpanProcessor (required for async exporters):
423///
424/// ```no_run
425/// use lambda_otel_lite::{init_telemetry, TelemetryConfig};
426/// use opentelemetry_otlp::{WithExportConfig, WithHttpConfig, Protocol};
427/// use opentelemetry_sdk::trace::BatchSpanProcessor;
428/// use lambda_runtime::Error;
429///
430/// # async fn example() -> Result<(), Error> {
431/// let batch_exporter = opentelemetry_otlp::SpanExporter::builder()
432///     .with_http()
433///     .with_http_client(reqwest::Client::new())
434///     .with_protocol(Protocol::HttpBinary)
435///     .build()?;
436///
437/// let (provider, completion) = init_telemetry(
438///     TelemetryConfig::builder()
439///         .with_span_processor(BatchSpanProcessor::builder(batch_exporter).build())
440///         .build()
441/// ).await?;
442/// # Ok(())
443/// # }
444/// ```
445///
446/// Using LambdaSpanProcessor with blocking http client:
447///
448/// ```no_run
449/// use lambda_otel_lite::{init_telemetry, TelemetryConfig, LambdaSpanProcessor};
450/// use opentelemetry_otlp::{WithExportConfig, WithHttpConfig, Protocol};
451/// use lambda_runtime::Error;
452///
453/// # async fn example() -> Result<(), Error> {
454/// let lambda_exporter = opentelemetry_otlp::SpanExporter::builder()
455///     .with_http()
456///     .with_http_client(reqwest::blocking::Client::new())
457///     .with_protocol(Protocol::HttpBinary)
458///     .build()?;
459///
460/// let (provider, completion) = init_telemetry(
461///     TelemetryConfig::builder()
462///         .with_span_processor(
463///             LambdaSpanProcessor::builder()
464///                 .exporter(lambda_exporter)
465///                 .max_batch_size(512)
466///                 .max_queue_size(2048)
467///                 .build()
468///         )
469///         .build()
470/// ).await?;
471/// # Ok(())
472/// # }
473/// ```
474///
475pub async fn init_telemetry(
476    mut config: TelemetryConfig,
477) -> Result<(opentelemetry_sdk::trace::Tracer, TelemetryCompletionHandler), Error> {
478    let mode = ProcessorMode::from_env();
479
480    // Set up the propagator(s)
481    if config.propagators.is_empty() {
482        config
483            .propagators
484            .push(Box::new(TraceContextPropagator::new()));
485    }
486
487    let composite_propagator = TextMapCompositePropagator::new(config.propagators);
488    global::set_text_map_propagator(composite_propagator);
489
490    // Add default span processor if none was added
491    if !config.has_processor {
492        let processor = LambdaSpanProcessor::builder()
493            .exporter(OtlpStdoutSpanExporter::default())
494            .build();
495        config.provider_builder = config.provider_builder.with_span_processor(processor);
496    }
497
498    // Apply defaults and build the provider
499    let resource = config.resource.unwrap_or_else(get_lambda_resource);
500    let provider = Arc::new(config.provider_builder.with_resource(resource).build());
501
502    // Register the extension if in async or finalize mode
503    let sender = match mode {
504        ProcessorMode::Async | ProcessorMode::Finalize => {
505            Some(register_extension(provider.clone(), mode.clone()).await?)
506        }
507        _ => None,
508    };
509
510    if config.set_global_provider {
511        // Set the provider as global
512        set_tracer_provider(provider.as_ref().clone());
513    }
514
515    // Initialize tracing subscriber with smart env var selection
516    let env_var_name = config.env_var_name.as_deref().unwrap_or_else(|| {
517        if env::var("RUST_LOG").is_ok() {
518            "RUST_LOG"
519        } else {
520            "AWS_LAMBDA_LOG_LEVEL"
521        }
522    });
523
524    let env_filter = tracing_subscriber::EnvFilter::builder()
525        .with_env_var(env_var_name)
526        .from_env_lossy();
527
528    let completion_handler = TelemetryCompletionHandler::new(provider.clone(), sender, mode);
529    let tracer = completion_handler.get_tracer().clone();
530
531    let subscriber = tracing_subscriber::registry::Registry::default()
532        .with(tracing_opentelemetry::OpenTelemetryLayer::new(
533            tracer.clone(),
534        ))
535        .with(env_filter);
536
537    // Always initialize the subscriber, with or without fmt layer
538    if config.enable_fmt_layer {
539        // Determine if the lambda logging configuration is set to output json logs
540        let is_json = env::var("AWS_LAMBDA_LOG_FORMAT")
541            .unwrap_or_default()
542            .to_uppercase()
543            == "JSON";
544
545        if is_json {
546            tracing::subscriber::set_global_default(
547                subscriber.with(
548                    tracing_subscriber::fmt::layer()
549                        .with_target(false)
550                        .without_time()
551                        .json(),
552                ),
553            )?;
554        } else {
555            tracing::subscriber::set_global_default(
556                subscriber.with(
557                    tracing_subscriber::fmt::layer()
558                        .with_target(false)
559                        .without_time()
560                        .with_ansi(false),
561                ),
562            )?;
563        }
564    } else {
565        tracing::subscriber::set_global_default(subscriber)?;
566    }
567
568    Ok((tracer, completion_handler))
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use opentelemetry_sdk::trace::SimpleSpanProcessor;
575    use sealed_test::prelude::*;
576    use tokio::sync::mpsc;
577
578    #[tokio::test]
579    #[sealed_test]
580    async fn test_init_telemetry_defaults() {
581        let (_, completion_handler) = init_telemetry(TelemetryConfig::default()).await.unwrap();
582        assert!(completion_handler.sender.is_none()); // Default mode is Sync
583    }
584
585    #[tokio::test]
586    #[sealed_test]
587    async fn test_init_telemetry_custom() {
588        let resource = Resource::builder().build();
589        let config = TelemetryConfig::builder()
590            .resource(resource)
591            .enable_fmt_layer(true)
592            .set_global_provider(false)
593            .build();
594
595        let (_, completion_handler) = init_telemetry(config).await.unwrap();
596        assert!(completion_handler.sender.is_none());
597    }
598
599    #[test]
600    fn test_telemetry_config_defaults() {
601        // Ensure environment variable is not set for this test
602        env::remove_var("LAMBDA_TRACING_ENABLE_FMT_LAYER");
603
604        let config = TelemetryConfig::builder().build();
605        assert!(config.set_global_provider); // Should be true by default
606        assert!(!config.has_processor);
607        assert!(!config.enable_fmt_layer);
608    }
609
610    #[test]
611    fn test_telemetry_config_env_fmt_layer() {
612        // Test with environment variable set to true
613        env::set_var("LAMBDA_TRACING_ENABLE_FMT_LAYER", "true");
614        let config = TelemetryConfig::default();
615        assert!(config.enable_fmt_layer);
616
617        // Test with environment variable set to 1
618        env::set_var("LAMBDA_TRACING_ENABLE_FMT_LAYER", "1");
619        let config = TelemetryConfig::default();
620        assert!(config.enable_fmt_layer);
621
622        // Test with environment variable set to false
623        env::set_var("LAMBDA_TRACING_ENABLE_FMT_LAYER", "false");
624        let config = TelemetryConfig::default();
625        assert!(!config.enable_fmt_layer);
626
627        // Clean up
628        env::remove_var("LAMBDA_TRACING_ENABLE_FMT_LAYER");
629    }
630
631    #[test]
632    fn test_completion_handler_sync_mode() {
633        let provider = Arc::new(
634            SdkTracerProvider::builder()
635                .with_span_processor(SimpleSpanProcessor::new(Box::new(
636                    OtlpStdoutSpanExporter::default(),
637                )))
638                .build(),
639        );
640
641        let handler = TelemetryCompletionHandler::new(provider, None, ProcessorMode::Sync);
642
643        // In sync mode, complete() should call force_flush
644        handler.complete();
645        // Note: We can't easily verify the flush was called since TracerProvider
646        // doesn't expose this information, but we can verify it doesn't panic
647    }
648
649    #[tokio::test]
650    async fn test_completion_handler_async_mode() {
651        let provider = Arc::new(
652            SdkTracerProvider::builder()
653                .with_span_processor(SimpleSpanProcessor::new(Box::new(
654                    OtlpStdoutSpanExporter::default(),
655                )))
656                .build(),
657        );
658
659        let (tx, mut rx) = mpsc::unbounded_channel();
660
661        let completion_handler =
662            TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Async);
663
664        // In async mode, complete() should send a message through the channel
665        completion_handler.complete();
666
667        // Verify that we received the completion signal
668        assert!(rx.try_recv().is_ok());
669        // Verify channel is now empty
670        assert!(rx.try_recv().is_err());
671    }
672
673    #[test]
674    fn test_completion_handler_finalize_mode() {
675        let provider = Arc::new(
676            SdkTracerProvider::builder()
677                .with_span_processor(SimpleSpanProcessor::new(Box::new(
678                    OtlpStdoutSpanExporter::default(),
679                )))
680                .build(),
681        );
682
683        let (tx, _rx) = mpsc::unbounded_channel();
684
685        let completion_handler =
686            TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Finalize);
687
688        // In finalize mode, complete() should do nothing
689        completion_handler.complete();
690        // Verify it doesn't panic or cause issues
691    }
692
693    #[test]
694    fn test_completion_handler_clone() {
695        let provider = Arc::new(
696            SdkTracerProvider::builder()
697                .with_span_processor(SimpleSpanProcessor::new(Box::new(
698                    OtlpStdoutSpanExporter::default(),
699                )))
700                .build(),
701        );
702
703        let (tx, _rx) = mpsc::unbounded_channel();
704
705        let completion_handler =
706            TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Async);
707
708        // Test that Clone is implemented correctly
709        let cloned = completion_handler.clone();
710
711        // Verify both handlers have the same mode
712        assert!(matches!(cloned.mode, ProcessorMode::Async));
713        assert!(cloned.sender.is_some());
714    }
715
716    #[test]
717    fn test_completion_handler_sync_mode_error_handling() {
718        let provider = Arc::new(
719            SdkTracerProvider::builder()
720                .with_span_processor(SimpleSpanProcessor::new(Box::new(
721                    OtlpStdoutSpanExporter::default(),
722                )))
723                .build(),
724        );
725
726        let completion_handler =
727            TelemetryCompletionHandler::new(provider, None, ProcessorMode::Sync);
728
729        // Test that complete() doesn't panic
730        completion_handler.complete();
731    }
732
733    #[tokio::test]
734    async fn test_completion_handler_async_mode_error_handling() {
735        let provider = Arc::new(
736            SdkTracerProvider::builder()
737                .with_span_processor(SimpleSpanProcessor::new(Box::new(
738                    OtlpStdoutSpanExporter::default(),
739                )))
740                .build(),
741        );
742
743        // Use UnboundedSender instead of Sender
744        let (tx, _rx) = mpsc::unbounded_channel();
745        // Fill the channel by dropping the receiver
746        drop(_rx);
747
748        let completion_handler =
749            TelemetryCompletionHandler::new(provider, Some(tx), ProcessorMode::Async);
750
751        // Test that complete() doesn't panic when receiver is dropped
752        completion_handler.complete();
753    }
754}