otlp_stdout_span_exporter/
lib.rs

1//! A span exporter that writes OpenTelemetry spans to stdout in OTLP format.
2//!
3//! This crate provides an implementation of OpenTelemetry's [`SpanExporter`] that writes spans to stdout
4//! in OTLP (OpenTelemetry Protocol) format. It is particularly useful in serverless environments like
5//! AWS Lambda where writing to stdout is a common pattern for exporting telemetry data.
6//!
7//! # Features
8//!
9//! - Uses OTLP Protobuf serialization for efficient encoding
10//! - Applies GZIP compression with configurable levels
11//! - Detects service name from environment variables
12//! - Supports custom headers via environment variables
13//! - Supports writing to stdout or named pipe
14//! - Consistent JSON output format
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use opentelemetry::global;
20//! use opentelemetry::trace::Tracer;
21//! use opentelemetry_sdk::{trace::SdkTracerProvider, Resource};
22//! use otlp_stdout_span_exporter::OtlpStdoutSpanExporter;
23//!
24//! #[tokio::main]
25//! async fn main() {
26//!     // Create a new stdout exporter with default configuration (stdout output)
27//!     let exporter = OtlpStdoutSpanExporter::default();
28//!
29//!     // Or create one that writes to a named pipe
30//!     let pipe_exporter = OtlpStdoutSpanExporter::builder()
31//!         .pipe(true)  // Will write to /tmp/otlp-stdout-span-exporter.pipe
32//!         .build();
33//!
34//!     // Create a new tracer provider with batch export
35//!     let provider = SdkTracerProvider::builder()
36//!         .with_batch_exporter(exporter)
37//!         .build();
38//!
39//!     // Register the provider with the OpenTelemetry global API
40//!     global::set_tracer_provider(provider.clone());
41//!
42//!     // Create a tracer
43//!     let tracer = global::tracer("my-service");
44//!
45//!     // Create spans
46//!     tracer.in_span("parent-operation", |_cx| {
47//!         println!("Doing work...");
48//!         
49//!         // Create nested spans
50//!         tracer.in_span("child-operation", |_cx| {
51//!             println!("Doing more work...");
52//!         });
53//!     });
54//!     
55//!     // Flush the provider to ensure all spans are exported
56//!     if let Err(err) = provider.force_flush() {
57//!         println!("Error flushing provider: {:?}", err);
58//!     }
59//! }
60//! ```
61//!
62//! # Environment Variables
63//!
64//! The exporter respects the following environment variables:
65//!
66//! - `OTEL_SERVICE_NAME`: Service name to use in output
67//! - `AWS_LAMBDA_FUNCTION_NAME`: Fallback service name (if `OTEL_SERVICE_NAME` not set)
68//! - `OTEL_EXPORTER_OTLP_HEADERS`: Global headers for OTLP export
69//! - `OTEL_EXPORTER_OTLP_TRACES_HEADERS`: Trace-specific headers (takes precedence if conflicting with `OTEL_EXPORTER_OTLP_HEADERS`)
70//! - `OTLP_STDOUT_SPAN_EXPORTER_COMPRESSION_LEVEL`: GZIP compression level (0-9, default: 6)
71//! - `OTLP_STDOUT_SPAN_EXPORTER_OUTPUT_TYPE`: Output type ("pipe" or "stdout", default: "stdout")
72//!
73//! # Configuration Precedence
74//!
75//! All configuration values follow this strict precedence order:
76//!
77//! 1. Environment variables (highest precedence)
78//! 2. Constructor parameters
79//! 3. Default values (lowest precedence)
80//!
81//! For example, when determining the output type:
82//!
83//! ```rust
84//! use otlp_stdout_span_exporter::OtlpStdoutSpanExporter;
85//!
86//! // This will use OTLP_STDOUT_SPAN_EXPORTER_OUTPUT_TYPE if set,
87//! // otherwise it will write to a named pipe as specified in the constructor
88//! let pipe_exporter = OtlpStdoutSpanExporter::builder()
89//!     .pipe(true)
90//!     .build();
91//!
92//! // This will use the environment variable if set, or default to stdout
93//! let default_exporter = OtlpStdoutSpanExporter::default();
94//! ```
95//!
96//! # Output Format
97//!
98//! The exporter writes each batch of spans as a JSON object to stdout or the named pipe:
99//!
100//! ```json
101//! {
102//!   "__otel_otlp_stdout": "0.1.0",
103//!   "source": "my-service",
104//!   "endpoint": "http://localhost:4318/v1/traces",
105//!   "method": "POST",
106//!   "content-type": "application/x-protobuf",
107//!   "content-encoding": "gzip",
108//!   "headers": {
109//!     "api-key": "secret123",
110//!     "custom-header": "value"
111//!   },
112//!   "payload": "<base64-encoded-gzipped-protobuf>",
113//!   "base64": true
114//! }
115//! ```
116
117use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
118use bon::bon;
119use flate2::{write::GzEncoder, Compression};
120use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
121use opentelemetry_proto::transform::common::tonic::ResourceAttributesWithSchema;
122use opentelemetry_proto::transform::trace::tonic::group_spans_by_resource_and_scope;
123use opentelemetry_sdk::error::OTelSdkResult;
124use opentelemetry_sdk::resource::Resource;
125use opentelemetry_sdk::{
126    error::OTelSdkError,
127    trace::{SpanData, SpanExporter},
128};
129use prost::Message;
130use serde::{Deserialize, Serialize};
131use std::{
132    collections::HashMap,
133    env,
134    fmt::{self, Display},
135    fs::OpenOptions,
136    io::{self, Write},
137    path::PathBuf,
138    result::Result,
139    str::FromStr,
140    sync::{Arc, Mutex},
141};
142
143mod constants;
144use constants::{defaults, env_vars};
145
146// Make the constants module and its sub-modules publicly available
147pub mod consts {
148    //! Constants used by the exporter.
149    //!
150    //! This module provides constants for environment variables,
151    //! default values, and resource attributes.
152
153    pub use crate::constants::defaults;
154    pub use crate::constants::env_vars;
155    pub use crate::constants::resource_attributes;
156}
157
158const VERSION: &str = env!("CARGO_PKG_VERSION");
159
160/// Log level for the exported spans
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
162pub enum LogLevel {
163    /// Debug level
164    Debug,
165    /// Info level (default)
166    #[default]
167    Info,
168    /// Warning level
169    Warn,
170    /// Error level (least verbose)
171    Error,
172}
173
174impl FromStr for LogLevel {
175    type Err = String;
176
177    fn from_str(s: &str) -> Result<Self, Self::Err> {
178        match s.to_lowercase().as_str() {
179            "debug" => Ok(LogLevel::Debug),
180            "info" => Ok(LogLevel::Info),
181            "warn" | "warning" => Ok(LogLevel::Warn),
182            "error" => Ok(LogLevel::Error),
183            _ => Err(format!("Invalid log level: {s}")),
184        }
185    }
186}
187
188impl Display for LogLevel {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        match self {
191            LogLevel::Debug => write!(f, "DEBUG"),
192            LogLevel::Info => write!(f, "INFO"),
193            LogLevel::Warn => write!(f, "WARN"),
194            LogLevel::Error => write!(f, "ERROR"),
195        }
196    }
197}
198
199/// Trait for output handling
200///
201/// This trait defines the interface for writing output lines. It is implemented
202/// by both the standard output handler and named pipe output handlers.
203pub trait Output: Send + Sync + std::fmt::Debug + std::any::Any {
204    /// Writes a single line of output
205    ///
206    /// # Arguments
207    ///
208    /// * `line` - The line to write
209    ///
210    /// # Returns
211    ///
212    /// Returns `Ok(())` if the write was successful, or a `TraceError` if it failed
213    fn write_line(&self, line: &str) -> Result<(), OTelSdkError>;
214
215    /// Checks if the output target is a named pipe.
216    ///
217    /// Returns `true` if the output is configured to write to a named pipe,
218    /// `false` otherwise (e.g., stdout).
219    fn is_pipe(&self) -> bool;
220
221    /// Performs a "touch" operation on the output target, primarily for named pipes.
222    ///
223    /// For named pipes, this should open and immediately close the pipe for writing
224    /// to generate an EOF signal for readers.
225    /// For other output types (like stdout), this is typically a no-op.
226    ///
227    /// # Returns
228    ///
229    /// Returns `Ok(())` if the touch was successful or is a no-op,
230    /// or a `TraceError` if opening/closing the pipe failed.
231    fn touch_pipe(&self) -> Result<(), OTelSdkError> {
232        // Default implementation is a no-op
233        Ok(())
234    }
235}
236
237/// Standard output implementation that writes to stdout
238#[derive(Debug, Default)]
239struct StdOutput;
240
241impl Output for StdOutput {
242    fn write_line(&self, line: &str) -> Result<(), OTelSdkError> {
243        // Get a locked stdout handle once
244        let stdout = io::stdout();
245        let mut handle = stdout.lock();
246
247        // Write the line and a newline in one operation
248        writeln!(handle, "{line}").map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?;
249
250        Ok(())
251    }
252
253    fn is_pipe(&self) -> bool {
254        false // Stdout is not a pipe
255    }
256}
257
258/// Output implementation that writes to a named pipe
259#[derive(Debug)]
260struct NamedPipeOutput {
261    path: PathBuf,
262}
263
264impl NamedPipeOutput {
265    fn new() -> Result<Self, OTelSdkError> {
266        let path_buf = PathBuf::from(defaults::PIPE_PATH);
267        if !path_buf.exists() {
268            log::warn!("Named pipe does not exist: {}", defaults::PIPE_PATH);
269            // On Unix systems we could create it with mkfifo but this would need cfg platform specifics
270        }
271
272        Ok(Self { path: path_buf })
273    }
274}
275
276impl Output for NamedPipeOutput {
277    fn write_line(&self, line: &str) -> Result<(), OTelSdkError> {
278        // Open the pipe for writing
279        let mut file = OpenOptions::new()
280            .write(true)
281            .open(&self.path)
282            .map_err(|e| OTelSdkError::InternalFailure(format!("Failed to open pipe: {e}")))?;
283
284        // Write line with newline
285        writeln!(file, "{line}")
286            .map_err(|e| OTelSdkError::InternalFailure(format!("Failed to write to pipe: {e}")))?;
287
288        Ok(())
289    }
290
291    fn is_pipe(&self) -> bool {
292        true // This implementation is for named pipes
293    }
294
295    fn touch_pipe(&self) -> Result<(), OTelSdkError> {
296        // Open the pipe for writing and immediately close it (RAII handles close)
297        let _file = OpenOptions::new()
298            .write(true)
299            .open(&self.path)
300            .map_err(|e| OTelSdkError::InternalFailure(format!("Failed to touch pipe: {e}")))?;
301        Ok(())
302    }
303}
304
305/// An Output implementation that writes lines to an internal buffer.
306#[derive(Clone, Default)]
307pub struct BufferOutput {
308    buffer: Arc<Mutex<Vec<String>>>,
309}
310
311impl BufferOutput {
312    /// Creates a new, empty BufferOutput.
313    pub fn new() -> Self {
314        Self::default()
315    }
316
317    /// Retrieves all lines currently in the buffer, clearing the buffer afterwards.
318    pub fn take_lines(&self) -> Result<Vec<String>, OTelSdkError> {
319        let mut guard = self.buffer.lock().map_err(|e| {
320            OTelSdkError::InternalFailure(format!(
321                "Failed to lock buffer mutex for take_lines: {e}"
322            ))
323        })?;
324        Ok(std::mem::take(&mut *guard)) // Efficiently swaps the Vec with an empty one and wraps in Ok
325    }
326}
327
328impl Output for BufferOutput {
329    fn write_line(&self, line: &str) -> Result<(), OTelSdkError> {
330        let mut guard = self.buffer.lock().map_err(|e| {
331            OTelSdkError::InternalFailure(format!(
332                "Failed to lock buffer mutex for write_line: {e}"
333            ))
334        })?;
335        guard.push(line.to_string());
336        Ok(())
337    }
338
339    fn is_pipe(&self) -> bool {
340        false // BufferOutput is not a pipe
341    }
342}
343
344// Need to implement Debug manually as Mutex doesn't implement Debug
345impl fmt::Debug for BufferOutput {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        // Avoid locking the mutex in debug formatting if possible, or provide minimal info
348        f.debug_struct("BufferOutput").finish_non_exhaustive()
349    }
350}
351
352/// Helper function to create output based on type
353fn create_output(use_pipe: bool) -> Arc<dyn Output> {
354    if use_pipe {
355        match NamedPipeOutput::new() {
356            Ok(output) => Arc::new(output),
357            Err(e) => {
358                log::warn!("Failed to create named pipe output: {e}, falling back to stdout");
359                Arc::new(StdOutput)
360            }
361        }
362    } else {
363        Arc::new(StdOutput)
364    }
365}
366
367/// Output format for the OTLP stdout exporter
368///
369/// This struct defines the JSON structure that will be written to stdout
370/// for each batch of spans.
371#[derive(Debug, Serialize, Deserialize)]
372pub struct ExporterOutput {
373    /// Version identifier for the output format
374    #[serde(rename = "__otel_otlp_stdout")]
375    pub version: String,
376    /// Service name that generated the spans
377    pub source: String,
378    /// OTLP endpoint (always http://localhost:4318/v1/traces)
379    pub endpoint: String,
380    /// HTTP method (always POST)
381    pub method: String,
382    /// Content type (always application/x-protobuf)
383    #[serde(rename = "content-type")]
384    pub content_type: String,
385    /// Content encoding (always gzip)
386    #[serde(rename = "content-encoding")]
387    pub content_encoding: String,
388    /// Custom headers from environment variables
389    #[serde(skip_serializing_if = "ExporterOutput::is_headers_empty")]
390    pub headers: Option<HashMap<String, String>>,
391    /// Base64-encoded, gzipped, protobuf-serialized span data
392    pub payload: String,
393    /// Whether the payload is base64 encoded (always true)
394    pub base64: bool,
395    /// Log level for filtering (optional)
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub level: Option<String>,
398}
399
400impl ExporterOutput {
401    /// Helper function for serde to skip serializing empty headers
402    fn is_headers_empty(headers: &Option<HashMap<String, String>>) -> bool {
403        headers.as_ref().map_or(true, |h| h.is_empty())
404    }
405}
406
407/// A span exporter that writes spans to stdout in OTLP format
408///
409/// This exporter implements the OpenTelemetry [`SpanExporter`] trait and writes spans
410/// to stdout in OTLP format with Protobuf serialization and GZIP compression.
411///
412/// # Features
413///
414/// - Configurable GZIP compression level (0-9)
415/// - Environment variable support for service name and headers
416/// - Efficient batching of spans
417/// - Base64 encoding of compressed data
418///
419/// # Example
420///
421/// ```rust,no_run
422/// use opentelemetry_sdk::runtime;
423/// use otlp_stdout_span_exporter::OtlpStdoutSpanExporter;
424///
425/// // Create an exporter with maximum compression
426/// let exporter = OtlpStdoutSpanExporter::builder()
427///     .compression_level(9)
428///     .build();
429/// ```
430#[derive(Debug)]
431pub struct OtlpStdoutSpanExporter {
432    /// GZIP compression level (0-9)
433    compression_level: u8,
434    /// Optional resource to be included with all spans
435    resource: Option<Resource>,
436    // Optional headers
437    headers: Option<HashMap<String, String>>,
438    /// Output implementation (stdout or named pipe)
439    output: Arc<dyn Output>,
440    /// Optional log level for the exported spans
441    level: Option<LogLevel>,
442}
443
444impl Default for OtlpStdoutSpanExporter {
445    fn default() -> Self {
446        Self::builder().build()
447    }
448}
449#[bon]
450impl OtlpStdoutSpanExporter {
451    /// Create a new `OtlpStdoutSpanExporter` with default configuration.
452    ///
453    /// This uses a GZIP compression level of 6 unless overridden by an environment variable.
454    ///
455    /// # Output Type
456    ///
457    /// The output type is determined in the following order:
458    ///
459    /// 1. The `OTLP_STDOUT_SPAN_EXPORTER_OUTPUT_TYPE` environment variable if set ("pipe" or "stdout")
460    /// 2. Constructor parameter (pipe)
461    /// 3. Default (stdout)
462    ///
463    /// # Example
464    ///
465    /// ```
466    /// use otlp_stdout_span_exporter::OtlpStdoutSpanExporter;
467    ///
468    /// let exporter = OtlpStdoutSpanExporter::default();
469    /// ```
470    #[builder]
471    pub fn new(
472        compression_level: Option<u8>,
473        resource: Option<Resource>,
474        headers: Option<HashMap<String, String>>,
475        output: Option<Arc<dyn Output>>,
476        level: Option<LogLevel>,
477        pipe: Option<bool>,
478    ) -> Self {
479        // Set gzip_level with proper precedence (env var > constructor param > default)
480        let compression_level = match env::var(env_vars::COMPRESSION_LEVEL) {
481            Ok(value) => match value.parse::<u8>() {
482                Ok(level) if level <= 9 => level,
483                Ok(level) => {
484                    log::warn!(
485                        "Invalid value in {}: {} (must be 0-9), using fallback",
486                        env_vars::COMPRESSION_LEVEL,
487                        level
488                    );
489                    compression_level.unwrap_or(defaults::COMPRESSION_LEVEL)
490                }
491                Err(_) => {
492                    log::warn!(
493                        "Failed to parse {}: {}, using fallback",
494                        env_vars::COMPRESSION_LEVEL,
495                        value
496                    );
497                    compression_level.unwrap_or(defaults::COMPRESSION_LEVEL)
498                }
499            },
500            Err(_) => {
501                // No environment variable, use parameter or default
502                compression_level.unwrap_or(defaults::COMPRESSION_LEVEL)
503            }
504        };
505
506        // Combine constructor headers with environment headers, giving priority to env vars
507        let headers = match headers {
508            Some(constructor_headers) => {
509                if let Some(env_headers) = Self::parse_headers() {
510                    // Merge, with env headers taking precedence
511                    let mut merged = constructor_headers;
512                    merged.extend(env_headers);
513                    Some(merged)
514                } else {
515                    // No env headers, use constructor headers
516                    Some(constructor_headers)
517                }
518            }
519            None => Self::parse_headers(), // Use env headers only
520        };
521
522        // Set log level with proper precedence (env var > constructor param > default)
523        let level = match env::var(env_vars::LOG_LEVEL) {
524            Ok(value) => match LogLevel::from_str(&value) {
525                Ok(log_level) => Some(log_level),
526                Err(e) => {
527                    log::warn!(
528                        "Invalid log level in {}: {}, using fallback",
529                        env_vars::LOG_LEVEL,
530                        e
531                    );
532                    level
533                }
534            },
535            Err(_) => {
536                // No environment variable, use parameter
537                level
538            }
539        };
540
541        // Determine output type with proper precedence (env var > constructor > default)
542        let use_pipe = match env::var(env_vars::OUTPUT_TYPE) {
543            Ok(value) => value.to_lowercase() == "pipe",
544            Err(_) => pipe.unwrap_or(false),
545        };
546
547        // Create output implementation
548        let output = output.unwrap_or_else(|| create_output(use_pipe));
549
550        Self {
551            compression_level,
552            resource,
553            headers,
554            output,
555            level,
556        }
557    }
558
559    /// Get the service name from environment variables.
560    ///
561    /// The service name is determined in the following order:
562    ///
563    /// 1. OTEL_SERVICE_NAME
564    /// 2. AWS_LAMBDA_FUNCTION_NAME
565    /// 3. "unknown-service" (fallback)
566    fn get_service_name() -> String {
567        env::var(env_vars::SERVICE_NAME)
568            .or_else(|_| env::var(env_vars::AWS_LAMBDA_FUNCTION_NAME))
569            .unwrap_or_else(|_| defaults::SERVICE_NAME.to_string())
570    }
571
572    #[cfg(test)]
573    fn with_test_output() -> (Self, Arc<TestOutput>) {
574        let output = Arc::new(TestOutput::new());
575
576        // Use the standard builder() method and explicitly set the output
577        let exporter = Self::builder().output(output.clone()).build();
578
579        (exporter, output)
580    }
581
582    /// Parse headers from environment variables
583    ///
584    /// This function reads headers from both global and trace-specific
585    /// environment variables, with trace-specific headers taking precedence.
586    fn parse_headers() -> Option<HashMap<String, String>> {
587        // Function to get and parse headers from an env var
588        let get_headers = |var_name: &str| -> Option<HashMap<String, String>> {
589            env::var(var_name).ok().map(|header_str| {
590                let mut map = HashMap::new();
591                Self::parse_header_string(&header_str, &mut map);
592                map
593            })
594        };
595
596        // Try to get headers from both env vars
597        let global_headers = get_headers("OTEL_EXPORTER_OTLP_HEADERS");
598        let trace_headers = get_headers("OTEL_EXPORTER_OTLP_TRACES_HEADERS");
599
600        // If no headers were found in either env var, return None
601        if global_headers.is_none() && trace_headers.is_none() {
602            return None;
603        }
604
605        // Create a merged map, with trace headers taking precedence
606        let mut result = HashMap::new();
607
608        // Add global headers first (if any)
609        if let Some(headers) = global_headers {
610            result.extend(headers);
611        }
612
613        // Add trace-specific headers (if any) - these will override any duplicates
614        if let Some(headers) = trace_headers {
615            result.extend(headers);
616        }
617
618        // Return None for empty map, otherwise Some
619        if result.is_empty() {
620            None
621        } else {
622            Some(result)
623        }
624    }
625
626    /// Parse a header string in the format key1=value1,key2=value2
627    ///
628    /// # Arguments
629    ///
630    /// * `header_str` - The header string to parse
631    /// * `headers` - The map to store parsed headers in
632    fn parse_header_string(header_str: &str, headers: &mut HashMap<String, String>) {
633        for pair in header_str.split(',') {
634            if let Some((key, value)) = pair.split_once('=') {
635                let key = key.trim().to_lowercase();
636                // Skip content-type and content-encoding as they are fixed
637                if key != "content-type" && key != "content-encoding" {
638                    headers.insert(key, value.trim().to_string());
639                }
640            }
641        }
642    }
643}
644
645impl SpanExporter for OtlpStdoutSpanExporter {
646    /// Export spans to stdout in OTLP format
647    ///
648    /// This function:
649    /// 1. Converts spans to OTLP format
650    /// 2. Serializes them to protobuf
651    /// 3. Compresses the data with GZIP
652    /// 4. Base64 encodes the result
653    /// 5. Writes a JSON object to stdout
654    ///
655    /// # Arguments
656    ///
657    /// * `batch` - A vector of spans to export
658    ///
659    /// # Returns
660    ///
661    /// Returns a resolved future with `Ok(())` if the export was successful, or a `TraceError` if it failed
662    fn export(
663        &self,
664        batch: Vec<SpanData>,
665    ) -> impl std::future::Future<Output = OTelSdkResult> + Send {
666        // Check for empty batch and pipe output configuration
667        if batch.is_empty() && self.output.is_pipe() {
668            // Perform the "pipe touch" operation: open for writing and immediately close.
669            let touch_result = self.output.touch_pipe();
670            return Box::pin(std::future::ready(touch_result));
671        }
672
673        // Original export logic for non-empty batches or stdout output
674        let result = (|| {
675            // Convert spans to OTLP format
676            let resource = self
677                .resource
678                .clone()
679                .unwrap_or_else(|| opentelemetry_sdk::Resource::builder_empty().build());
680            let resource_attrs = ResourceAttributesWithSchema::from(&resource);
681            let resource_spans = group_spans_by_resource_and_scope(batch, &resource_attrs);
682            let request = ExportTraceServiceRequest { resource_spans };
683
684            // Serialize to protobuf
685            let proto_bytes = request.encode_to_vec();
686
687            // Compress with GZIP
688            let mut encoder =
689                GzEncoder::new(Vec::new(), Compression::new(self.compression_level as u32));
690            encoder
691                .write_all(&proto_bytes)
692                .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?;
693            let compressed_bytes = encoder
694                .finish()
695                .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?;
696
697            // Base64 encode
698            let payload = base64_engine.encode(compressed_bytes);
699
700            // Prepare the output
701            let output_data = ExporterOutput {
702                version: VERSION.to_string(),
703                source: Self::get_service_name(),
704                endpoint: defaults::ENDPOINT.to_string(),
705                method: "POST".to_string(),
706                content_type: "application/x-protobuf".to_string(),
707                content_encoding: "gzip".to_string(),
708                headers: self.headers.clone(),
709                payload,
710                base64: true,
711                level: self.level.map(|l| l.to_string()),
712            };
713
714            // Write using the output implementation
715            self.output.write_line(
716                &serde_json::to_string(&output_data)
717                    .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?,
718            )?;
719
720            Ok(())
721        })();
722
723        // Return a resolved future with the result
724        Box::pin(std::future::ready(result))
725    }
726
727    /// Shuts down the exporter with a timeout
728    ///
729    /// This is a no-op for stdout export as no cleanup is needed.
730    /// The timeout parameter is ignored since there's nothing to flush.
731    ///
732    /// # Returns
733    ///
734    /// Returns `Ok(())` as there is nothing to clean up.
735    fn shutdown_with_timeout(&mut self, _timeout: std::time::Duration) -> Result<(), OTelSdkError> {
736        Ok(())
737    }
738
739    /// Force flushes any pending spans
740    ///
741    /// This is a no-op for stdout export as spans are written immediately.
742    ///
743    /// # Returns
744    ///
745    /// Returns `Ok(())` as there is nothing to flush.
746    fn force_flush(&mut self) -> Result<(), OTelSdkError> {
747        Ok(())
748    }
749
750    /// Sets the resource for this exporter.
751    ///
752    /// This method stores a clone of the provided resource to be used when exporting spans.
753    /// The resource represents the entity producing telemetry and will be included in the
754    /// exported trace data.
755    ///
756    /// # Arguments
757    ///
758    /// * `resource` - The resource to associate with this exporter
759    fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) {
760        self.resource = Some(<opentelemetry_sdk::Resource as Into<Resource>>::into(
761            resource.clone(),
762        ));
763    }
764}
765
766#[cfg(doctest)]
767#[macro_use]
768extern crate doc_comment;
769
770#[cfg(doctest)]
771use doc_comment::doctest;
772
773#[cfg(doctest)]
774doctest!("../README.md", readme);
775
776/// Test output implementation that captures to a buffer
777#[cfg(test)]
778#[derive(Debug, Default)]
779struct TestOutput {
780    buffer: Arc<Mutex<Vec<String>>>,
781}
782
783#[cfg(test)]
784impl TestOutput {
785    fn new() -> Self {
786        Self {
787            buffer: Arc::new(Mutex::new(Vec::new())),
788        }
789    }
790
791    fn get_output(&self) -> Vec<String> {
792        self.buffer.lock().unwrap().clone()
793    }
794}
795
796#[cfg(test)]
797impl Output for TestOutput {
798    fn write_line(&self, line: &str) -> Result<(), OTelSdkError> {
799        self.buffer.lock().unwrap().push(line.to_string());
800        Ok(())
801    }
802
803    fn is_pipe(&self) -> bool {
804        false // TestOutput is not a pipe
805    }
806
807    // touch_pipe uses the default no-op implementation
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813    use opentelemetry::{
814        trace::{SpanContext, SpanId, SpanKind, Status, TraceFlags, TraceId, TraceState},
815        InstrumentationScope, KeyValue,
816    };
817    use opentelemetry_sdk::trace::{SpanData, SpanEvents, SpanLinks};
818    use serde_json::Value;
819    use serial_test::serial;
820    use std::time::SystemTime;
821
822    fn create_test_span() -> SpanData {
823        let trace_id_bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42];
824        let span_id_bytes = [0, 0, 0, 0, 0, 0, 0, 123];
825        let parent_id_bytes = [0, 0, 0, 0, 0, 0, 0, 42];
826
827        let span_context = SpanContext::new(
828            TraceId::from_bytes(trace_id_bytes),
829            SpanId::from_bytes(span_id_bytes),
830            TraceFlags::default(),
831            false,
832            TraceState::default(),
833        );
834
835        SpanData {
836            span_context,
837            parent_span_id: SpanId::from_bytes(parent_id_bytes),
838            span_kind: SpanKind::Client,
839            name: "test-span".into(),
840            start_time: SystemTime::UNIX_EPOCH,
841            end_time: SystemTime::UNIX_EPOCH,
842            attributes: vec![KeyValue::new("test.key", "test-value")],
843            dropped_attributes_count: 0,
844            events: SpanEvents::default(),
845            links: SpanLinks::default(),
846            status: Status::Ok,
847            instrumentation_scope: InstrumentationScope::builder("test-library")
848                .with_version("1.0.0")
849                .with_schema_url("https://opentelemetry.io/schema/1.0.0")
850                .build(),
851        }
852    }
853
854    #[test]
855    fn test_parse_headers() {
856        std::env::set_var("OTEL_EXPORTER_OTLP_HEADERS", "key1=value1,key2=value2");
857        std::env::set_var(
858            "OTEL_EXPORTER_OTLP_TRACES_HEADERS",
859            "key2=override,key3=value3",
860        );
861
862        let headers = OtlpStdoutSpanExporter::parse_headers();
863
864        // Headers should be Some since we set environment variables
865        assert!(headers.is_some());
866        let headers = headers.unwrap();
867
868        assert_eq!(headers.get("key1").unwrap(), "value1");
869        assert_eq!(headers.get("key2").unwrap(), "override");
870        assert_eq!(headers.get("key3").unwrap(), "value3");
871
872        // Clean up
873        std::env::remove_var("OTEL_EXPORTER_OTLP_HEADERS");
874        std::env::remove_var("OTEL_EXPORTER_OTLP_TRACES_HEADERS");
875    }
876
877    #[test]
878    fn test_service_name_resolution() {
879        // Test OTEL_SERVICE_NAME priority
880        std::env::set_var(env_vars::SERVICE_NAME, "otel-service");
881        std::env::set_var(env_vars::AWS_LAMBDA_FUNCTION_NAME, "lambda-function");
882        assert_eq!(OtlpStdoutSpanExporter::get_service_name(), "otel-service");
883
884        // Test AWS_LAMBDA_FUNCTION_NAME fallback
885        std::env::remove_var(env_vars::SERVICE_NAME);
886        assert_eq!(
887            OtlpStdoutSpanExporter::get_service_name(),
888            "lambda-function"
889        );
890
891        // Test default fallback
892        std::env::remove_var(env_vars::AWS_LAMBDA_FUNCTION_NAME);
893        assert_eq!(
894            OtlpStdoutSpanExporter::get_service_name(),
895            defaults::SERVICE_NAME
896        );
897    }
898
899    #[test]
900    fn test_compression_level_precedence() {
901        // Test env var takes precedence over options
902        std::env::set_var(env_vars::COMPRESSION_LEVEL, "3");
903        let exporter = OtlpStdoutSpanExporter::builder()
904            .compression_level(7)
905            .build();
906        assert_eq!(exporter.compression_level, 3);
907
908        // Test invalid env var falls back to options
909        std::env::set_var(env_vars::COMPRESSION_LEVEL, "invalid");
910        let exporter = OtlpStdoutSpanExporter::builder()
911            .compression_level(7)
912            .build();
913        assert_eq!(exporter.compression_level, 7);
914
915        // Test no env var uses options
916        std::env::remove_var(env_vars::COMPRESSION_LEVEL);
917        let exporter = OtlpStdoutSpanExporter::builder()
918            .compression_level(7)
919            .build();
920        assert_eq!(exporter.compression_level, 7);
921
922        // Test fallback to default
923        let exporter = OtlpStdoutSpanExporter::builder()
924            .compression_level(defaults::COMPRESSION_LEVEL)
925            .build();
926        assert_eq!(exporter.compression_level, defaults::COMPRESSION_LEVEL);
927    }
928
929    #[test]
930    fn test_new_uses_env_compression_level() {
931        // Set environment variable
932        std::env::set_var(env_vars::COMPRESSION_LEVEL, "3");
933        let exporter = OtlpStdoutSpanExporter::default();
934        assert_eq!(exporter.compression_level, 3);
935
936        // Test with unset variable (should use default)
937        std::env::remove_var(env_vars::COMPRESSION_LEVEL);
938        let exporter = OtlpStdoutSpanExporter::default();
939        assert_eq!(exporter.compression_level, defaults::COMPRESSION_LEVEL);
940    }
941
942    #[tokio::test]
943    #[serial]
944    async fn test_compression_level_affects_output_size() {
945        // Create a large span batch to make compression differences more noticeable
946        let mut spans = Vec::new();
947        for i in 0..100 {
948            let mut span = create_test_span();
949            // Add unique attributes to each span to increase data size
950            span.attributes.push(KeyValue::new("index", i));
951            // Add a large attribute to make compression more effective
952            span.attributes
953                .push(KeyValue::new("data", "a".repeat(1000)));
954            spans.push(span);
955        }
956
957        // Make sure environment variables don't interfere with our test
958        std::env::remove_var(env_vars::COMPRESSION_LEVEL);
959
960        // Create exporter with no compression (level 0)
961        let no_compression_output = Arc::new(TestOutput::new());
962        let no_compression_exporter = OtlpStdoutSpanExporter {
963            compression_level: 0,
964            resource: None,
965            output: no_compression_output.clone() as Arc<dyn Output>,
966            headers: None,
967            level: None,
968        };
969        let _ = no_compression_exporter.export(spans.clone()).await;
970        let no_compression_size = extract_payload_size(&no_compression_output.get_output()[0]);
971
972        // Create exporter with max compression (level 9)
973        let max_compression_output = Arc::new(TestOutput::new());
974        let max_compression_exporter = OtlpStdoutSpanExporter {
975            compression_level: 9,
976            resource: None,
977            output: max_compression_output.clone() as Arc<dyn Output>,
978            headers: None,
979            level: None,
980        };
981        let _ = max_compression_exporter.export(spans.clone()).await;
982        let max_compression_size = extract_payload_size(&max_compression_output.get_output()[0]);
983
984        // Verify that higher compression levels result in smaller payloads
985        assert!(no_compression_size > max_compression_size,
986            "Maximum compression (level 9) should produce output no larger than no compression (level 0). Got {} vs {}",
987            max_compression_size, no_compression_size);
988
989        // Verify that all outputs can be properly decoded and contain the same data
990        let no_compression_spans = decode_and_count_spans(&no_compression_output.get_output()[0]);
991        let max_compression_spans = decode_and_count_spans(&max_compression_output.get_output()[0]);
992
993        assert_eq!(
994            no_compression_spans,
995            spans.len(),
996            "No compression output should contain all spans"
997        );
998        assert_eq!(
999            max_compression_spans,
1000            spans.len(),
1001            "Maximum compression output should contain all spans"
1002        );
1003    }
1004
1005    // Helper function to extract the size of the base64-decoded payload
1006    fn extract_payload_size(json_str: &str) -> usize {
1007        let json: Value = serde_json::from_str(json_str).unwrap();
1008        let payload = json["payload"].as_str().unwrap();
1009        base64_engine.decode(payload).unwrap().len()
1010    }
1011
1012    // Helper function to decode the payload and count the number of spans
1013    fn decode_and_count_spans(json_str: &str) -> usize {
1014        let json: Value = serde_json::from_str(json_str).unwrap();
1015        let payload = json["payload"].as_str().unwrap();
1016        let decoded = base64_engine.decode(payload).unwrap();
1017
1018        let mut decoder = flate2::read::GzDecoder::new(&decoded[..]);
1019        let mut decompressed = Vec::new();
1020        std::io::Read::read_to_end(&mut decoder, &mut decompressed).unwrap();
1021
1022        let request = ExportTraceServiceRequest::decode(&*decompressed).unwrap();
1023
1024        // Count total spans across all resource spans
1025        let mut span_count = 0;
1026        for resource_span in &request.resource_spans {
1027            for scope_span in &resource_span.scope_spans {
1028                span_count += scope_span.spans.len();
1029            }
1030        }
1031
1032        span_count
1033    }
1034
1035    #[tokio::test]
1036    async fn test_export_single_span() {
1037        let (exporter, output) = OtlpStdoutSpanExporter::with_test_output();
1038        let span = create_test_span();
1039
1040        let result = exporter.export(vec![span]).await;
1041        assert!(result.is_ok());
1042
1043        let output = output.get_output();
1044        assert_eq!(output.len(), 1);
1045
1046        // Parse and verify the output
1047        let json: Value = serde_json::from_str(&output[0]).unwrap();
1048        assert_eq!(json["__otel_otlp_stdout"], VERSION);
1049        assert_eq!(json["method"], "POST");
1050        assert_eq!(json["content-type"], "application/x-protobuf");
1051        assert_eq!(json["content-encoding"], "gzip");
1052        assert_eq!(json["base64"], true);
1053
1054        // Verify payload is valid base64 and can be decoded
1055        let payload = json["payload"].as_str().unwrap();
1056        let decoded = base64_engine.decode(payload).unwrap();
1057
1058        // Verify it can be decompressed
1059        let mut decoder = flate2::read::GzDecoder::new(&decoded[..]);
1060        let mut decompressed = Vec::new();
1061        std::io::Read::read_to_end(&mut decoder, &mut decompressed).unwrap();
1062
1063        // Verify it's valid OTLP protobuf
1064        let request = ExportTraceServiceRequest::decode(&*decompressed).unwrap();
1065        assert_eq!(request.resource_spans.len(), 1);
1066    }
1067
1068    #[tokio::test]
1069    async fn test_export_empty_batch() {
1070        let exporter = OtlpStdoutSpanExporter::default();
1071        let result = exporter.export(vec![]).await;
1072        assert!(result.is_ok());
1073    }
1074
1075    #[test]
1076    #[serial]
1077    fn test_gzip_level_configuration() {
1078        // Ensure all environment variables are removed first
1079        std::env::remove_var(env_vars::COMPRESSION_LEVEL);
1080
1081        // Now test the constructor parameter
1082        let exporter = OtlpStdoutSpanExporter::builder()
1083            .compression_level(9)
1084            .build();
1085        assert_eq!(exporter.compression_level, 9);
1086    }
1087
1088    #[tokio::test]
1089    #[serial]
1090    async fn test_env_var_affects_export_compression() {
1091        // Create more test data with repeated content to make compression differences noticeable
1092        let span = create_test_span();
1093        let mut spans = Vec::new();
1094        // Create 100 spans with large attributes to make compression differences noticeable
1095        for i in 0..100 {
1096            let mut span = span.clone();
1097            // Add unique attribute with large value to make compression more effective
1098            span.attributes
1099                .push(KeyValue::new(format!("test-key-{}", i), "a".repeat(1000)));
1100            spans.push(span);
1101        }
1102
1103        // First, create data with no compression
1104        std::env::set_var(env_vars::COMPRESSION_LEVEL, "0");
1105        let no_compression_output = Arc::new(TestOutput::new());
1106        let mut no_compression_exporter = OtlpStdoutSpanExporter::builder()
1107            .compression_level(0)
1108            .build();
1109        no_compression_exporter.output = no_compression_output.clone() as Arc<dyn Output>;
1110        let _ = no_compression_exporter.export(spans.clone()).await;
1111        let no_compression_size = extract_payload_size(&no_compression_output.get_output()[0]);
1112
1113        // Now with max compression
1114        std::env::set_var(env_vars::COMPRESSION_LEVEL, "9");
1115        let max_compression_output = Arc::new(TestOutput::new());
1116        let mut max_compression_exporter = OtlpStdoutSpanExporter::builder()
1117            .compression_level(9)
1118            .build();
1119        max_compression_exporter.output = max_compression_output.clone() as Arc<dyn Output>;
1120        let _ = max_compression_exporter.export(spans.clone()).await;
1121        let max_compression_size = extract_payload_size(&max_compression_output.get_output()[0]);
1122
1123        // Verify that the environment variable affected the compression level
1124        assert!(no_compression_size > max_compression_size,
1125            "Environment variable COMPRESSION_LEVEL=9 should produce smaller output than COMPRESSION_LEVEL=0. Got {} vs {}",
1126            max_compression_size, no_compression_size);
1127
1128        // Test with explicit level when env var is set (env var should take precedence)
1129        std::env::set_var(env_vars::COMPRESSION_LEVEL, "0");
1130        let explicit_output = Arc::new(TestOutput::new());
1131
1132        // Create an exporter with the default() method which will use the environment variable
1133        let explicit_exporter = OtlpStdoutSpanExporter::builder()
1134            .output(explicit_output.clone())
1135            .build();
1136
1137        // The environment variable should make it use compression level 0
1138        let _ = explicit_exporter.export(spans.clone()).await;
1139        let explicit_size = extract_payload_size(&explicit_output.get_output()[0]);
1140
1141        // Should be approximately the same size as the no_compression_size since
1142        // the environment variable (level 0) should take precedence
1143        assert!(explicit_size > max_compression_size,
1144            "Environment variable should take precedence over explicitly set level. Expected size closer to {} but got {}",
1145            no_compression_size, explicit_size);
1146
1147        // Clean up
1148        std::env::remove_var(env_vars::COMPRESSION_LEVEL);
1149    }
1150
1151    #[tokio::test]
1152    #[serial]
1153    async fn test_environment_variable_precedence() {
1154        // Set environment variable
1155        std::env::set_var(env_vars::COMPRESSION_LEVEL, "3");
1156
1157        // With the new precedence rules, environment variables take precedence
1158        // over constructor parameters
1159        let exporter = OtlpStdoutSpanExporter::builder()
1160            .compression_level(9)
1161            .build();
1162        assert_eq!(exporter.compression_level, 3);
1163
1164        // When environment variable is removed, constructor parameter should be used
1165        std::env::remove_var(env_vars::COMPRESSION_LEVEL);
1166        let exporter = OtlpStdoutSpanExporter::builder()
1167            .compression_level(9)
1168            .build();
1169        assert_eq!(exporter.compression_level, 9);
1170    }
1171
1172    #[test]
1173    fn test_exporter_output_deserialization() {
1174        // Create a sample JSON string that would be produced by the exporter
1175        let json_str = r#"{
1176            "__otel_otlp_stdout": "0.11.1",
1177            "source": "test-service",
1178            "endpoint": "http://localhost:4318/v1/traces",
1179            "method": "POST",
1180            "content-type": "application/x-protobuf",
1181            "content-encoding": "gzip",
1182            "headers": {
1183                "api-key": "test-key",
1184                "custom-header": "test-value"
1185            },
1186            "payload": "SGVsbG8gd29ybGQ=",
1187            "base64": true
1188        }"#;
1189
1190        // Deserialize the JSON string into an ExporterOutput
1191        let output: ExporterOutput = serde_json::from_str(json_str).unwrap();
1192
1193        // Verify that all fields are correctly deserialized
1194        assert_eq!(output.version, "0.11.1");
1195        assert_eq!(output.source, "test-service");
1196        assert_eq!(output.endpoint, "http://localhost:4318/v1/traces");
1197        assert_eq!(output.method, "POST");
1198        assert_eq!(output.content_type, "application/x-protobuf");
1199        assert_eq!(output.content_encoding, "gzip");
1200        assert_eq!(output.headers.as_ref().unwrap().len(), 2);
1201        assert_eq!(
1202            output.headers.as_ref().unwrap().get("api-key").unwrap(),
1203            "test-key"
1204        );
1205        assert_eq!(
1206            output
1207                .headers
1208                .as_ref()
1209                .unwrap()
1210                .get("custom-header")
1211                .unwrap(),
1212            "test-value"
1213        );
1214        assert_eq!(output.payload, "SGVsbG8gd29ybGQ=");
1215        assert!(output.base64);
1216
1217        // Verify that we can decode the base64 payload (if it's valid base64)
1218        let decoded = base64_engine.decode(&output.payload).unwrap();
1219        let payload_text = String::from_utf8(decoded).unwrap();
1220        assert_eq!(payload_text, "Hello world");
1221    }
1222
1223    #[test]
1224    fn test_exporter_output_deserialization_dynamic() {
1225        // Create a dynamic JSON string using String operations
1226        let version = "0.11.1".to_string();
1227        let service = "dynamic-service".to_string();
1228        let payload = base64_engine.encode("Dynamic payload");
1229
1230        // Build the JSON dynamically
1231        let json_str = format!(
1232            r#"{{
1233                "__otel_otlp_stdout": "{}",
1234                "source": "{}",
1235                "endpoint": "http://localhost:4318/v1/traces",
1236                "method": "POST",
1237                "content-type": "application/x-protobuf",
1238                "content-encoding": "gzip",
1239                "headers": {{
1240                    "dynamic-key": "dynamic-value"
1241                }},
1242                "payload": "{}",
1243                "base64": true
1244            }}"#,
1245            version, service, payload
1246        );
1247
1248        // Deserialize the dynamic JSON string
1249        let output: ExporterOutput = serde_json::from_str(&json_str).unwrap();
1250
1251        // Verify fields
1252        assert_eq!(output.version, version);
1253        assert_eq!(output.source, service);
1254        assert_eq!(output.endpoint, "http://localhost:4318/v1/traces");
1255        assert_eq!(output.method, "POST");
1256        assert_eq!(output.content_type, "application/x-protobuf");
1257        assert_eq!(output.content_encoding, "gzip");
1258        assert_eq!(output.headers.as_ref().unwrap().len(), 1);
1259        assert_eq!(
1260            output.headers.as_ref().unwrap().get("dynamic-key").unwrap(),
1261            "dynamic-value"
1262        );
1263        assert_eq!(output.payload, payload);
1264        assert!(output.base64);
1265
1266        // Verify payload decoding
1267        let decoded = base64_engine.decode(&output.payload).unwrap();
1268        let payload_text = String::from_utf8(decoded).unwrap();
1269        assert_eq!(payload_text, "Dynamic payload");
1270    }
1271
1272    #[test]
1273    fn test_log_level_from_str() {
1274        assert_eq!(LogLevel::from_str("debug").unwrap(), LogLevel::Debug);
1275        assert_eq!(LogLevel::from_str("DEBUG").unwrap(), LogLevel::Debug);
1276        assert_eq!(LogLevel::from_str("info").unwrap(), LogLevel::Info);
1277        assert_eq!(LogLevel::from_str("INFO").unwrap(), LogLevel::Info);
1278        assert_eq!(LogLevel::from_str("warn").unwrap(), LogLevel::Warn);
1279        assert_eq!(LogLevel::from_str("warning").unwrap(), LogLevel::Warn);
1280        assert_eq!(LogLevel::from_str("WARN").unwrap(), LogLevel::Warn);
1281        assert_eq!(LogLevel::from_str("error").unwrap(), LogLevel::Error);
1282        assert_eq!(LogLevel::from_str("ERROR").unwrap(), LogLevel::Error);
1283
1284        assert!(LogLevel::from_str("invalid").is_err());
1285    }
1286
1287    #[test]
1288    fn test_log_level_display() {
1289        assert_eq!(LogLevel::Debug.to_string(), "DEBUG");
1290        assert_eq!(LogLevel::Info.to_string(), "INFO");
1291        assert_eq!(LogLevel::Warn.to_string(), "WARN");
1292        assert_eq!(LogLevel::Error.to_string(), "ERROR");
1293    }
1294
1295    #[test]
1296    #[serial]
1297    fn test_log_level_from_env() {
1298        // Set environment variable
1299        std::env::set_var(env_vars::LOG_LEVEL, "debug");
1300        let exporter = OtlpStdoutSpanExporter::default();
1301        assert_eq!(exporter.level, Some(LogLevel::Debug));
1302
1303        // Test with invalid level
1304        std::env::set_var(env_vars::LOG_LEVEL, "invalid");
1305        let exporter = OtlpStdoutSpanExporter::default();
1306        assert_eq!(exporter.level, None);
1307
1308        // Test with constructor parameter
1309        std::env::remove_var(env_vars::LOG_LEVEL);
1310        let exporter = OtlpStdoutSpanExporter::builder()
1311            .level(LogLevel::Error)
1312            .build();
1313        assert_eq!(exporter.level, Some(LogLevel::Error));
1314
1315        // Test env var takes precedence over constructor
1316        std::env::set_var(env_vars::LOG_LEVEL, "warn");
1317        let exporter = OtlpStdoutSpanExporter::builder()
1318            .level(LogLevel::Error)
1319            .build();
1320        assert_eq!(exporter.level, Some(LogLevel::Warn));
1321
1322        // Clean up
1323        std::env::remove_var(env_vars::LOG_LEVEL);
1324    }
1325
1326    #[tokio::test]
1327    #[serial]
1328    async fn test_log_level_in_output() {
1329        // Create a test exporter with a specific log level
1330        let (mut exporter, output) = OtlpStdoutSpanExporter::with_test_output();
1331        exporter.level = Some(LogLevel::Debug);
1332        let span = create_test_span();
1333
1334        let result = exporter.export(vec![span]).await;
1335        assert!(result.is_ok());
1336
1337        let output_lines = output.get_output();
1338        assert_eq!(output_lines.len(), 1);
1339
1340        // Parse the JSON to check the level field
1341        let json: Value = serde_json::from_str(&output_lines[0]).unwrap();
1342        assert_eq!(json["level"], "DEBUG");
1343
1344        // Test with no level set
1345        let (mut exporter, output) = OtlpStdoutSpanExporter::with_test_output();
1346        exporter.level = None;
1347        let span = create_test_span();
1348
1349        let result = exporter.export(vec![span]).await;
1350        assert!(result.is_ok());
1351
1352        let output_lines = output.get_output();
1353        assert_eq!(output_lines.len(), 1);
1354
1355        // Parse the JSON to check level field is omitted
1356        let json: Value = serde_json::from_str(&output_lines[0]).unwrap();
1357        assert!(!json.as_object().unwrap().contains_key("level"));
1358    }
1359
1360    #[test]
1361    fn test_stdout_output() {
1362        let output = create_output(false);
1363        // We can't easily test stdout directly, but we can verify the type is created
1364        assert!(format!("{:?}", output).contains("StdOutput"));
1365    }
1366
1367    #[test]
1368    fn test_pipe_output() {
1369        let output = create_output(true);
1370        // Even if pipe doesn't exist, we should get a NamedPipeOutput or StdOutput fallback
1371        let debug_str = format!("{:?}", output);
1372        assert!(debug_str.contains("NamedPipeOutput") || debug_str.contains("StdOutput"));
1373    }
1374
1375    #[test]
1376    fn test_env_var_precedence() {
1377        // Create a temporary directory for testing
1378        let temp_dir = std::env::temp_dir();
1379        let path = temp_dir.join("test_pipe");
1380
1381        // Make sure no other environment variables interfere
1382        std::env::remove_var(env_vars::OUTPUT_TYPE);
1383
1384        // Set the environment variable to use pipe
1385        std::env::set_var(env_vars::OUTPUT_TYPE, "pipe");
1386
1387        // Create the exporter
1388        let exporter = OtlpStdoutSpanExporter::default();
1389
1390        // Verify the output type
1391        let debug_str = format!("{:?}", exporter.output);
1392        assert!(debug_str.contains("NamedPipeOutput") || debug_str.contains("StdOutput"));
1393
1394        // Clean up
1395        std::env::remove_var(env_vars::OUTPUT_TYPE);
1396        if path.exists() {
1397            let _ = std::fs::remove_file(path);
1398        }
1399    }
1400
1401    #[test]
1402    fn test_constructor_precedence() {
1403        // Create a temporary directory for testing
1404        let temp_dir = std::env::temp_dir();
1405        let path = temp_dir.join("test_pipe");
1406
1407        // Make sure the environment variable is not set
1408        std::env::remove_var(env_vars::OUTPUT_TYPE);
1409
1410        // Create the exporter with pipe output
1411        let exporter = OtlpStdoutSpanExporter::builder().pipe(true).build();
1412
1413        // Verify the output type
1414        let debug_str = format!("{:?}", exporter.output);
1415        assert!(debug_str.contains("NamedPipeOutput") || debug_str.contains("StdOutput"));
1416
1417        // Clean up
1418        if path.exists() {
1419            let _ = std::fs::remove_file(path);
1420        }
1421    }
1422
1423    #[test]
1424    fn test_env_var_overrides_constructor() {
1425        // Create temporary directory for testing
1426        let temp_dir = std::env::temp_dir();
1427        let path = temp_dir.join("test_pipe");
1428
1429        // Set the environment variable to use pipe
1430        std::env::set_var(env_vars::OUTPUT_TYPE, "pipe");
1431
1432        // Create the exporter with stdout in constructor
1433        let exporter = OtlpStdoutSpanExporter::builder().pipe(false).build();
1434
1435        // Verify that env var took precedence (pipe output)
1436        let debug_str = format!("{:?}", exporter.output);
1437        assert!(debug_str.contains("NamedPipeOutput") || debug_str.contains("StdOutput"));
1438
1439        // Clean up
1440        std::env::remove_var(env_vars::OUTPUT_TYPE);
1441        if path.exists() {
1442            let _ = std::fs::remove_file(path);
1443        }
1444    }
1445}