1use std::time::Duration;
4use thiserror::Error;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ExporterType {
9 Jaeger,
10 Otlp,
11}
12
13#[derive(Debug, Clone)]
15pub struct JaegerExporter {
16 pub endpoint: String,
18 pub max_batch_size: usize,
20 pub max_queue_size: usize,
22 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 pub fn new(endpoint: String) -> Self {
40 Self {
41 endpoint,
42 ..Default::default()
43 }
44 }
45
46 pub fn with_max_batch_size(mut self, size: usize) -> Self {
48 self.max_batch_size = size;
49 self
50 }
51
52 pub fn with_max_queue_size(mut self, size: usize) -> Self {
54 self.max_queue_size = size;
55 self
56 }
57
58 pub fn with_batch_timeout(mut self, timeout: Duration) -> Self {
60 self.batch_timeout = timeout;
61 self
62 }
63
64 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#[derive(Debug, Clone)]
88pub struct OtlpExporter {
89 pub endpoint: String,
91 pub protocol: OtlpProtocol,
93 pub headers: Vec<(String, String)>,
95 pub timeout: Duration,
97 pub compression: Option<OtlpCompression>,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum OtlpProtocol {
104 Grpc,
105 HttpProtobuf,
106}
107
108#[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 pub fn new(endpoint: String) -> Self {
129 Self {
130 endpoint,
131 ..Default::default()
132 }
133 }
134
135 pub fn with_protocol(mut self, protocol: OtlpProtocol) -> Self {
137 self.protocol = protocol;
138 self
139 }
140
141 pub fn with_header(mut self, key: String, value: String) -> Self {
143 self.headers.push((key, value));
144 self
145 }
146
147 pub fn with_timeout(mut self, timeout: Duration) -> Self {
149 self.timeout = timeout;
150 self
151 }
152
153 pub fn with_compression(mut self, compression: OtlpCompression) -> Self {
155 self.compression = Some(compression);
156 self
157 }
158
159 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 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#[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
189pub type JaegerExporterError = ExporterError;
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[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 #[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); 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 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 #[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 #[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 = Clone::clone(&proto);
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 #[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 = Clone::clone(&comp);
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 #[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 let error: JaegerExporterError = ExporterError::InvalidEndpoint("test".to_string());
459 assert!(matches!(error, ExporterError::InvalidEndpoint(_)));
460 }
461
462 #[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}