clnrm_core/validation/
otel.rs

1//! OpenTelemetry validation for observability testing
2//!
3//! This module provides comprehensive validation of OpenTelemetry instrumentation,
4//! following the TTBD (Test That Backs Documentation) philosophy - ensuring that
5//! observability claims are backed by verifiable telemetry data.
6//!
7//! ## Core Validation Capabilities
8//!
9//! 1. **Span Creation Validation** - Verify that operations create expected spans
10//! 2. **Span Attribute Validation** - Validate span attributes match claims
11//! 3. **Trace Completeness** - Ensure all expected spans are present in traces
12//! 4. **Export Validation** - Verify telemetry reaches configured destinations
13//! 5. **Performance Overhead** - Measure and validate telemetry performance impact
14//!
15//! ## Design Principles
16//!
17//! - **Zero Unwrap/Expect** - All operations return Result<T, CleanroomError>
18//! - **Sync Trait Methods** - Maintains dyn compatibility
19//! - **AAA Test Pattern** - Arrange, Act, Assert structure
20//! - **No False Positives** - Uses unimplemented!() for incomplete features
21
22use crate::error::{CleanroomError, Result};
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25
26/// OpenTelemetry validation configuration
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct OtelValidationConfig {
29    /// Enable span validation
30    pub validate_spans: bool,
31    /// Enable trace completeness validation
32    pub validate_traces: bool,
33    /// Enable export validation
34    pub validate_exports: bool,
35    /// Enable performance overhead validation
36    pub validate_performance: bool,
37    /// Maximum allowed performance overhead in milliseconds
38    pub max_overhead_ms: f64,
39    /// Expected span attributes
40    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, // Requires external collector
49            validate_performance: true,
50            max_overhead_ms: 100.0,
51            expected_attributes: HashMap::new(),
52        }
53    }
54}
55
56/// Span assertion configuration
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct SpanAssertion {
59    /// Expected span name (operation name)
60    pub name: String,
61    /// Expected span attributes
62    pub attributes: HashMap<String, String>,
63    /// Whether span must exist
64    pub required: bool,
65    /// Minimum span duration in milliseconds
66    pub min_duration_ms: Option<f64>,
67    /// Maximum span duration in milliseconds
68    pub max_duration_ms: Option<f64>,
69}
70
71/// Trace assertion configuration
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TraceAssertion {
74    /// Expected trace ID (optional, for specific trace validation)
75    pub trace_id: Option<String>,
76    /// Expected spans in the trace
77    pub expected_spans: Vec<SpanAssertion>,
78    /// Whether all spans must be present
79    pub complete: bool,
80    /// Expected parent-child relationships
81    pub parent_child_relationships: Vec<(String, String)>, // (parent_name, child_name)
82}
83
84/// Span validation result
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SpanValidationResult {
87    /// Whether validation passed
88    pub passed: bool,
89    /// Span name that was validated
90    pub span_name: String,
91    /// Validation errors (if any)
92    pub errors: Vec<String>,
93    /// Actual span attributes found
94    pub actual_attributes: HashMap<String, String>,
95    /// Actual span duration in milliseconds
96    pub actual_duration_ms: Option<f64>,
97}
98
99/// Trace validation result
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct TraceValidationResult {
102    /// Whether validation passed
103    pub passed: bool,
104    /// Trace ID that was validated
105    pub trace_id: Option<String>,
106    /// Number of expected spans
107    pub expected_span_count: usize,
108    /// Number of actual spans found
109    pub actual_span_count: usize,
110    /// Individual span validation results
111    pub span_results: Vec<SpanValidationResult>,
112    /// Validation errors (if any)
113    pub errors: Vec<String>,
114}
115
116/// OpenTelemetry validator
117#[derive(Debug, Clone)]
118pub struct OtelValidator {
119    /// Validation configuration
120    config: OtelValidationConfig,
121}
122
123impl OtelValidator {
124    /// Create a new OTel validator with default configuration
125    pub fn new() -> Self {
126        Self {
127            config: OtelValidationConfig::default(),
128        }
129    }
130
131    /// Create a new OTel validator with custom configuration
132    pub fn with_config(config: OtelValidationConfig) -> Self {
133        Self { config }
134    }
135
136    /// Validate a span assertion
137    ///
138    /// This method validates that a span with the expected attributes exists.
139    /// Following core team standards:
140    /// - No .unwrap() or .expect()
141    /// - Sync method (dyn compatible)
142    /// - Returns Result<T, CleanroomError>
143    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        // CRITICAL: This is a placeholder implementation
155        // Real implementation requires integration with OTel SDK's span processor
156        // or in-memory span exporter for testing
157        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    /// Validate a trace assertion
168    ///
169    /// This method validates that a complete trace with all expected spans exists.
170    /// Following core team standards:
171    /// - No .unwrap() or .expect()
172    /// - Sync method (dyn compatible)
173    /// - Returns Result<T, CleanroomError>
174    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        // CRITICAL: This is a placeholder implementation
187        // Real implementation requires integration with OTel SDK's span processor
188        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    /// Validate telemetry export
200    ///
201    /// This method validates that telemetry data reaches configured destinations.
202    /// Following core team standards:
203    /// - No .unwrap() or .expect()
204    /// - Sync method (dyn compatible)
205    /// - Returns Result<T, CleanroomError>
206    pub fn validate_export(&self, _endpoint: &str) -> Result<bool> {
207        if !self.config.validate_exports {
208            return Ok(true);
209        }
210
211        // CRITICAL: This is a placeholder implementation
212        // Real implementation requires:
213        // 1. Mock OTLP collector or test endpoint
214        // 2. Span data capture and verification
215        // 3. Export success/failure tracking
216        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    /// Validate performance overhead
228    ///
229    /// This method measures telemetry performance impact.
230    /// Following core team standards:
231    /// - No .unwrap() or .expect()
232    /// - Sync method (dyn compatible)
233    /// - Returns Result<T, CleanroomError>
234    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    /// Get validation configuration
256    pub fn config(&self) -> &OtelValidationConfig {
257        &self.config
258    }
259
260    /// Update validation configuration
261    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
272/// Helper function to create span assertion from TOML configuration
273pub 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
283/// Helper function to create trace assertion from TOML configuration
284pub 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        // Arrange & Act
303        let validator = OtelValidator::new();
304
305        // Assert
306        assert!(validator.config().validate_spans);
307        assert!(validator.config().validate_traces);
308    }
309
310    #[test]
311    fn test_otel_validator_with_custom_config() {
312        // Arrange
313        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        // Act
323        let validator = OtelValidator::with_config(config.clone());
324
325        // Assert
326        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        // Arrange
334        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        // Act
339        let assertion = span_assertion_from_toml("test.span", attributes.clone());
340
341        // Assert
342        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        // Arrange
350        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        // Act
366        let assertion =
367            trace_assertion_from_toml(Some("trace-123".to_string()), vec![span1, span2]);
368
369        // Assert
370        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        // Arrange
378        let validator = OtelValidator::new();
379        let baseline = 100.0;
380        let with_telemetry = 150.0; // 50ms overhead < 100ms max
381
382        // Act
383        let result = validator.validate_performance_overhead(baseline, with_telemetry);
384
385        // Assert
386        assert!(result.is_ok());
387        assert!(result?);
388        Ok(())
389    }
390
391    #[test]
392    fn test_performance_overhead_validation_failure() {
393        // Arrange
394        let validator = OtelValidator::new();
395        let baseline = 100.0;
396        let with_telemetry = 250.0; // 150ms overhead > 100ms max
397
398        // Act
399        let result = validator.validate_performance_overhead(baseline, with_telemetry);
400
401        // Assert
402        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        // Arrange
410        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; // Large overhead but validation disabled
417
418        // Act
419        let result = validator.validate_performance_overhead(baseline, with_telemetry);
420
421        // Assert
422        assert!(result.is_ok());
423    }
424
425    #[test]
426    fn test_otel_config_default() {
427        // Arrange & Act
428        let config = OtelValidationConfig::default();
429
430        // Assert
431        assert!(config.validate_spans);
432        assert!(config.validate_traces);
433        assert!(!config.validate_exports); // Disabled by default
434        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        // Arrange
441        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
450        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        // Arrange
457        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
468        assert_eq!(assertion.parent_child_relationships.len(), 2);
469        assert_eq!(assertion.parent_child_relationships[0].0, "parent_span");
470    }
471}