clnrm_core/telemetry/
validation_processor.rs

1//! Span processor for validation
2//!
3//! Custom OpenTelemetry span processor that stores spans in memory
4//! for runtime validation against test expectations.
5//!
6//! ## Architecture
7//!
8//! - **Dual Export**: Spans are both exported to OTLP/stdout AND stored in memory
9//! - **Non-Blocking**: Uses simple span processor (no batching) for immediate storage
10//! - **Zero Overhead**: No-op when span expectations not configured
11//!
12//! ## Integration
13//!
14//! Added to the OTEL tracer provider pipeline alongside batch span processor:
15//!
16//! ```no_run
17//! use clnrm_core::telemetry::validation_processor::ValidationSpanProcessor;
18//!
19//! let tracer_provider = TracerProvider::builder()
20//!     .with_span_processor(BatchSpanProcessor::builder(exporter, runtime).build())
21//!     .with_span_processor(ValidationSpanProcessor::new())
22//!     .build();
23//! ```
24
25use opentelemetry::Context;
26use opentelemetry_sdk::error::OTelSdkResult;
27use opentelemetry_sdk::trace::{SpanData, SpanProcessor};
28
29use super::span_storage;
30
31/// Span processor that stores spans for validation
32///
33/// Implements the OpenTelemetry `SpanProcessor` trait to intercept
34/// spans as they complete and store them in memory for validation.
35#[derive(Debug)]
36pub struct ValidationSpanProcessor;
37
38impl ValidationSpanProcessor {
39    /// Create a new validation span processor
40    pub fn new() -> Self {
41        Self
42    }
43}
44
45impl Default for ValidationSpanProcessor {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl SpanProcessor for ValidationSpanProcessor {
52    fn on_start(&self, _span: &mut opentelemetry_sdk::trace::Span, _cx: &Context) {
53        // No-op: We only care about completed spans
54    }
55
56    fn on_end(&self, span: SpanData) {
57        // Store span for validation
58        span_storage::store_span(span);
59    }
60
61    fn force_flush(&self) -> OTelSdkResult {
62        // No buffering, nothing to flush
63        Ok(())
64    }
65
66    fn shutdown(&self) -> OTelSdkResult {
67        // No resources to clean up
68        Ok(())
69    }
70
71    fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult {
72        // No resources to clean up, timeout not needed
73        Ok(())
74    }
75
76    fn set_resource(&mut self, _resource: &opentelemetry_sdk::Resource) {
77        // Resource not needed for validation
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use opentelemetry::trace::{SpanContext, SpanId, SpanKind, TraceFlags, TraceId, TraceState};
85    use opentelemetry_sdk::trace::{SpanEvents, SpanLinks};
86    use std::borrow::Cow;
87    use std::time::SystemTime;
88
89    fn create_test_span(name: &str) -> SpanData {
90        SpanData {
91            span_context: SpanContext::new(
92                TraceId::from_bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
93                SpanId::from_bytes([0, 0, 0, 0, 0, 0, 0, 1]),
94                TraceFlags::default(),
95                false,
96                TraceState::default(),
97            ),
98            parent_span_id: SpanId::INVALID,
99            parent_span_is_remote: false,
100            span_kind: SpanKind::Internal,
101            name: Cow::Owned(name.to_string()),
102            start_time: SystemTime::now(),
103            end_time: SystemTime::now(),
104            attributes: Vec::new(),
105            dropped_attributes_count: 0,
106            events: SpanEvents::default(),
107            links: SpanLinks::default(),
108            status: opentelemetry::trace::Status::Unset,
109            instrumentation_scope: Default::default(),
110        }
111    }
112
113    #[test]
114    fn test_validation_processor_stores_span() {
115        span_storage::clear_collected_spans();
116
117        let processor = ValidationSpanProcessor::new();
118        let span = create_test_span("test_span");
119
120        processor.on_end(span);
121
122        let stored_spans = span_storage::get_collected_spans();
123        assert_eq!(stored_spans.len(), 1);
124        assert_eq!(stored_spans[0].name, "test_span");
125    }
126
127    #[test]
128    fn test_validation_processor_force_flush() {
129        let processor = ValidationSpanProcessor::new();
130        assert!(processor.force_flush().is_ok());
131    }
132
133    #[test]
134    fn test_validation_processor_shutdown() {
135        let processor = ValidationSpanProcessor::new();
136        assert!(processor.shutdown().is_ok());
137    }
138}