mockforge_tracing/
exporter.rs

1//! Exporter configuration and utilities for Jaeger and OTLP
2
3use std::time::Duration;
4use thiserror::Error;
5
6/// Exporter type
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ExporterType {
9    Jaeger,
10    Otlp,
11}
12
13/// Jaeger exporter configuration
14#[derive(Debug, Clone)]
15pub struct JaegerExporter {
16    /// Jaeger agent endpoint
17    pub endpoint: String,
18    /// Maximum batch size for spans
19    pub max_batch_size: usize,
20    /// Maximum queue size
21    pub max_queue_size: usize,
22    /// Batch timeout
23    pub batch_timeout: Duration,
24}
25
26impl Default for JaegerExporter {
27    fn default() -> Self {
28        Self {
29            endpoint: "http://localhost:14268/api/traces".to_string(),
30            max_batch_size: 512,
31            max_queue_size: 2048,
32            batch_timeout: Duration::from_secs(5),
33        }
34    }
35}
36
37impl JaegerExporter {
38    /// Create a new Jaeger exporter with custom configuration
39    pub fn new(endpoint: String) -> Self {
40        Self {
41            endpoint,
42            ..Default::default()
43        }
44    }
45
46    /// Set maximum batch size
47    pub fn with_max_batch_size(mut self, size: usize) -> Self {
48        self.max_batch_size = size;
49        self
50    }
51
52    /// Set maximum queue size
53    pub fn with_max_queue_size(mut self, size: usize) -> Self {
54        self.max_queue_size = size;
55        self
56    }
57
58    /// Set batch timeout
59    pub fn with_batch_timeout(mut self, timeout: Duration) -> Self {
60        self.batch_timeout = timeout;
61        self
62    }
63
64    /// Validate configuration
65    pub fn validate(&self) -> Result<(), ExporterError> {
66        if self.endpoint.is_empty() {
67            return Err(ExporterError::InvalidEndpoint("Endpoint cannot be empty".to_string()));
68        }
69
70        if self.max_batch_size == 0 {
71            return Err(ExporterError::InvalidConfig(
72                "Max batch size must be greater than 0".to_string(),
73            ));
74        }
75
76        if self.max_queue_size < self.max_batch_size {
77            return Err(ExporterError::InvalidConfig(
78                "Max queue size must be >= max batch size".to_string(),
79            ));
80        }
81
82        Ok(())
83    }
84}
85
86/// OTLP exporter configuration
87#[derive(Debug, Clone)]
88pub struct OtlpExporter {
89    /// OTLP endpoint (e.g., "http://localhost:4317" for gRPC)
90    pub endpoint: String,
91    /// Protocol (grpc or http/protobuf)
92    pub protocol: OtlpProtocol,
93    /// Optional headers for authentication
94    pub headers: Vec<(String, String)>,
95    /// Timeout for export requests
96    pub timeout: Duration,
97    /// Compression (none, gzip)
98    pub compression: Option<OtlpCompression>,
99}
100
101/// OTLP protocol type
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum OtlpProtocol {
104    Grpc,
105    HttpProtobuf,
106}
107
108/// OTLP compression type
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum OtlpCompression {
111    Gzip,
112}
113
114impl Default for OtlpExporter {
115    fn default() -> Self {
116        Self {
117            endpoint: "http://localhost:4317".to_string(),
118            protocol: OtlpProtocol::Grpc,
119            headers: Vec::new(),
120            timeout: Duration::from_secs(10),
121            compression: None,
122        }
123    }
124}
125
126impl OtlpExporter {
127    /// Create a new OTLP exporter with custom endpoint
128    pub fn new(endpoint: String) -> Self {
129        Self {
130            endpoint,
131            ..Default::default()
132        }
133    }
134
135    /// Set protocol
136    pub fn with_protocol(mut self, protocol: OtlpProtocol) -> Self {
137        self.protocol = protocol;
138        self
139    }
140
141    /// Add authentication header
142    pub fn with_header(mut self, key: String, value: String) -> Self {
143        self.headers.push((key, value));
144        self
145    }
146
147    /// Set timeout
148    pub fn with_timeout(mut self, timeout: Duration) -> Self {
149        self.timeout = timeout;
150        self
151    }
152
153    /// Enable compression
154    pub fn with_compression(mut self, compression: OtlpCompression) -> Self {
155        self.compression = Some(compression);
156        self
157    }
158
159    /// Validate configuration
160    pub fn validate(&self) -> Result<(), ExporterError> {
161        if self.endpoint.is_empty() {
162            return Err(ExporterError::InvalidEndpoint("Endpoint cannot be empty".to_string()));
163        }
164
165        // Validate URL format
166        if !self.endpoint.starts_with("http://") && !self.endpoint.starts_with("https://") {
167            return Err(ExporterError::InvalidEndpoint(
168                "Endpoint must start with http:// or https://".to_string(),
169            ));
170        }
171
172        Ok(())
173    }
174}
175
176/// Exporter configuration errors
177#[derive(Error, Debug)]
178pub enum ExporterError {
179    #[error("Invalid endpoint: {0}")]
180    InvalidEndpoint(String),
181
182    #[error("Invalid configuration: {0}")]
183    InvalidConfig(String),
184
185    #[error("Export failed: {0}")]
186    ExportFailed(String),
187}
188
189// Maintain backwards compatibility
190pub type JaegerExporterError = ExporterError;
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_jaeger_default_config() {
198        let exporter = JaegerExporter::default();
199        assert_eq!(exporter.endpoint, "http://localhost:14268/api/traces");
200        assert_eq!(exporter.max_batch_size, 512);
201        assert_eq!(exporter.max_queue_size, 2048);
202        assert!(exporter.validate().is_ok());
203    }
204
205    #[test]
206    fn test_jaeger_custom_config() {
207        let exporter = JaegerExporter::new("http://custom:14268/api/traces".to_string())
208            .with_max_batch_size(256)
209            .with_max_queue_size(1024)
210            .with_batch_timeout(Duration::from_secs(10));
211
212        assert_eq!(exporter.endpoint, "http://custom:14268/api/traces");
213        assert_eq!(exporter.max_batch_size, 256);
214        assert_eq!(exporter.max_queue_size, 1024);
215        assert_eq!(exporter.batch_timeout, Duration::from_secs(10));
216        assert!(exporter.validate().is_ok());
217    }
218
219    #[test]
220    fn test_jaeger_invalid_config() {
221        let exporter = JaegerExporter::new("http://localhost:14268".to_string())
222            .with_max_batch_size(1024)
223            .with_max_queue_size(512); // Less than batch size
224
225        assert!(exporter.validate().is_err());
226    }
227
228    #[test]
229    fn test_jaeger_empty_endpoint() {
230        let exporter = JaegerExporter::new("".to_string());
231        assert!(exporter.validate().is_err());
232    }
233
234    #[test]
235    fn test_otlp_default_config() {
236        let exporter = OtlpExporter::default();
237        assert_eq!(exporter.endpoint, "http://localhost:4317");
238        assert_eq!(exporter.protocol, OtlpProtocol::Grpc);
239        assert!(exporter.headers.is_empty());
240        assert_eq!(exporter.timeout, Duration::from_secs(10));
241        assert!(exporter.compression.is_none());
242        assert!(exporter.validate().is_ok());
243    }
244
245    #[test]
246    fn test_otlp_custom_config() {
247        let exporter = OtlpExporter::new("https://otel-collector:4317".to_string())
248            .with_protocol(OtlpProtocol::HttpProtobuf)
249            .with_header("Authorization".to_string(), "Bearer token123".to_string())
250            .with_timeout(Duration::from_secs(30))
251            .with_compression(OtlpCompression::Gzip);
252
253        assert_eq!(exporter.endpoint, "https://otel-collector:4317");
254        assert_eq!(exporter.protocol, OtlpProtocol::HttpProtobuf);
255        assert_eq!(exporter.headers.len(), 1);
256        assert_eq!(exporter.timeout, Duration::from_secs(30));
257        assert_eq!(exporter.compression, Some(OtlpCompression::Gzip));
258        assert!(exporter.validate().is_ok());
259    }
260
261    #[test]
262    fn test_otlp_empty_endpoint() {
263        let exporter = OtlpExporter::new("".to_string());
264        assert!(exporter.validate().is_err());
265    }
266
267    #[test]
268    fn test_otlp_invalid_endpoint_protocol() {
269        let exporter = OtlpExporter::new("ftp://localhost:4317".to_string());
270        assert!(exporter.validate().is_err());
271    }
272
273    #[test]
274    fn test_otlp_multiple_headers() {
275        let exporter = OtlpExporter::new("http://localhost:4317".to_string())
276            .with_header("X-API-Key".to_string(), "key123".to_string())
277            .with_header("X-Tenant-ID".to_string(), "tenant1".to_string());
278
279        assert_eq!(exporter.headers.len(), 2);
280    }
281}