allframe_core/otel/
testing.rs

1//! Testing utilities for OpenTelemetry
2//!
3//! This module provides in-memory recorders for spans and metrics
4//! that are useful for testing instrumented code.
5
6use std::{
7    collections::HashMap,
8    sync::{Arc, RwLock},
9};
10
11/// Span represents a unit of work in distributed tracing
12#[derive(Debug, Clone)]
13pub struct Span {
14    /// Span ID
15    pub span_id: String,
16    /// Parent span ID (if any)
17    pub parent_span_id: Option<String>,
18    /// Trace ID
19    pub trace_id: String,
20    /// Span name
21    pub name: String,
22    /// Span attributes
23    pub attributes: HashMap<String, String>,
24    /// Span status
25    pub status: String,
26    /// Error message (if failed)
27    pub error_message: String,
28    /// Duration in milliseconds
29    pub duration_ms: f64,
30    /// Architecture layer (if applicable)
31    pub layer: String,
32}
33
34/// SpanRecorder for testing - records all spans
35#[derive(Clone, Default)]
36pub struct SpanRecorder {
37    spans: Arc<RwLock<Vec<Span>>>,
38}
39
40impl SpanRecorder {
41    /// Create a new span recorder
42    pub fn new() -> Self {
43        Self {
44            spans: Arc::new(RwLock::new(Vec::new())),
45        }
46    }
47
48    /// Record a span
49    pub fn record(&self, span: Span) {
50        let mut spans = self.spans.write().unwrap();
51        spans.push(span);
52    }
53
54    /// Get all recorded spans
55    pub fn spans(&self) -> Vec<Span> {
56        self.spans.read().unwrap().clone()
57    }
58
59    /// Clear all recorded spans
60    pub fn clear(&self) {
61        let mut spans = self.spans.write().unwrap();
62        spans.clear();
63    }
64}
65
66/// MetricsRecorder for testing - records all metrics
67#[derive(Clone, Default)]
68pub struct MetricsRecorder {
69    counters: Arc<RwLock<HashMap<String, u64>>>,
70    gauges: Arc<RwLock<HashMap<String, i64>>>,
71    histograms: Arc<RwLock<HashMap<String, Vec<f64>>>>,
72}
73
74impl MetricsRecorder {
75    /// Create a new metrics recorder
76    pub fn new() -> Self {
77        Self {
78            counters: Arc::new(RwLock::new(HashMap::new())),
79            gauges: Arc::new(RwLock::new(HashMap::new())),
80            histograms: Arc::new(RwLock::new(HashMap::new())),
81        }
82    }
83
84    /// Get current instance (placeholder)
85    pub fn current() -> Self {
86        Self::new()
87    }
88
89    /// Increment a counter
90    pub fn increment_counter(&self, name: &str, value: u64) {
91        let mut counters = self.counters.write().unwrap();
92        *counters.entry(name.to_string()).or_insert(0) += value;
93    }
94
95    /// Set a gauge value
96    pub fn set_gauge(&self, name: &str, value: i64) {
97        let mut gauges = self.gauges.write().unwrap();
98        gauges.insert(name.to_string(), value);
99    }
100
101    /// Record a histogram value
102    pub fn record_histogram(&self, name: &str, value: f64) {
103        let mut histograms = self.histograms.write().unwrap();
104        histograms
105            .entry(name.to_string())
106            .or_insert_with(Vec::new)
107            .push(value);
108    }
109
110    /// Get counter value
111    pub fn get_counter(&self, name: &str) -> u64 {
112        self.counters
113            .read()
114            .unwrap()
115            .get(name)
116            .copied()
117            .unwrap_or(0)
118    }
119
120    /// Get gauge value
121    pub fn get_gauge(&self, name: &str) -> i64 {
122        self.gauges.read().unwrap().get(name).copied().unwrap_or(0)
123    }
124
125    /// Get histogram
126    pub fn get_histogram(&self, name: &str) -> Histogram {
127        let values = self
128            .histograms
129            .read()
130            .unwrap()
131            .get(name)
132            .cloned()
133            .unwrap_or_default();
134        Histogram::new(values)
135    }
136
137    /// Get counter with labels (placeholder)
138    pub fn get_counter_with_labels(&self, name: &str, _labels: &[(&str, &str)]) -> u64 {
139        self.get_counter(name)
140    }
141}
142
143/// SpanContext for distributed tracing
144#[derive(Debug, Clone)]
145pub struct SpanContext {
146    /// Trace ID
147    pub trace_id: String,
148    /// Parent span ID
149    pub parent_span_id: String,
150    /// Sampled flag
151    pub sampled: bool,
152}
153
154impl SpanContext {
155    /// Create a new span context
156    pub fn new(trace_id: &str, parent_span_id: &str) -> Self {
157        Self {
158            trace_id: trace_id.to_string(),
159            parent_span_id: parent_span_id.to_string(),
160            sampled: true,
161        }
162    }
163}
164
165/// Histogram for latency measurements
166pub struct Histogram {
167    values: Vec<f64>,
168}
169
170impl Histogram {
171    /// Create new histogram
172    pub fn new(values: Vec<f64>) -> Self {
173        Self { values }
174    }
175
176    /// Get count of measurements
177    pub fn count(&self) -> usize {
178        self.values.len()
179    }
180
181    /// Get sum of all values
182    pub fn sum(&self) -> f64 {
183        self.values.iter().sum()
184    }
185
186    /// Get p50 percentile
187    pub fn p50(&self) -> f64 {
188        self.percentile(0.5)
189    }
190
191    /// Get p95 percentile
192    pub fn p95(&self) -> f64 {
193        self.percentile(0.95)
194    }
195
196    /// Get p99 percentile
197    pub fn p99(&self) -> f64 {
198        self.percentile(0.99)
199    }
200
201    fn percentile(&self, p: f64) -> f64 {
202        if self.values.is_empty() {
203            return 0.0;
204        }
205        let mut sorted = self.values.clone();
206        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
207        let index = ((self.values.len() as f64 - 1.0) * p) as usize;
208        sorted[index]
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_span_recorder() {
218        let recorder = SpanRecorder::new();
219
220        let span = Span {
221            span_id: "span-1".to_string(),
222            parent_span_id: None,
223            trace_id: "trace-1".to_string(),
224            name: "test".to_string(),
225            attributes: HashMap::new(),
226            status: "ok".to_string(),
227            error_message: String::new(),
228            duration_ms: 100.0,
229            layer: String::new(),
230        };
231
232        recorder.record(span.clone());
233        let spans = recorder.spans();
234
235        assert_eq!(spans.len(), 1);
236        assert_eq!(spans[0].span_id, "span-1");
237    }
238
239    #[test]
240    fn test_histogram() {
241        let hist = Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
242
243        assert_eq!(hist.count(), 5);
244        assert_eq!(hist.sum(), 15.0);
245        assert_eq!(hist.p50(), 3.0);
246    }
247
248    #[test]
249    fn test_metrics_recorder() {
250        let recorder = MetricsRecorder::new();
251
252        recorder.increment_counter("requests", 1);
253        recorder.increment_counter("requests", 2);
254        assert_eq!(recorder.get_counter("requests"), 3);
255
256        recorder.set_gauge("active_connections", 42);
257        assert_eq!(recorder.get_gauge("active_connections"), 42);
258
259        recorder.record_histogram("latency", 100.0);
260        recorder.record_histogram("latency", 200.0);
261        let hist = recorder.get_histogram("latency");
262        assert_eq!(hist.count(), 2);
263    }
264}