Skip to main content

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    // ==================== ExporterType Tests ====================
197
198    #[test]
199    fn test_exporter_type_debug() {
200        assert_eq!(format!("{:?}", ExporterType::Jaeger), "Jaeger");
201        assert_eq!(format!("{:?}", ExporterType::Otlp), "Otlp");
202    }
203
204    #[test]
205    fn test_exporter_type_clone() {
206        let exporter = ExporterType::Jaeger;
207        let cloned = exporter.clone();
208        assert_eq!(exporter, cloned);
209    }
210
211    #[test]
212    fn test_exporter_type_eq() {
213        assert_eq!(ExporterType::Jaeger, ExporterType::Jaeger);
214        assert_eq!(ExporterType::Otlp, ExporterType::Otlp);
215        assert_ne!(ExporterType::Jaeger, ExporterType::Otlp);
216    }
217
218    // ==================== JaegerExporter Tests ====================
219
220    #[test]
221    fn test_jaeger_default_config() {
222        let exporter = JaegerExporter::default();
223        assert_eq!(exporter.endpoint, "http://localhost:14268/api/traces");
224        assert_eq!(exporter.max_batch_size, 512);
225        assert_eq!(exporter.max_queue_size, 2048);
226        assert!(exporter.validate().is_ok());
227    }
228
229    #[test]
230    fn test_jaeger_custom_config() {
231        let exporter = JaegerExporter::new("http://custom:14268/api/traces".to_string())
232            .with_max_batch_size(256)
233            .with_max_queue_size(1024)
234            .with_batch_timeout(Duration::from_secs(10));
235
236        assert_eq!(exporter.endpoint, "http://custom:14268/api/traces");
237        assert_eq!(exporter.max_batch_size, 256);
238        assert_eq!(exporter.max_queue_size, 1024);
239        assert_eq!(exporter.batch_timeout, Duration::from_secs(10));
240        assert!(exporter.validate().is_ok());
241    }
242
243    #[test]
244    fn test_jaeger_invalid_config() {
245        let exporter = JaegerExporter::new("http://localhost:14268".to_string())
246            .with_max_batch_size(1024)
247            .with_max_queue_size(512); // Less than batch size
248
249        assert!(exporter.validate().is_err());
250    }
251
252    #[test]
253    fn test_jaeger_empty_endpoint() {
254        let exporter = JaegerExporter::new("".to_string());
255        assert!(exporter.validate().is_err());
256    }
257
258    #[test]
259    fn test_jaeger_zero_batch_size() {
260        let exporter = JaegerExporter::new("http://localhost:14268/api/traces".to_string())
261            .with_max_batch_size(0);
262        let result = exporter.validate();
263        assert!(result.is_err());
264        assert!(matches!(result.unwrap_err(), ExporterError::InvalidConfig(_)));
265    }
266
267    #[test]
268    fn test_jaeger_debug() {
269        let exporter = JaegerExporter::default();
270        let debug_str = format!("{:?}", exporter);
271        assert!(debug_str.contains("JaegerExporter"));
272        assert!(debug_str.contains("endpoint"));
273    }
274
275    #[test]
276    fn test_jaeger_clone() {
277        let exporter =
278            JaegerExporter::new("http://test:14268".to_string()).with_max_batch_size(100);
279        let cloned = exporter.clone();
280        assert_eq!(cloned.endpoint, exporter.endpoint);
281        assert_eq!(cloned.max_batch_size, exporter.max_batch_size);
282    }
283
284    #[test]
285    fn test_jaeger_queue_equals_batch() {
286        // Queue size equal to batch size should be valid
287        let exporter = JaegerExporter::new("http://localhost:14268/api/traces".to_string())
288            .with_max_batch_size(512)
289            .with_max_queue_size(512);
290        assert!(exporter.validate().is_ok());
291    }
292
293    // ==================== OtlpExporter Tests ====================
294
295    #[test]
296    fn test_otlp_default_config() {
297        let exporter = OtlpExporter::default();
298        assert_eq!(exporter.endpoint, "http://localhost:4317");
299        assert_eq!(exporter.protocol, OtlpProtocol::Grpc);
300        assert!(exporter.headers.is_empty());
301        assert_eq!(exporter.timeout, Duration::from_secs(10));
302        assert!(exporter.compression.is_none());
303        assert!(exporter.validate().is_ok());
304    }
305
306    #[test]
307    fn test_otlp_custom_config() {
308        let exporter = OtlpExporter::new("https://otel-collector:4317".to_string())
309            .with_protocol(OtlpProtocol::HttpProtobuf)
310            .with_header("Authorization".to_string(), "Bearer token123".to_string())
311            .with_timeout(Duration::from_secs(30))
312            .with_compression(OtlpCompression::Gzip);
313
314        assert_eq!(exporter.endpoint, "https://otel-collector:4317");
315        assert_eq!(exporter.protocol, OtlpProtocol::HttpProtobuf);
316        assert_eq!(exporter.headers.len(), 1);
317        assert_eq!(exporter.timeout, Duration::from_secs(30));
318        assert_eq!(exporter.compression, Some(OtlpCompression::Gzip));
319        assert!(exporter.validate().is_ok());
320    }
321
322    #[test]
323    fn test_otlp_empty_endpoint() {
324        let exporter = OtlpExporter::new("".to_string());
325        assert!(exporter.validate().is_err());
326    }
327
328    #[test]
329    fn test_otlp_invalid_endpoint_protocol() {
330        let exporter = OtlpExporter::new("ftp://localhost:4317".to_string());
331        assert!(exporter.validate().is_err());
332    }
333
334    #[test]
335    fn test_otlp_multiple_headers() {
336        let exporter = OtlpExporter::new("http://localhost:4317".to_string())
337            .with_header("X-API-Key".to_string(), "key123".to_string())
338            .with_header("X-Tenant-ID".to_string(), "tenant1".to_string());
339
340        assert_eq!(exporter.headers.len(), 2);
341    }
342
343    #[test]
344    fn test_otlp_https_endpoint() {
345        let exporter = OtlpExporter::new("https://secure-collector:4317".to_string());
346        assert!(exporter.validate().is_ok());
347    }
348
349    #[test]
350    fn test_otlp_debug() {
351        let exporter = OtlpExporter::default();
352        let debug_str = format!("{:?}", exporter);
353        assert!(debug_str.contains("OtlpExporter"));
354        assert!(debug_str.contains("endpoint"));
355    }
356
357    #[test]
358    fn test_otlp_clone() {
359        let exporter = OtlpExporter::new("http://test:4317".to_string())
360            .with_protocol(OtlpProtocol::HttpProtobuf)
361            .with_compression(OtlpCompression::Gzip);
362        let cloned = exporter.clone();
363        assert_eq!(cloned.endpoint, exporter.endpoint);
364        assert_eq!(cloned.protocol, exporter.protocol);
365        assert_eq!(cloned.compression, exporter.compression);
366    }
367
368    // ==================== OtlpProtocol Tests ====================
369
370    #[test]
371    fn test_otlp_protocol_debug() {
372        assert_eq!(format!("{:?}", OtlpProtocol::Grpc), "Grpc");
373        assert_eq!(format!("{:?}", OtlpProtocol::HttpProtobuf), "HttpProtobuf");
374    }
375
376    #[test]
377    fn test_otlp_protocol_clone() {
378        let proto = OtlpProtocol::Grpc;
379        let cloned = proto.clone();
380        assert_eq!(proto, cloned);
381    }
382
383    #[test]
384    fn test_otlp_protocol_copy() {
385        let proto = OtlpProtocol::HttpProtobuf;
386        let copied = proto;
387        assert_eq!(OtlpProtocol::HttpProtobuf, copied);
388    }
389
390    #[test]
391    fn test_otlp_protocol_eq() {
392        assert_eq!(OtlpProtocol::Grpc, OtlpProtocol::Grpc);
393        assert_ne!(OtlpProtocol::Grpc, OtlpProtocol::HttpProtobuf);
394    }
395
396    // ==================== OtlpCompression Tests ====================
397
398    #[test]
399    fn test_otlp_compression_debug() {
400        assert_eq!(format!("{:?}", OtlpCompression::Gzip), "Gzip");
401    }
402
403    #[test]
404    fn test_otlp_compression_clone() {
405        let comp = OtlpCompression::Gzip;
406        let cloned = comp.clone();
407        assert_eq!(comp, cloned);
408    }
409
410    #[test]
411    fn test_otlp_compression_copy() {
412        let comp = OtlpCompression::Gzip;
413        let copied = comp;
414        assert_eq!(OtlpCompression::Gzip, copied);
415    }
416
417    #[test]
418    fn test_otlp_compression_eq() {
419        assert_eq!(OtlpCompression::Gzip, OtlpCompression::Gzip);
420    }
421
422    // ==================== ExporterError Tests ====================
423
424    #[test]
425    fn test_exporter_error_invalid_endpoint() {
426        let error = ExporterError::InvalidEndpoint("test error".to_string());
427        let error_str = format!("{}", error);
428        assert!(error_str.contains("Invalid endpoint"));
429        assert!(error_str.contains("test error"));
430    }
431
432    #[test]
433    fn test_exporter_error_invalid_config() {
434        let error = ExporterError::InvalidConfig("config error".to_string());
435        let error_str = format!("{}", error);
436        assert!(error_str.contains("Invalid configuration"));
437        assert!(error_str.contains("config error"));
438    }
439
440    #[test]
441    fn test_exporter_error_export_failed() {
442        let error = ExporterError::ExportFailed("export error".to_string());
443        let error_str = format!("{}", error);
444        assert!(error_str.contains("Export failed"));
445        assert!(error_str.contains("export error"));
446    }
447
448    #[test]
449    fn test_exporter_error_debug() {
450        let error = ExporterError::InvalidEndpoint("test".to_string());
451        let debug_str = format!("{:?}", error);
452        assert!(debug_str.contains("InvalidEndpoint"));
453    }
454
455    #[test]
456    fn test_jaeger_exporter_error_alias() {
457        // Test the type alias for backwards compatibility
458        let error: JaegerExporterError = ExporterError::InvalidEndpoint("test".to_string());
459        assert!(matches!(error, ExporterError::InvalidEndpoint(_)));
460    }
461
462    // ==================== Validation Edge Cases ====================
463
464    #[test]
465    fn test_jaeger_validation_error_messages() {
466        let exporter = JaegerExporter::new("".to_string());
467        if let Err(e) = exporter.validate() {
468            let error_msg = format!("{}", e);
469            assert!(error_msg.contains("empty"));
470        } else {
471            panic!("Expected validation error");
472        }
473    }
474
475    #[test]
476    fn test_otlp_validation_error_messages() {
477        let exporter = OtlpExporter::new("invalid-url".to_string());
478        if let Err(e) = exporter.validate() {
479            let error_msg = format!("{}", e);
480            assert!(error_msg.contains("http://") || error_msg.contains("https://"));
481        } else {
482            panic!("Expected validation error");
483        }
484    }
485
486    #[test]
487    fn test_otlp_no_compression() {
488        let exporter = OtlpExporter::new("http://localhost:4317".to_string());
489        assert!(exporter.compression.is_none());
490    }
491
492    #[test]
493    fn test_jaeger_default_batch_timeout() {
494        let exporter = JaegerExporter::default();
495        assert_eq!(exporter.batch_timeout, Duration::from_secs(5));
496    }
497}