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.entry(name.to_string()).or_default().push(value);
105    }
106
107    /// Get counter value
108    pub fn get_counter(&self, name: &str) -> u64 {
109        self.counters
110            .read()
111            .unwrap()
112            .get(name)
113            .copied()
114            .unwrap_or(0)
115    }
116
117    /// Get gauge value
118    pub fn get_gauge(&self, name: &str) -> i64 {
119        self.gauges.read().unwrap().get(name).copied().unwrap_or(0)
120    }
121
122    /// Get histogram
123    pub fn get_histogram(&self, name: &str) -> Histogram {
124        let values = self
125            .histograms
126            .read()
127            .unwrap()
128            .get(name)
129            .cloned()
130            .unwrap_or_default();
131        Histogram::new(values)
132    }
133
134    /// Get counter with labels (placeholder)
135    pub fn get_counter_with_labels(&self, name: &str, _labels: &[(&str, &str)]) -> u64 {
136        self.get_counter(name)
137    }
138}
139
140/// SpanContext for distributed tracing
141#[derive(Debug, Clone)]
142pub struct SpanContext {
143    /// Trace ID
144    pub trace_id: String,
145    /// Parent span ID
146    pub parent_span_id: String,
147    /// Sampled flag
148    pub sampled: bool,
149}
150
151impl SpanContext {
152    /// Create a new span context
153    pub fn new(trace_id: &str, parent_span_id: &str) -> Self {
154        Self {
155            trace_id: trace_id.to_string(),
156            parent_span_id: parent_span_id.to_string(),
157            sampled: true,
158        }
159    }
160}
161
162/// Histogram for latency measurements
163pub struct Histogram {
164    values: Vec<f64>,
165}
166
167impl Histogram {
168    /// Create new histogram
169    pub fn new(values: Vec<f64>) -> Self {
170        Self { values }
171    }
172
173    /// Get count of measurements
174    pub fn count(&self) -> usize {
175        self.values.len()
176    }
177
178    /// Get sum of all values
179    pub fn sum(&self) -> f64 {
180        self.values.iter().sum()
181    }
182
183    /// Get p50 percentile
184    pub fn p50(&self) -> f64 {
185        self.percentile(0.5)
186    }
187
188    /// Get p95 percentile
189    pub fn p95(&self) -> f64 {
190        self.percentile(0.95)
191    }
192
193    /// Get p99 percentile
194    pub fn p99(&self) -> f64 {
195        self.percentile(0.99)
196    }
197
198    fn percentile(&self, p: f64) -> f64 {
199        if self.values.is_empty() {
200            return 0.0;
201        }
202        let mut sorted = self.values.clone();
203        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
204        let index = ((self.values.len() as f64 - 1.0) * p) as usize;
205        sorted[index]
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_span_recorder() {
215        let recorder = SpanRecorder::new();
216
217        let span = Span {
218            span_id: "span-1".to_string(),
219            parent_span_id: None,
220            trace_id: "trace-1".to_string(),
221            name: "test".to_string(),
222            attributes: HashMap::new(),
223            status: "ok".to_string(),
224            error_message: String::new(),
225            duration_ms: 100.0,
226            layer: String::new(),
227        };
228
229        recorder.record(span.clone());
230        let spans = recorder.spans();
231
232        assert_eq!(spans.len(), 1);
233        assert_eq!(spans[0].span_id, "span-1");
234    }
235
236    #[test]
237    fn test_histogram() {
238        let hist = Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
239
240        assert_eq!(hist.count(), 5);
241        assert_eq!(hist.sum(), 15.0);
242        assert_eq!(hist.p50(), 3.0);
243    }
244
245    #[test]
246    fn test_metrics_recorder() {
247        let recorder = MetricsRecorder::new();
248
249        recorder.increment_counter("requests", 1);
250        recorder.increment_counter("requests", 2);
251        assert_eq!(recorder.get_counter("requests"), 3);
252
253        recorder.set_gauge("active_connections", 42);
254        assert_eq!(recorder.get_gauge("active_connections"), 42);
255
256        recorder.record_histogram("latency", 100.0);
257        recorder.record_histogram("latency", 200.0);
258        let hist = recorder.get_histogram("latency");
259        assert_eq!(hist.count(), 2);
260    }
261}