1use crate::error::{CleanroomError, Result};
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct OtelValidationConfig {
29 pub validate_spans: bool,
31 pub validate_traces: bool,
33 pub validate_exports: bool,
35 pub validate_performance: bool,
37 pub max_overhead_ms: f64,
39 pub expected_attributes: HashMap<String, String>,
41}
42
43impl Default for OtelValidationConfig {
44 fn default() -> Self {
45 Self {
46 validate_spans: true,
47 validate_traces: true,
48 validate_exports: false, validate_performance: true,
50 max_overhead_ms: 100.0,
51 expected_attributes: HashMap::new(),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct SpanAssertion {
59 pub name: String,
61 pub attributes: HashMap<String, String>,
63 pub required: bool,
65 pub min_duration_ms: Option<f64>,
67 pub max_duration_ms: Option<f64>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TraceAssertion {
74 pub trace_id: Option<String>,
76 pub expected_spans: Vec<SpanAssertion>,
78 pub complete: bool,
80 pub parent_child_relationships: Vec<(String, String)>, }
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SpanValidationResult {
87 pub passed: bool,
89 pub span_name: String,
91 pub errors: Vec<String>,
93 pub actual_attributes: HashMap<String, String>,
95 pub actual_duration_ms: Option<f64>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct TraceValidationResult {
102 pub passed: bool,
104 pub trace_id: Option<String>,
106 pub expected_span_count: usize,
108 pub actual_span_count: usize,
110 pub span_results: Vec<SpanValidationResult>,
112 pub errors: Vec<String>,
114}
115
116#[derive(Debug, Clone)]
118pub struct OtelValidator {
119 config: OtelValidationConfig,
121}
122
123impl OtelValidator {
124 pub fn new() -> Self {
126 Self {
127 config: OtelValidationConfig::default(),
128 }
129 }
130
131 pub fn with_config(config: OtelValidationConfig) -> Self {
133 Self { config }
134 }
135
136 pub fn validate_span(&self, assertion: &SpanAssertion) -> Result<SpanValidationResult> {
144 if !self.config.validate_spans {
145 return Ok(SpanValidationResult {
146 passed: true,
147 span_name: assertion.name.clone(),
148 errors: vec!["Span validation disabled".to_string()],
149 actual_attributes: HashMap::new(),
150 actual_duration_ms: None,
151 });
152 }
153
154 unimplemented!(
158 "validate_span: Requires integration with OpenTelemetry span processor. \
159 Future implementation will:\n\
160 1. Query in-memory span exporter for spans matching assertion.name\n\
161 2. Validate span attributes against assertion.attributes\n\
162 3. Validate span duration if min/max_duration_ms specified\n\
163 4. Return detailed validation results"
164 )
165 }
166
167 pub fn validate_trace(&self, assertion: &TraceAssertion) -> Result<TraceValidationResult> {
175 if !self.config.validate_traces {
176 return Ok(TraceValidationResult {
177 passed: true,
178 trace_id: assertion.trace_id.clone(),
179 expected_span_count: assertion.expected_spans.len(),
180 actual_span_count: 0,
181 span_results: Vec::new(),
182 errors: vec!["Trace validation disabled".to_string()],
183 });
184 }
185
186 unimplemented!(
189 "validate_trace: Requires integration with OpenTelemetry span processor. \
190 Future implementation will:\n\
191 1. Query spans by trace_id if provided\n\
192 2. Validate each expected_span using validate_span\n\
193 3. Validate parent-child relationships from parent_child_relationships\n\
194 4. Check trace completeness if assertion.complete is true\n\
195 5. Return comprehensive trace validation results"
196 )
197 }
198
199 pub fn validate_export(&self, _endpoint: &str) -> Result<bool> {
207 if !self.config.validate_exports {
208 return Ok(true);
209 }
210
211 unimplemented!(
217 "validate_export: Requires mock OTLP collector implementation. \
218 Future implementation will:\n\
219 1. Start mock OTLP collector at endpoint\n\
220 2. Generate test spans\n\
221 3. Verify spans reach the collector\n\
222 4. Validate span data integrity\n\
223 5. Return export validation result"
224 )
225 }
226
227 pub fn validate_performance_overhead(
235 &self,
236 baseline_duration_ms: f64,
237 with_telemetry_duration_ms: f64,
238 ) -> Result<bool> {
239 if !self.config.validate_performance {
240 return Ok(true);
241 }
242
243 let overhead_ms = with_telemetry_duration_ms - baseline_duration_ms;
244
245 if overhead_ms > self.config.max_overhead_ms {
246 return Err(CleanroomError::validation_error(format!(
247 "Telemetry performance overhead {}ms exceeds maximum allowed {}ms",
248 overhead_ms, self.config.max_overhead_ms
249 )));
250 }
251
252 Ok(true)
253 }
254
255 pub fn config(&self) -> &OtelValidationConfig {
257 &self.config
258 }
259
260 pub fn set_config(&mut self, config: OtelValidationConfig) {
262 self.config = config;
263 }
264}
265
266impl Default for OtelValidator {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272pub fn span_assertion_from_toml(name: &str, attributes: HashMap<String, String>) -> SpanAssertion {
274 SpanAssertion {
275 name: name.to_string(),
276 attributes,
277 required: true,
278 min_duration_ms: None,
279 max_duration_ms: None,
280 }
281}
282
283pub fn trace_assertion_from_toml(
285 trace_id: Option<String>,
286 span_assertions: Vec<SpanAssertion>,
287) -> TraceAssertion {
288 TraceAssertion {
289 trace_id,
290 expected_spans: span_assertions,
291 complete: true,
292 parent_child_relationships: Vec::new(),
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_otel_validator_creation() {
302 let validator = OtelValidator::new();
304
305 assert!(validator.config().validate_spans);
307 assert!(validator.config().validate_traces);
308 }
309
310 #[test]
311 fn test_otel_validator_with_custom_config() {
312 let config = OtelValidationConfig {
314 validate_spans: false,
315 validate_traces: true,
316 validate_exports: false,
317 validate_performance: true,
318 max_overhead_ms: 50.0,
319 expected_attributes: HashMap::new(),
320 };
321
322 let validator = OtelValidator::with_config(config.clone());
324
325 assert!(!validator.config().validate_spans);
327 assert!(validator.config().validate_traces);
328 assert_eq!(validator.config().max_overhead_ms, 50.0);
329 }
330
331 #[test]
332 fn test_span_assertion_creation() {
333 let mut attributes = HashMap::new();
335 attributes.insert("service.name".to_string(), "test-service".to_string());
336 attributes.insert("operation".to_string(), "test-operation".to_string());
337
338 let assertion = span_assertion_from_toml("test.span", attributes.clone());
340
341 assert_eq!(assertion.name, "test.span");
343 assert_eq!(assertion.attributes.len(), 2);
344 assert!(assertion.required);
345 }
346
347 #[test]
348 fn test_trace_assertion_creation() {
349 let span1 = SpanAssertion {
351 name: "span1".to_string(),
352 attributes: HashMap::new(),
353 required: true,
354 min_duration_ms: None,
355 max_duration_ms: None,
356 };
357 let span2 = SpanAssertion {
358 name: "span2".to_string(),
359 attributes: HashMap::new(),
360 required: true,
361 min_duration_ms: None,
362 max_duration_ms: None,
363 };
364
365 let assertion =
367 trace_assertion_from_toml(Some("trace-123".to_string()), vec![span1, span2]);
368
369 assert_eq!(assertion.trace_id, Some("trace-123".to_string()));
371 assert_eq!(assertion.expected_spans.len(), 2);
372 assert!(assertion.complete);
373 }
374
375 #[test]
376 fn test_performance_overhead_validation_success() -> Result<()> {
377 let validator = OtelValidator::new();
379 let baseline = 100.0;
380 let with_telemetry = 150.0; let result = validator.validate_performance_overhead(baseline, with_telemetry);
384
385 assert!(result.is_ok());
387 assert!(result?);
388 Ok(())
389 }
390
391 #[test]
392 fn test_performance_overhead_validation_failure() {
393 let validator = OtelValidator::new();
395 let baseline = 100.0;
396 let with_telemetry = 250.0; let result = validator.validate_performance_overhead(baseline, with_telemetry);
400
401 assert!(result.is_err());
403 let error = result.unwrap_err();
404 assert!(error.message.contains("exceeds maximum allowed"));
405 }
406
407 #[test]
408 fn test_performance_overhead_validation_disabled() {
409 let config = OtelValidationConfig {
411 validate_performance: false,
412 ..Default::default()
413 };
414 let validator = OtelValidator::with_config(config);
415 let baseline = 100.0;
416 let with_telemetry = 1000.0; let result = validator.validate_performance_overhead(baseline, with_telemetry);
420
421 assert!(result.is_ok());
423 }
424
425 #[test]
426 fn test_otel_config_default() {
427 let config = OtelValidationConfig::default();
429
430 assert!(config.validate_spans);
432 assert!(config.validate_traces);
433 assert!(!config.validate_exports); assert!(config.validate_performance);
435 assert_eq!(config.max_overhead_ms, 100.0);
436 }
437
438 #[test]
439 fn test_span_assertion_with_duration_constraints() {
440 let assertion = SpanAssertion {
442 name: "test.span".to_string(),
443 attributes: HashMap::new(),
444 required: true,
445 min_duration_ms: Some(10.0),
446 max_duration_ms: Some(1000.0),
447 };
448
449 assert_eq!(assertion.min_duration_ms, Some(10.0));
451 assert_eq!(assertion.max_duration_ms, Some(1000.0));
452 }
453
454 #[test]
455 fn test_trace_assertion_with_relationships() {
456 let assertion = TraceAssertion {
458 trace_id: None,
459 expected_spans: Vec::new(),
460 complete: true,
461 parent_child_relationships: vec![
462 ("parent_span".to_string(), "child_span_1".to_string()),
463 ("parent_span".to_string(), "child_span_2".to_string()),
464 ],
465 };
466
467 assert_eq!(assertion.parent_child_relationships.len(), 2);
469 assert_eq!(assertion.parent_child_relationships[0].0, "parent_span");
470 }
471}