clnrm_core/otel/validators/
status.rs1use crate::error::{CleanroomError, Result};
7use crate::validation::span_validator::SpanData;
8use glob::Pattern;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "UPPERCASE")]
15pub enum StatusCode {
16 Unset,
18 Ok,
20 Error,
22}
23
24impl StatusCode {
25 pub fn parse(s: &str) -> Result<Self> {
27 match s.to_uppercase().as_str() {
28 "UNSET" => Ok(StatusCode::Unset),
29 "OK" => Ok(StatusCode::Ok),
30 "ERROR" => Ok(StatusCode::Error),
31 _ => Err(CleanroomError::validation_error(format!(
32 "Invalid status code '{}'. Must be UNSET, OK, or ERROR",
33 s
34 ))),
35 }
36 }
37
38 pub fn as_str(&self) -> &'static str {
40 match self {
41 StatusCode::Unset => "UNSET",
42 StatusCode::Ok => "OK",
43 StatusCode::Error => "ERROR",
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ValidationResult {
51 pub passed: bool,
53 pub errors: Vec<String>,
55 pub spans_checked: usize,
57}
58
59impl ValidationResult {
60 pub fn pass(spans_checked: usize) -> Self {
62 Self {
63 passed: true,
64 errors: Vec::new(),
65 spans_checked,
66 }
67 }
68
69 pub fn add_error(&mut self, error: String) {
71 self.passed = false;
72 self.errors.push(error);
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct StatusExpectation {
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub all: Option<StatusCode>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub by_name: Option<HashMap<String, StatusCode>>,
86}
87
88impl StatusExpectation {
89 pub fn new() -> Self {
91 Self {
92 all: None,
93 by_name: None,
94 }
95 }
96
97 pub fn with_all(mut self, status: StatusCode) -> Self {
99 self.all = Some(status);
100 self
101 }
102
103 pub fn with_name_pattern(mut self, pattern: String, status: StatusCode) -> Self {
105 self.by_name.get_or_insert_with(HashMap::new).insert(pattern, status);
106 self
107 }
108
109 pub fn validate(&self, spans: &[SpanData]) -> Result<ValidationResult> {
122 let mut result = ValidationResult::pass(0);
123
124 if let Some(expected_all) = self.all {
126 for span in spans {
127 result.spans_checked += 1;
128 let actual = self.get_span_status(span)?;
129 if actual != expected_all {
130 result.add_error(format!(
131 "Status validation failed: span '{}' has status {} but expected {} (fake-green: incorrect status)",
132 span.name, actual.as_str(), expected_all.as_str()
133 ));
134 }
135 }
136 }
137
138 if let Some(ref patterns) = self.by_name {
140 for (pattern, expected_status) in patterns {
141 let glob_pattern = Pattern::new(pattern).map_err(|e| {
142 CleanroomError::validation_error(format!(
143 "Invalid glob pattern '{}': {}",
144 pattern, e
145 ))
146 })?;
147
148 let matching_spans: Vec<_> = spans
150 .iter()
151 .filter(|s| glob_pattern.matches(&s.name))
152 .collect();
153
154 if matching_spans.is_empty() {
155 result.add_error(format!(
156 "Status validation failed: no spans match pattern '{}' (fake-green: spans never created?)",
157 pattern
158 ));
159 continue;
160 }
161
162 for span in matching_spans {
164 result.spans_checked += 1;
165 let actual = self.get_span_status(span)?;
166 if actual != *expected_status {
167 result.add_error(format!(
168 "Status validation failed: span '{}' has status {} but pattern '{}' expects {} (fake-green: incorrect status)",
169 span.name, actual.as_str(), pattern, expected_status.as_str()
170 ));
171 }
172 }
173 }
174 }
175
176 Ok(result)
177 }
178
179 fn get_span_status(&self, span: &SpanData) -> Result<StatusCode> {
181 if let Some(status_value) = span.attributes.get("otel.status_code") {
183 if let Some(status_str) = status_value.as_str() {
184 return StatusCode::parse(status_str);
185 }
186 }
187
188 Ok(StatusCode::Unset)
190 }
191}
192
193impl Default for StatusExpectation {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use std::collections::HashMap;
203
204 fn create_span(name: &str, status: Option<StatusCode>) -> SpanData {
205 let mut attributes = HashMap::new();
206 if let Some(s) = status {
207 attributes.insert("otel.status_code".to_string(), serde_json::Value::String(s.as_str().to_string()));
208 }
209
210 SpanData {
211 name: name.to_string(),
212 span_id: format!("span_{}", name),
213 trace_id: "trace123".to_string(),
214 parent_span_id: None,
215 attributes,
216 start_time_unix_nano: Some(1000000000),
217 end_time_unix_nano: Some(1100000000),
218 kind: None,
219 events: None,
220 resource_attributes: HashMap::new(),
221 }
222 }
223
224 #[test]
225 fn test_status_code_parse() -> Result<()> {
226 assert_eq!(StatusCode::parse("UNSET")?, StatusCode::Unset);
228 assert_eq!(StatusCode::parse("unset")?, StatusCode::Unset);
229 assert_eq!(StatusCode::parse("OK")?, StatusCode::Ok);
230 assert_eq!(StatusCode::parse("ok")?, StatusCode::Ok);
231 assert_eq!(StatusCode::parse("ERROR")?, StatusCode::Error);
232 assert_eq!(StatusCode::parse("error")?, StatusCode::Error);
233 assert!(StatusCode::parse("INVALID").is_err());
234 Ok(())
235 }
236
237 #[test]
238 fn test_status_expectation_all() -> Result<()> {
239 let spans = vec![
241 create_span("span1", Some(StatusCode::Ok)),
242 create_span("span2", Some(StatusCode::Ok)),
243 ];
244 let expectation = StatusExpectation::new().with_all(StatusCode::Ok);
245
246 let result = expectation.validate(&spans)?;
248
249 assert!(result.passed);
251 assert_eq!(result.spans_checked, 2);
252 Ok(())
253 }
254
255 #[test]
256 fn test_status_expectation_all_failure() -> Result<()> {
257 let spans = vec![
259 create_span("span1", Some(StatusCode::Ok)),
260 create_span("span2", Some(StatusCode::Error)), ];
262 let expectation = StatusExpectation::new().with_all(StatusCode::Ok);
263
264 let result = expectation.validate(&spans)?;
266
267 assert!(!result.passed);
269 assert!(!result.errors.is_empty());
270 Ok(())
271 }
272
273 #[test]
274 fn test_status_expectation_by_name() -> Result<()> {
275 let spans = vec![
277 create_span("container.start", Some(StatusCode::Ok)),
278 create_span("container.exec", Some(StatusCode::Ok)),
279 create_span("test.failed", Some(StatusCode::Error)),
280 ];
281 let expectation = StatusExpectation::new()
282 .with_name_pattern("container.*".to_string(), StatusCode::Ok)
283 .with_name_pattern("test.*".to_string(), StatusCode::Error);
284
285 let result = expectation.validate(&spans)?;
287
288 assert!(result.passed);
290 Ok(())
291 }
292
293 #[test]
294 fn test_status_expectation_by_name_failure() -> Result<()> {
295 let spans = vec![
297 create_span("container.start", Some(StatusCode::Error)), ];
299 let expectation = StatusExpectation::new()
300 .with_name_pattern("container.*".to_string(), StatusCode::Ok);
301
302 let result = expectation.validate(&spans)?;
304
305 assert!(!result.passed);
307 assert!(!result.errors.is_empty());
308 Ok(())
309 }
310
311 #[test]
312 fn test_status_expectation_default_unset() -> Result<()> {
313 let spans = vec![create_span("span1", None)]; let expectation = StatusExpectation::new().with_all(StatusCode::Unset);
316
317 let result = expectation.validate(&spans)?;
319
320 assert!(result.passed);
322 Ok(())
323 }
324}