clnrm_core/otel/validators/
window.rs

1//! Temporal window validation for fake-green detection
2//!
3//! Validates that spans are temporally contained within outer spans.
4//! This ensures proper execution flow and detects timing anomalies.
5
6use crate::error::Result;
7use crate::validation::span_validator::SpanData;
8use serde::{Deserialize, Serialize};
9
10/// Window validation result
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ValidationResult {
13    /// Whether validation passed
14    pub passed: bool,
15    /// Validation error messages
16    pub errors: Vec<String>,
17    /// Number of windows checked
18    pub windows_checked: usize,
19}
20
21impl ValidationResult {
22    /// Create a passing result
23    pub fn pass(windows_checked: usize) -> Self {
24        Self {
25            passed: true,
26            errors: Vec::new(),
27            windows_checked,
28        }
29    }
30
31    /// Add an error
32    pub fn add_error(&mut self, error: String) {
33        self.passed = false;
34        self.errors.push(error);
35    }
36}
37
38/// Window expectation for temporal containment validation
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct WindowExpectation {
41    /// Outer span name that defines the temporal window
42    pub outer: String,
43    /// Span names that must be temporally contained within outer
44    pub contains: Vec<String>,
45}
46
47impl WindowExpectation {
48    /// Create a new window expectation
49    pub fn new(outer: impl Into<String>, contains: Vec<String>) -> Self {
50        Self {
51            outer: outer.into(),
52            contains,
53        }
54    }
55
56    /// Validate window expectation against spans
57    ///
58    /// # Arguments
59    /// * `spans` - All spans to validate
60    ///
61    /// # Returns
62    /// * `Result<ValidationResult>` - Validation result
63    ///
64    /// # Errors
65    /// * Outer span not found
66    /// * Inner spans not found
67    /// * Temporal containment violations
68    pub fn validate(&self, spans: &[SpanData]) -> Result<ValidationResult> {
69        let validator = WindowValidator::new(spans);
70        let mut result = ValidationResult::pass(0);
71
72        // Find outer spans
73        let outer_spans: Vec<_> = spans
74            .iter()
75            .filter(|s| s.name == self.outer)
76            .collect();
77
78        if outer_spans.is_empty() {
79            result.add_error(format!(
80                "Window validation failed: outer span '{}' not found (fake-green: container never started?)",
81                self.outer
82            ));
83            return Ok(result);
84        }
85
86        // Validate each inner span is contained in at least one outer span
87        for inner_name in &self.contains {
88            result.windows_checked += 1;
89
90            let inner_spans: Vec<_> = spans
91                .iter()
92                .filter(|s| s.name == *inner_name)
93                .collect();
94
95            if inner_spans.is_empty() {
96                result.add_error(format!(
97                    "Window validation failed: inner span '{}' not found (fake-green: operation never executed?)",
98                    inner_name
99                ));
100                continue;
101            }
102
103            // Check if all inner spans are contained in at least one outer span
104            for inner in &inner_spans {
105                let is_contained = outer_spans.iter().any(|outer| {
106                    validator.is_temporally_contained(inner, outer)
107                });
108
109                if !is_contained {
110                    result.add_error(format!(
111                        "Window validation failed: span '{}' (id: {}) is not temporally contained within '{}' (fake-green: timing anomaly)",
112                        inner.name, inner.span_id, self.outer
113                    ));
114                }
115            }
116        }
117
118        Ok(result)
119    }
120}
121
122/// Window validator internal implementation
123pub struct WindowValidator<'a> {
124    /// All spans
125    _spans: &'a [SpanData],
126}
127
128impl<'a> WindowValidator<'a> {
129    /// Create a new window validator
130    pub fn new(spans: &'a [SpanData]) -> Self {
131        Self { _spans: spans }
132    }
133
134    /// Check if inner span is temporally contained within outer span
135    ///
136    /// A span is contained if:
137    /// - inner.start >= outer.start
138    /// - inner.end <= outer.end
139    pub fn is_temporally_contained(&self, inner: &SpanData, outer: &SpanData) -> bool {
140        let (Some(inner_start), Some(inner_end)) = (inner.start_time_unix_nano, inner.end_time_unix_nano) else {
141            return false;
142        };
143
144        let (Some(outer_start), Some(outer_end)) = (outer.start_time_unix_nano, outer.end_time_unix_nano) else {
145            return false;
146        };
147
148        inner_start >= outer_start && inner_end <= outer_end
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use std::collections::HashMap;
156
157    fn create_span_with_times(name: &str, start: u64, end: u64) -> SpanData {
158        SpanData {
159            name: name.to_string(),
160            span_id: format!("span_{}", name),
161            trace_id: "trace123".to_string(),
162            parent_span_id: None,
163            attributes: HashMap::new(),
164            start_time_unix_nano: Some(start),
165            end_time_unix_nano: Some(end),
166            kind: None,
167            events: None,
168            resource_attributes: HashMap::new(),
169        }
170    }
171
172    #[test]
173    fn test_window_expectation_contained() -> Result<()> {
174        // Arrange
175        let spans = vec![
176            create_span_with_times("container.lifecycle", 1000, 5000),
177            create_span_with_times("container.start", 1100, 2000),
178            create_span_with_times("container.exec", 2100, 3000),
179        ];
180        let expectation = WindowExpectation::new(
181            "container.lifecycle",
182            vec!["container.start".to_string(), "container.exec".to_string()],
183        );
184
185        // Act
186        let result = expectation.validate(&spans)?;
187
188        // Assert
189        assert!(result.passed);
190        assert_eq!(result.windows_checked, 2);
191        Ok(())
192    }
193
194    #[test]
195    fn test_window_expectation_not_contained() -> Result<()> {
196        // Arrange
197        let spans = vec![
198            create_span_with_times("container.lifecycle", 1000, 3000),
199            create_span_with_times("container.start", 1100, 2000),
200            create_span_with_times("container.exec", 3100, 4000), // Outside window!
201        ];
202        let expectation = WindowExpectation::new(
203            "container.lifecycle",
204            vec!["container.start".to_string(), "container.exec".to_string()],
205        );
206
207        // Act
208        let result = expectation.validate(&spans)?;
209
210        // Assert
211        assert!(!result.passed);
212        assert!(!result.errors.is_empty());
213        Ok(())
214    }
215
216    #[test]
217    fn test_window_expectation_outer_not_found() -> Result<()> {
218        // Arrange
219        let spans = vec![create_span_with_times("container.start", 1100, 2000)];
220        let expectation = WindowExpectation::new(
221            "container.lifecycle",
222            vec!["container.start".to_string()],
223        );
224
225        // Act
226        let result = expectation.validate(&spans)?;
227
228        // Assert
229        assert!(!result.passed);
230        assert!(!result.errors.is_empty());
231        Ok(())
232    }
233
234    #[test]
235    fn test_window_validator_is_contained() {
236        // Arrange
237        let outer = create_span_with_times("outer", 1000, 5000);
238        let inner = create_span_with_times("inner", 2000, 3000);
239        let spans = vec![outer.clone(), inner.clone()];
240        let validator = WindowValidator::new(&spans);
241
242        // Act
243        let is_contained = validator.is_temporally_contained(&inner, &outer);
244
245        // Assert
246        assert!(is_contained);
247    }
248
249    #[test]
250    fn test_window_validator_not_contained() {
251        // Arrange
252        let outer = create_span_with_times("outer", 1000, 3000);
253        let inner = create_span_with_times("inner", 2000, 4000); // Ends after outer
254        let spans = vec![outer.clone(), inner.clone()];
255        let validator = WindowValidator::new(&spans);
256
257        // Act
258        let is_contained = validator.is_temporally_contained(&inner, &outer);
259
260        // Assert
261        assert!(!is_contained);
262    }
263}