clnrm_core/otel/validators/
window.rs1use crate::error::Result;
7use crate::validation::span_validator::SpanData;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ValidationResult {
13 pub passed: bool,
15 pub errors: Vec<String>,
17 pub windows_checked: usize,
19}
20
21impl ValidationResult {
22 pub fn pass(windows_checked: usize) -> Self {
24 Self {
25 passed: true,
26 errors: Vec::new(),
27 windows_checked,
28 }
29 }
30
31 pub fn add_error(&mut self, error: String) {
33 self.passed = false;
34 self.errors.push(error);
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct WindowExpectation {
41 pub outer: String,
43 pub contains: Vec<String>,
45}
46
47impl WindowExpectation {
48 pub fn new(outer: impl Into<String>, contains: Vec<String>) -> Self {
50 Self {
51 outer: outer.into(),
52 contains,
53 }
54 }
55
56 pub fn validate(&self, spans: &[SpanData]) -> Result<ValidationResult> {
69 let validator = WindowValidator::new(spans);
70 let mut result = ValidationResult::pass(0);
71
72 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 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 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
122pub struct WindowValidator<'a> {
124 _spans: &'a [SpanData],
126}
127
128impl<'a> WindowValidator<'a> {
129 pub fn new(spans: &'a [SpanData]) -> Self {
131 Self { _spans: spans }
132 }
133
134 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 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 let result = expectation.validate(&spans)?;
187
188 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 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), ];
202 let expectation = WindowExpectation::new(
203 "container.lifecycle",
204 vec!["container.start".to_string(), "container.exec".to_string()],
205 );
206
207 let result = expectation.validate(&spans)?;
209
210 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 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 let result = expectation.validate(&spans)?;
227
228 assert!(!result.passed);
230 assert!(!result.errors.is_empty());
231 Ok(())
232 }
233
234 #[test]
235 fn test_window_validator_is_contained() {
236 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 let is_contained = validator.is_temporally_contained(&inner, &outer);
244
245 assert!(is_contained);
247 }
248
249 #[test]
250 fn test_window_validator_not_contained() {
251 let outer = create_span_with_times("outer", 1000, 3000);
253 let inner = create_span_with_times("inner", 2000, 4000); let spans = vec![outer.clone(), inner.clone()];
255 let validator = WindowValidator::new(&spans);
256
257 let is_contained = validator.is_temporally_contained(&inner, &outer);
259
260 assert!(!is_contained);
262 }
263}