1use crate::error::{CleanroomError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum SpanKind {
16 Internal,
18 Server,
20 Client,
22 Producer,
24 Consumer,
26}
27
28impl SpanKind {
29 pub fn parse_kind(s: &str) -> Result<Self> {
31 match s.to_lowercase().as_str() {
32 "internal" => Ok(SpanKind::Internal),
33 "server" => Ok(SpanKind::Server),
34 "client" => Ok(SpanKind::Client),
35 "producer" => Ok(SpanKind::Producer),
36 "consumer" => Ok(SpanKind::Consumer),
37 _ => Err(CleanroomError::validation_error(format!(
38 "Invalid span kind: '{}'. Must be one of: internal, server, client, producer, consumer",
39 s
40 ))),
41 }
42 }
43
44 pub fn to_otel_int(&self) -> i32 {
46 match self {
47 SpanKind::Internal => 1,
48 SpanKind::Server => 2,
49 SpanKind::Client => 3,
50 SpanKind::Producer => 4,
51 SpanKind::Consumer => 5,
52 }
53 }
54
55 pub fn from_otel_int(i: i32) -> Result<Self> {
57 match i {
58 1 => Ok(SpanKind::Internal),
59 2 => Ok(SpanKind::Server),
60 3 => Ok(SpanKind::Client),
61 4 => Ok(SpanKind::Producer),
62 5 => Ok(SpanKind::Consumer),
63 _ => Err(CleanroomError::validation_error(format!(
64 "Invalid OTEL span kind integer: {}",
65 i
66 ))),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SpanData {
74 pub name: String,
76 pub attributes: HashMap<String, serde_json::Value>,
78 pub trace_id: String,
80 pub span_id: String,
82 pub parent_span_id: Option<String>,
84 pub start_time_unix_nano: Option<u64>,
86 pub end_time_unix_nano: Option<u64>,
88 pub kind: Option<SpanKind>,
90 pub events: Option<Vec<String>>,
92 #[serde(default)]
94 pub resource_attributes: HashMap<String, serde_json::Value>,
95}
96
97impl SpanData {
98 pub fn duration_ms(&self) -> Option<f64> {
100 match (self.start_time_unix_nano, self.end_time_unix_nano) {
101 (Some(start), Some(end)) => {
102 if end >= start {
103 Some((end - start) as f64 / 1_000_000.0)
104 } else {
105 None
106 }
107 }
108 _ => None,
109 }
110 }
111}
112
113#[derive(Debug, Clone)]
115pub enum SpanAssertion {
116 SpanExists { name: String },
118 SpanCount { name: String, count: usize },
120 SpanAttribute {
122 name: String,
123 attribute_key: String,
124 attribute_value: String,
125 },
126 SpanHierarchy { parent: String, child: String },
128
129 SpanKind { name: String, kind: SpanKind },
132
133 SpanAllAttributes {
136 name: String,
137 attributes: HashMap<String, String>,
138 },
139
140 SpanAnyAttributes {
144 name: String,
145 attribute_patterns: Vec<String>,
146 },
147
148 SpanEvents { name: String, events: Vec<String> },
151
152 SpanDuration {
155 name: String,
156 min_ms: Option<u64>,
157 max_ms: Option<u64>,
158 },
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct FailureDetails {
164 pub rule: String,
166 pub span_name: String,
168 pub expected: String,
170 pub actual: Option<String>,
172 pub message: String,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ValidationResult {
179 pub passed: bool,
181 pub failures: Vec<FailureDetails>,
183 pub validations_count: usize,
185}
186
187impl ValidationResult {
188 pub fn success(validations_count: usize) -> Self {
190 Self {
191 passed: true,
192 failures: Vec::new(),
193 validations_count,
194 }
195 }
196
197 pub fn failure(failure: FailureDetails) -> Self {
199 Self {
200 passed: false,
201 failures: vec![failure],
202 validations_count: 1,
203 }
204 }
205
206 pub fn add_failure(&mut self, failure: FailureDetails) {
208 self.passed = false;
209 self.failures.push(failure);
210 }
211
212 pub fn merge(results: Vec<ValidationResult>) -> Self {
214 let passed = results.iter().all(|r| r.passed);
215 let failures: Vec<FailureDetails> =
216 results.iter().flat_map(|r| r.failures.clone()).collect();
217 let validations_count: usize = results.iter().map(|r| r.validations_count).sum();
218
219 Self {
220 passed,
221 failures,
222 validations_count,
223 }
224 }
225}
226
227pub struct SpanValidator {
229 pub(crate) spans: Vec<SpanData>,
231}
232
233impl SpanValidator {
234 pub fn from_span_data(spans: &[opentelemetry_sdk::trace::SpanData]) -> Result<Self> {
247 let converted_spans: Vec<SpanData> = spans.iter().map(Self::convert_otel_span).collect();
248
249 Ok(Self {
250 spans: converted_spans,
251 })
252 }
253
254 fn convert_otel_span(span: &opentelemetry_sdk::trace::SpanData) -> SpanData {
256 let mut attributes = std::collections::HashMap::new();
258 for kv in &span.attributes {
259 let key = kv.key.to_string();
260 let value = match &kv.value {
261 opentelemetry::Value::Bool(b) => serde_json::json!(b),
262 opentelemetry::Value::I64(i) => serde_json::json!(i),
263 opentelemetry::Value::F64(f) => serde_json::json!(f),
264 opentelemetry::Value::String(s) => serde_json::json!(s.to_string()),
265 _ => serde_json::json!(kv.value.to_string()),
266 };
267 attributes.insert(key, value);
268 }
269
270 let kind = match span.span_kind {
272 opentelemetry::trace::SpanKind::Internal => Some(SpanKind::Internal),
273 opentelemetry::trace::SpanKind::Server => Some(SpanKind::Server),
274 opentelemetry::trace::SpanKind::Client => Some(SpanKind::Client),
275 opentelemetry::trace::SpanKind::Producer => Some(SpanKind::Producer),
276 opentelemetry::trace::SpanKind::Consumer => Some(SpanKind::Consumer),
277 };
278
279 let events = if span.events.is_empty() {
281 None
282 } else {
283 Some(span.events.iter().map(|e| e.name.to_string()).collect())
284 };
285
286 let parent_span_id = if span.parent_span_id != opentelemetry::trace::SpanId::INVALID {
288 Some(format!("{:x}", span.parent_span_id))
289 } else {
290 None
291 };
292
293 let start_time_unix_nano = span
295 .start_time
296 .duration_since(std::time::SystemTime::UNIX_EPOCH)
297 .ok()
298 .map(|d| d.as_nanos() as u64);
299
300 let end_time_unix_nano = span
301 .end_time
302 .duration_since(std::time::SystemTime::UNIX_EPOCH)
303 .ok()
304 .map(|d| d.as_nanos() as u64);
305
306 SpanData {
307 name: span.name.to_string(),
308 attributes,
309 trace_id: format!("{:x}", span.span_context.trace_id()),
310 span_id: format!("{:x}", span.span_context.span_id()),
311 parent_span_id,
312 start_time_unix_nano,
313 end_time_unix_nano,
314 kind,
315 events,
316 resource_attributes: std::collections::HashMap::new(),
317 }
318 }
319
320 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
334 let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
335 CleanroomError::config_error(format!("Failed to read spans file: {}", e))
336 })?;
337
338 Self::from_json(&content)
339 }
340
341 pub fn from_json(json: &str) -> Result<Self> {
352 let mut all_spans = Vec::new();
355
356 for line in json.lines() {
357 let line = line.trim();
358 if line.is_empty() {
359 continue;
360 }
361
362 if let Ok(spans) = serde_json::from_str::<Vec<SpanData>>(line) {
364 all_spans.extend(spans);
365 } else if let Ok(span) = serde_json::from_str::<SpanData>(line) {
366 all_spans.push(span);
368 } else {
369 if let Ok(value) = serde_json::from_str::<serde_json::Value>(line) {
371 if let Some(spans) = Self::extract_spans_from_otel_format(&value) {
372 all_spans.extend(spans);
373 }
374 }
375 }
376 }
377
378 Ok(Self { spans: all_spans })
379 }
380
381 fn extract_spans_from_otel_format(value: &serde_json::Value) -> Option<Vec<SpanData>> {
383 let mut spans = Vec::new();
384
385 if let Some(resource_spans) = value.get("resourceSpans").and_then(|v| v.as_array()) {
387 for resource_span in resource_spans {
388 if let Some(scope_spans) =
389 resource_span.get("scopeSpans").and_then(|v| v.as_array())
390 {
391 for scope_span in scope_spans {
392 if let Some(span_array) = scope_span.get("spans").and_then(|v| v.as_array())
393 {
394 for span_obj in span_array {
395 if let Some(span) = Self::parse_otel_span(span_obj) {
396 spans.push(span);
397 }
398 }
399 }
400 }
401 }
402 }
403 }
404
405 if spans.is_empty() {
406 None
407 } else {
408 Some(spans)
409 }
410 }
411
412 fn parse_otel_span(span_obj: &serde_json::Value) -> Option<SpanData> {
414 let name = span_obj.get("name")?.as_str()?.to_string();
415 let trace_id = span_obj.get("traceId")?.as_str()?.to_string();
416 let span_id = span_obj.get("spanId")?.as_str()?.to_string();
417 let parent_span_id = span_obj
418 .get("parentSpanId")
419 .and_then(|v| v.as_str())
420 .map(|s| s.to_string());
421
422 let start_time_unix_nano = span_obj
423 .get("startTimeUnixNano")
424 .and_then(|v| v.as_str())
425 .and_then(|s| s.parse::<u64>().ok());
426
427 let end_time_unix_nano = span_obj
428 .get("endTimeUnixNano")
429 .and_then(|v| v.as_str())
430 .and_then(|s| s.parse::<u64>().ok());
431
432 let kind = span_obj
434 .get("kind")
435 .and_then(|v| v.as_i64())
436 .and_then(|i| SpanKind::from_otel_int(i as i32).ok());
437
438 let mut attributes = HashMap::new();
440 if let Some(attrs) = span_obj.get("attributes").and_then(|v| v.as_array()) {
441 for attr in attrs {
442 if let (Some(key), Some(value)) =
443 (attr.get("key").and_then(|k| k.as_str()), attr.get("value"))
444 {
445 attributes.insert(key.to_string(), value.clone());
446 }
447 }
448 }
449
450 let events = span_obj
452 .get("events")
453 .and_then(|v| v.as_array())
454 .map(|events_array| {
455 events_array
456 .iter()
457 .filter_map(|event| {
458 event.get("name").and_then(|n| n.as_str()).map(String::from)
459 })
460 .collect()
461 });
462
463 Some(SpanData {
464 name,
465 attributes,
466 trace_id,
467 span_id,
468 parent_span_id,
469 start_time_unix_nano,
470 end_time_unix_nano,
471 kind,
472 events,
473 resource_attributes: HashMap::new(),
474 })
475 }
476
477 pub fn spans(&self) -> &[SpanData] {
479 &self.spans
480 }
481
482 pub fn find_spans_by_name(&self, name: &str) -> Vec<&SpanData> {
484 self.spans.iter().filter(|s| s.name == name).collect()
485 }
486
487 pub fn find_span(&self, trace_id: &str, span_id: &str) -> Option<&SpanData> {
489 self.spans
490 .iter()
491 .find(|s| s.trace_id == trace_id && s.span_id == span_id)
492 }
493
494 pub fn has_span(&self, name: &str) -> bool {
496 self.spans.iter().any(|s| s.name == name)
497 }
498
499 pub fn count_spans(&self, name: &str) -> usize {
501 self.spans.iter().filter(|s| s.name == name).count()
502 }
503
504 pub fn validate_expectations(
515 &self,
516 expectations: &[crate::config::SpanExpectationConfig],
517 ) -> Result<ValidationResult> {
518 let mut results = Vec::new();
519
520 for expectation in expectations {
521 let result = self.validate_single_expectation(expectation)?;
522 results.push(result);
523 }
524
525 Ok(ValidationResult::merge(results))
526 }
527
528 fn validate_single_expectation(
530 &self,
531 expectation: &crate::config::SpanExpectationConfig,
532 ) -> Result<ValidationResult> {
533 let span_name = &expectation.name;
534
535 let matching_spans = self.find_spans_by_name(span_name);
537 if matching_spans.is_empty() {
538 return Ok(ValidationResult::failure(FailureDetails {
539 rule: format!("expect.span[{}].existence", span_name),
540 span_name: span_name.clone(),
541 expected: format!("Span '{}' to exist", span_name),
542 actual: None,
543 message: format!("Span '{}' not found in trace", span_name),
544 }));
545 }
546
547 let mut validation_count = 1; let mut failures = Vec::new();
549
550 let span = matching_spans[0];
553
554 if let Some(ref parent_name) = expectation.parent {
556 validation_count += 1;
557 if let Some(failure) = self.validate_parent_relationship(span, parent_name, span_name) {
558 failures.push(failure);
559 }
560 }
561
562 if let Some(ref kind_str) = expectation.kind {
564 validation_count += 1;
565 if let Some(failure) = self.validate_span_kind(span, kind_str, span_name)? {
566 failures.push(failure);
567 }
568 }
569
570 if let Some(ref attrs_config) = expectation.attrs {
572 if let Some(ref all_attrs) = attrs_config.all {
574 validation_count += all_attrs.len();
575 if let Some(failure) = self.validate_attrs_all(span, all_attrs, span_name) {
576 failures.push(failure);
577 }
578 }
579
580 if let Some(ref any_attrs) = attrs_config.any {
582 validation_count += 1;
583 if let Some(failure) = self.validate_attrs_any(span, any_attrs, span_name) {
584 failures.push(failure);
585 }
586 }
587 }
588
589 if let Some(ref events_config) = expectation.events {
591 if let Some(ref any_events) = events_config.any {
592 validation_count += 1;
593 if let Some(failure) = self.validate_events_any(span, any_events, span_name) {
594 failures.push(failure);
595 }
596 }
597
598 if let Some(ref all_events) = events_config.all {
599 validation_count += all_events.len();
600 if let Some(failure) = self.validate_events_all(span, all_events, span_name) {
601 failures.push(failure);
602 }
603 }
604 }
605
606 if let Some(ref duration_config) = expectation.duration_ms {
608 validation_count += 1;
609 if let Some(failure) = self.validate_duration(span, duration_config, span_name) {
610 failures.push(failure);
611 }
612 }
613
614 if failures.is_empty() {
615 Ok(ValidationResult::success(validation_count))
616 } else {
617 Ok(ValidationResult {
618 passed: false,
619 failures,
620 validations_count: validation_count,
621 })
622 }
623 }
624
625 fn validate_parent_relationship(
627 &self,
628 span: &SpanData,
629 parent_name: &str,
630 span_name: &str,
631 ) -> Option<FailureDetails> {
632 let parent_spans = self.find_spans_by_name(parent_name);
634 if parent_spans.is_empty() {
635 return Some(FailureDetails {
636 rule: format!("expect.span[{}].parent", span_name),
637 span_name: span_name.to_string(),
638 expected: format!("Parent span '{}'", parent_name),
639 actual: None,
640 message: format!(
641 "Parent span '{}' not found for span '{}'",
642 parent_name, span_name
643 ),
644 });
645 }
646
647 if let Some(ref parent_id) = span.parent_span_id {
649 if parent_spans.iter().any(|p| &p.span_id == parent_id) {
650 return None; }
652
653 Some(FailureDetails {
655 rule: format!("expect.span[{}].parent", span_name),
656 span_name: span_name.to_string(),
657 expected: format!("Parent span '{}'", parent_name),
658 actual: Some(format!("Different parent (ID: {})", parent_id)),
659 message: format!(
660 "Span '{}' parent mismatch: expected '{}', found different parent",
661 span_name, parent_name
662 ),
663 })
664 } else {
665 Some(FailureDetails {
667 rule: format!("expect.span[{}].parent", span_name),
668 span_name: span_name.to_string(),
669 expected: format!("Parent span '{}'", parent_name),
670 actual: Some("none".to_string()),
671 message: format!(
672 "Span '{}' parent mismatch: expected '{}', found none",
673 span_name, parent_name
674 ),
675 })
676 }
677 }
678
679 fn validate_span_kind(
681 &self,
682 span: &SpanData,
683 kind_str: &str,
684 span_name: &str,
685 ) -> Result<Option<FailureDetails>> {
686 let expected_kind = SpanKind::parse_kind(kind_str)?;
687
688 match span.kind {
689 Some(actual_kind) if actual_kind == expected_kind => Ok(None),
690 Some(actual_kind) => Ok(Some(FailureDetails {
691 rule: format!("expect.span[{}].kind", span_name),
692 span_name: span_name.to_string(),
693 expected: format!("{:?}", expected_kind),
694 actual: Some(format!("{:?}", actual_kind)),
695 message: format!(
696 "Span '{}' kind mismatch: expected {:?}, found {:?}",
697 span_name, expected_kind, actual_kind
698 ),
699 })),
700 None => Ok(Some(FailureDetails {
701 rule: format!("expect.span[{}].kind", span_name),
702 span_name: span_name.to_string(),
703 expected: format!("{:?}", expected_kind),
704 actual: None,
705 message: format!(
706 "Span '{}' kind mismatch: expected {:?}, found none",
707 span_name, expected_kind
708 ),
709 })),
710 }
711 }
712
713 fn validate_attrs_all(
715 &self,
716 span: &SpanData,
717 all_attrs: &HashMap<String, String>,
718 span_name: &str,
719 ) -> Option<FailureDetails> {
720 let mut missing = Vec::new();
721
722 for (key, expected_value) in all_attrs {
723 let matches = span
724 .attributes
725 .get(key)
726 .and_then(|v| v.as_str())
727 .map(|v| v == expected_value)
728 .unwrap_or(false);
729
730 if !matches {
731 let actual = span
732 .attributes
733 .get(key)
734 .and_then(|v| v.as_str())
735 .map(|s| s.to_string());
736
737 if actual.is_none() {
738 missing.push(format!("{}={}", key, expected_value));
739 } else {
740 missing.push(format!(
741 "{}={} (found: {})",
742 key,
743 expected_value,
744 actual.unwrap_or_default()
745 ));
746 }
747 }
748 }
749
750 if missing.is_empty() {
751 None
752 } else {
753 Some(FailureDetails {
754 rule: format!("expect.span[{}].attrs.all", span_name),
755 span_name: span_name.to_string(),
756 expected: format!("All attributes: {:?}", all_attrs),
757 actual: Some(format!("Missing/incorrect: [{}]", missing.join(", "))),
758 message: format!(
759 "Span '{}' missing required attributes: [{}]",
760 span_name,
761 missing.join(", ")
762 ),
763 })
764 }
765 }
766
767 fn validate_attrs_any(
769 &self,
770 span: &SpanData,
771 any_attrs: &HashMap<String, String>,
772 span_name: &str,
773 ) -> Option<FailureDetails> {
774 let has_any = any_attrs.iter().any(|(key, expected_value)| {
775 span.attributes
776 .get(key)
777 .and_then(|v| v.as_str())
778 .map(|v| v == expected_value)
779 .unwrap_or(false)
780 });
781
782 if has_any {
783 None
784 } else {
785 let patterns: Vec<String> = any_attrs
786 .iter()
787 .map(|(k, v)| format!("{}={}", k, v))
788 .collect();
789
790 Some(FailureDetails {
791 rule: format!("expect.span[{}].attrs.any", span_name),
792 span_name: span_name.to_string(),
793 expected: format!("Any of: [{}]", patterns.join(", ")),
794 actual: None,
795 message: format!(
796 "Span '{}' missing any of required attributes: [{}]",
797 span_name,
798 patterns.join(", ")
799 ),
800 })
801 }
802 }
803
804 fn validate_events_any(
806 &self,
807 span: &SpanData,
808 any_events: &[String],
809 span_name: &str,
810 ) -> Option<FailureDetails> {
811 if let Some(ref span_events) = span.events {
812 let has_any = any_events.iter().any(|event| span_events.contains(event));
813
814 if has_any {
815 return None;
816 }
817 }
818
819 Some(FailureDetails {
820 rule: format!("expect.span[{}].events.any", span_name),
821 span_name: span_name.to_string(),
822 expected: format!("Any of: [{}]", any_events.join(", ")),
823 actual: span.events.as_ref().map(|events| format!("{:?}", events)),
824 message: format!(
825 "Span '{}' missing required events: [{}]",
826 span_name,
827 any_events.join(", ")
828 ),
829 })
830 }
831
832 fn validate_events_all(
834 &self,
835 span: &SpanData,
836 all_events: &[String],
837 span_name: &str,
838 ) -> Option<FailureDetails> {
839 if let Some(ref span_events) = span.events {
840 let missing: Vec<&String> = all_events
841 .iter()
842 .filter(|event| !span_events.contains(event))
843 .collect();
844
845 if missing.is_empty() {
846 return None;
847 }
848
849 return Some(FailureDetails {
850 rule: format!("expect.span[{}].events.all", span_name),
851 span_name: span_name.to_string(),
852 expected: format!("All of: [{}]", all_events.join(", ")),
853 actual: Some(format!("Missing: {:?}", missing)),
854 message: format!(
855 "Span '{}' missing required events: {:?}",
856 span_name, missing
857 ),
858 });
859 }
860
861 Some(FailureDetails {
862 rule: format!("expect.span[{}].events.all", span_name),
863 span_name: span_name.to_string(),
864 expected: format!("All of: [{}]", all_events.join(", ")),
865 actual: None,
866 message: format!("Span '{}' has no events", span_name),
867 })
868 }
869
870 fn validate_duration(
872 &self,
873 span: &SpanData,
874 duration_config: &crate::config::DurationBoundConfig,
875 span_name: &str,
876 ) -> Option<FailureDetails> {
877 let duration_ms = span.duration_ms()?;
878
879 if let Some(min) = duration_config.min {
881 if duration_ms < min {
882 return Some(FailureDetails {
883 rule: format!("expect.span[{}].duration_ms.min", span_name),
884 span_name: span_name.to_string(),
885 expected: format!("duration >= {}ms", min),
886 actual: Some(format!("{}ms", duration_ms)),
887 message: format!(
888 "Span '{}' duration {}ms < min {}ms",
889 span_name, duration_ms, min
890 ),
891 });
892 }
893 }
894
895 if let Some(max) = duration_config.max {
897 if duration_ms > max {
898 return Some(FailureDetails {
899 rule: format!("expect.span[{}].duration_ms.max", span_name),
900 span_name: span_name.to_string(),
901 expected: format!("duration <= {}ms", max),
902 actual: Some(format!("{}ms", duration_ms)),
903 message: format!(
904 "Span '{}' duration {}ms > max {}ms",
905 span_name, duration_ms, max
906 ),
907 });
908 }
909 }
910
911 None
912 }
913
914 pub fn first_failure(result: &ValidationResult) -> Option<&FailureDetails> {
916 result.failures.first()
917 }
918
919 pub fn validate_assertion(&self, assertion: &SpanAssertion) -> Result<()> {
921 match assertion {
922 SpanAssertion::SpanExists { name } => {
923 if !self.has_span(name) {
924 return Err(CleanroomError::validation_error(format!(
925 "Span assertion failed: span '{}' does not exist",
926 name
927 )));
928 }
929 Ok(())
930 }
931 SpanAssertion::SpanCount { name, count } => {
932 let actual_count = self.count_spans(name);
933 if actual_count != *count {
934 return Err(CleanroomError::validation_error(format!(
935 "Span count assertion failed: expected {} spans named '{}', found {}",
936 count, name, actual_count
937 )));
938 }
939 Ok(())
940 }
941 SpanAssertion::SpanAttribute {
942 name,
943 attribute_key,
944 attribute_value,
945 } => {
946 let spans = self.find_spans_by_name(name);
947 if spans.is_empty() {
948 return Err(CleanroomError::validation_error(format!(
949 "Span attribute assertion failed: span '{}' does not exist",
950 name
951 )));
952 }
953
954 let has_attribute = spans.iter().any(|span| {
956 span.attributes
958 .get(attribute_key)
959 .and_then(|v| v.as_str())
960 .map(|v| v == attribute_value)
961 .unwrap_or(false)
962 });
963
964 if !has_attribute {
965 return Err(CleanroomError::validation_error(format!(
966 "Span attribute assertion failed: no span '{}' has attribute '{}' = '{}'",
967 name, attribute_key, attribute_value
968 )));
969 }
970 Ok(())
971 }
972 SpanAssertion::SpanHierarchy { parent, child } => {
973 let parent_spans = self.find_spans_by_name(parent);
974 let child_spans = self.find_spans_by_name(child);
975
976 if parent_spans.is_empty() {
977 return Err(CleanroomError::validation_error(format!(
978 "Span hierarchy assertion failed: parent span '{}' does not exist",
979 parent
980 )));
981 }
982
983 if child_spans.is_empty() {
984 return Err(CleanroomError::validation_error(format!(
985 "Span hierarchy assertion failed: child span '{}' does not exist",
986 child
987 )));
988 }
989
990 let has_hierarchy = child_spans.iter().any(|child_span| {
992 if let Some(parent_id) = &child_span.parent_span_id {
993 parent_spans.iter().any(|p| &p.span_id == parent_id)
994 } else {
995 false
996 }
997 });
998
999 if !has_hierarchy {
1000 return Err(CleanroomError::validation_error(format!(
1001 "Span hierarchy assertion failed: no '{}' span is a child of '{}' span",
1002 child, parent
1003 )));
1004 }
1005 Ok(())
1006 }
1007
1008 SpanAssertion::SpanKind { name, kind } => {
1010 let spans = self.find_spans_by_name(name);
1011 if spans.is_empty() {
1012 return Err(CleanroomError::validation_error(format!(
1013 "Span kind assertion failed: span '{}' does not exist",
1014 name
1015 )));
1016 }
1017
1018 let has_kind = spans
1021 .iter()
1022 .any(|span| span.kind.map(|k| k == *kind).unwrap_or(false));
1023
1024 if !has_kind {
1025 return Err(CleanroomError::validation_error(format!(
1026 "Span kind assertion failed: no span '{}' has kind '{:?}'",
1027 name, kind
1028 )));
1029 }
1030 Ok(())
1031 }
1032
1033 SpanAssertion::SpanAllAttributes { name, attributes } => {
1034 let spans = self.find_spans_by_name(name);
1035 if spans.is_empty() {
1036 return Err(CleanroomError::validation_error(format!(
1037 "Span all attributes assertion failed: span '{}' does not exist",
1038 name
1039 )));
1040 }
1041
1042 let has_all_attributes = spans.iter().any(|span| {
1044 attributes.iter().all(|(key, expected_value)| {
1045 span.attributes
1047 .get(key)
1048 .and_then(|v| v.as_str())
1049 .map(|v| v == expected_value)
1050 .unwrap_or(false)
1051 })
1052 });
1053
1054 if !has_all_attributes {
1055 let missing: Vec<String> = attributes
1056 .iter()
1057 .filter(|(key, expected_value)| {
1058 !spans.iter().any(|span| {
1059 span.attributes
1061 .get(*key)
1062 .and_then(|v| v.as_str())
1063 .map(|v| v == *expected_value)
1064 .unwrap_or(false)
1065 })
1066 })
1067 .map(|(k, v)| format!("{}={}", k, v))
1068 .collect();
1069
1070 return Err(CleanroomError::validation_error(format!(
1071 "Span all attributes assertion failed: span '{}' is missing attributes: [{}]",
1072 name,
1073 missing.join(", ")
1074 )));
1075 }
1076 Ok(())
1077 }
1078
1079 SpanAssertion::SpanAnyAttributes {
1080 name,
1081 attribute_patterns,
1082 } => {
1083 let spans = self.find_spans_by_name(name);
1084 if spans.is_empty() {
1085 return Err(CleanroomError::validation_error(format!(
1086 "Span any attributes assertion failed: span '{}' does not exist",
1087 name
1088 )));
1089 }
1090
1091 let has_any_match = spans.iter().any(|span| {
1093 attribute_patterns.iter().any(|pattern| {
1094 if let Some((key, value)) = pattern.split_once('=') {
1095 span.attributes
1097 .get(key)
1098 .and_then(|v| v.as_str())
1099 .map(|v| v == value)
1100 .unwrap_or(false)
1101 } else {
1102 false
1103 }
1104 })
1105 });
1106
1107 if !has_any_match {
1108 return Err(CleanroomError::validation_error(format!(
1109 "Span any attributes assertion failed: span '{}' does not have any of the patterns: [{}]",
1110 name,
1111 attribute_patterns.join(", ")
1112 )));
1113 }
1114 Ok(())
1115 }
1116
1117 SpanAssertion::SpanEvents { name, events } => {
1118 let spans = self.find_spans_by_name(name);
1119 if spans.is_empty() {
1120 return Err(CleanroomError::validation_error(format!(
1121 "Span events assertion failed: span '{}' does not exist",
1122 name
1123 )));
1124 }
1125
1126 let has_any_event = spans.iter().any(|span| {
1128 if let Some(span_events) = &span.events {
1129 events.iter().any(|event| span_events.contains(event))
1130 } else {
1131 false
1132 }
1133 });
1134
1135 if !has_any_event {
1136 return Err(CleanroomError::validation_error(format!(
1137 "Span events assertion failed: span '{}' does not have any of the events: [{}]",
1138 name,
1139 events.join(", ")
1140 )));
1141 }
1142 Ok(())
1143 }
1144
1145 SpanAssertion::SpanDuration {
1146 name,
1147 min_ms,
1148 max_ms,
1149 } => {
1150 let spans = self.find_spans_by_name(name);
1151 if spans.is_empty() {
1152 return Err(CleanroomError::validation_error(format!(
1153 "Span duration assertion failed: span '{}' does not exist",
1154 name
1155 )));
1156 }
1157
1158 let has_valid_duration = spans.iter().any(|span| {
1160 if let Some(duration) = span.duration_ms() {
1161 let duration_u64 = duration as u64;
1162
1163 let min_ok = min_ms.map(|min| duration_u64 >= min).unwrap_or(true);
1164 let max_ok = max_ms.map(|max| duration_u64 <= max).unwrap_or(true);
1165
1166 min_ok && max_ok
1167 } else {
1168 false
1169 }
1170 });
1171
1172 if !has_valid_duration {
1173 let bounds = match (min_ms, max_ms) {
1174 (Some(min), Some(max)) => format!("between {}ms and {}ms", min, max),
1175 (Some(min), None) => format!("at least {}ms", min),
1176 (None, Some(max)) => format!("at most {}ms", max),
1177 (None, None) => "any duration".to_string(),
1178 };
1179
1180 return Err(CleanroomError::validation_error(format!(
1181 "Span duration assertion failed: span '{}' does not have duration {}",
1182 name, bounds
1183 )));
1184 }
1185 Ok(())
1186 }
1187 }
1188 }
1189
1190 pub fn validate_assertions(&self, assertions: &[SpanAssertion]) -> Result<()> {
1192 for assertion in assertions {
1193 self.validate_assertion(assertion)?;
1194 }
1195 Ok(())
1196 }
1197
1198 pub fn get_span(&self, name: &str) -> Option<&SpanData> {
1200 self.spans.iter().find(|s| s.name == name)
1201 }
1202
1203 pub fn get_span_by_id(&self, span_id: &str) -> Option<&SpanData> {
1205 self.spans.iter().find(|s| s.span_id == span_id)
1206 }
1207
1208 pub fn all_spans(&self) -> &[SpanData] {
1210 &self.spans
1211 }
1212}