allframe_core/
otel.rs

1//! OpenTelemetry automatic instrumentation
2//!
3//! This module provides automatic distributed tracing, metrics, and context
4//! propagation for AllFrame applications with zero manual instrumentation
5//! required.
6
7use std::{
8    collections::HashMap,
9    sync::{Arc, RwLock},
10};
11
12// Re-export macros
13#[cfg(feature = "otel")]
14pub use allframe_macros::traced;
15
16/// Span represents a unit of work in distributed tracing
17#[derive(Debug, Clone)]
18pub struct Span {
19    /// Span ID
20    pub span_id: String,
21    /// Parent span ID (if any)
22    pub parent_span_id: Option<String>,
23    /// Trace ID
24    pub trace_id: String,
25    /// Span name
26    pub name: String,
27    /// Span attributes
28    pub attributes: HashMap<String, String>,
29    /// Span status
30    pub status: String,
31    /// Error message (if failed)
32    pub error_message: String,
33    /// Duration in milliseconds
34    pub duration_ms: f64,
35    /// Architecture layer (if applicable)
36    pub layer: String,
37}
38
39/// SpanRecorder for testing - records all spans
40#[derive(Clone, Default)]
41pub struct SpanRecorder {
42    spans: Arc<RwLock<Vec<Span>>>,
43}
44
45impl SpanRecorder {
46    /// Create a new span recorder
47    pub fn new() -> Self {
48        Self {
49            spans: Arc::new(RwLock::new(Vec::new())),
50        }
51    }
52
53    /// Record a span
54    pub fn record(&self, span: Span) {
55        let mut spans = self.spans.write().unwrap();
56        spans.push(span);
57    }
58
59    /// Get all recorded spans
60    pub fn spans(&self) -> Vec<Span> {
61        self.spans.read().unwrap().clone()
62    }
63}
64
65/// Get the current span ID (placeholder)
66pub fn current_span_id() -> String {
67    "span-placeholder".to_string()
68}
69
70/// Get the current trace ID (placeholder)
71pub fn current_trace_id() -> String {
72    "trace-placeholder".to_string()
73}
74
75/// Start a new trace (placeholder)
76pub fn start_trace(_trace_id: &str) {
77    // Placeholder for MVP
78}
79
80/// Set baggage value (placeholder)
81pub fn set_baggage(_key: &str, _value: &str) {
82    // Placeholder for MVP
83}
84
85/// Get baggage value (placeholder)
86pub fn get_baggage(_key: &str) -> Option<String> {
87    // Placeholder for MVP
88    None
89}
90
91/// SpanContext for distributed tracing
92#[derive(Debug, Clone)]
93pub struct SpanContext {
94    /// Trace ID
95    pub trace_id: String,
96    /// Parent span ID
97    pub parent_span_id: String,
98    /// Sampled flag
99    pub sampled: bool,
100}
101
102impl SpanContext {
103    /// Create a new span context
104    pub fn new(trace_id: &str, parent_span_id: &str) -> Self {
105        Self {
106            trace_id: trace_id.to_string(),
107            parent_span_id: parent_span_id.to_string(),
108            sampled: true,
109        }
110    }
111}
112
113/// Inject context into headers (placeholder)
114pub fn inject_context(_context: &SpanContext) -> HashMap<String, String> {
115    let mut headers = HashMap::new();
116    headers.insert("traceparent".to_string(), "placeholder".to_string());
117    headers
118}
119
120/// Extract context from headers (placeholder)
121pub fn extract_context(headers: &HashMap<String, String>) -> Option<SpanContext> {
122    headers.get("traceparent").map(|_| SpanContext {
123        trace_id: "extracted-trace".to_string(),
124        parent_span_id: "extracted-span".to_string(),
125        sampled: true,
126    })
127}
128
129/// MetricsRecorder for testing - records all metrics
130#[derive(Clone, Default)]
131pub struct MetricsRecorder {
132    counters: Arc<RwLock<HashMap<String, u64>>>,
133    gauges: Arc<RwLock<HashMap<String, i64>>>,
134    histograms: Arc<RwLock<HashMap<String, Vec<f64>>>>,
135}
136
137impl MetricsRecorder {
138    /// Create a new metrics recorder
139    pub fn new() -> Self {
140        Self {
141            counters: Arc::new(RwLock::new(HashMap::new())),
142            gauges: Arc::new(RwLock::new(HashMap::new())),
143            histograms: Arc::new(RwLock::new(HashMap::new())),
144        }
145    }
146
147    /// Get current instance (placeholder)
148    pub fn current() -> Self {
149        Self::new()
150    }
151
152    /// Get counter value
153    pub fn get_counter(&self, name: &str) -> u64 {
154        self.counters
155            .read()
156            .unwrap()
157            .get(name)
158            .copied()
159            .unwrap_or(0)
160    }
161
162    /// Get gauge value
163    pub fn get_gauge(&self, name: &str) -> i64 {
164        self.gauges.read().unwrap().get(name).copied().unwrap_or(0)
165    }
166
167    /// Get histogram
168    pub fn get_histogram(&self, name: &str) -> Histogram {
169        let values = self
170            .histograms
171            .read()
172            .unwrap()
173            .get(name)
174            .cloned()
175            .unwrap_or_default();
176        Histogram::new(values)
177    }
178
179    /// Get counter with labels (placeholder)
180    pub fn get_counter_with_labels(&self, name: &str, _labels: &[(&str, &str)]) -> u64 {
181        self.get_counter(name)
182    }
183}
184
185/// Histogram for latency measurements
186pub struct Histogram {
187    values: Vec<f64>,
188}
189
190impl Histogram {
191    /// Create new histogram
192    pub fn new(values: Vec<f64>) -> Self {
193        Self { values }
194    }
195
196    /// Get count of measurements
197    pub fn count(&self) -> usize {
198        self.values.len()
199    }
200
201    /// Get sum of all values
202    pub fn sum(&self) -> f64 {
203        self.values.iter().sum()
204    }
205
206    /// Get p50 percentile
207    pub fn p50(&self) -> f64 {
208        self.percentile(0.5)
209    }
210
211    /// Get p95 percentile
212    pub fn p95(&self) -> f64 {
213        self.percentile(0.95)
214    }
215
216    /// Get p99 percentile
217    pub fn p99(&self) -> f64 {
218        self.percentile(0.99)
219    }
220
221    fn percentile(&self, p: f64) -> f64 {
222        if self.values.is_empty() {
223            return 0.0;
224        }
225        let mut sorted = self.values.clone();
226        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
227        let index = ((self.values.len() as f64 - 1.0) * p) as usize;
228        sorted[index]
229    }
230}
231
232/// Exporter type
233#[derive(Debug, Clone, PartialEq)]
234pub enum ExporterType {
235    /// Stdout console exporter
236    Stdout,
237    /// Jaeger exporter
238    Jaeger {
239        /// Jaeger endpoint URL
240        endpoint: String,
241    },
242    /// OTLP exporter
243    Otlp {
244        /// OTLP endpoint URL
245        endpoint: String,
246    },
247}
248
249/// Configure exporter (placeholder)
250pub fn configure_exporter(_exporter: ExporterType) {
251    // Placeholder for MVP
252}
253
254/// Configure batch export (placeholder)
255pub fn configure_batch_export(_batch_size: usize, _flush_interval_ms: u64) {
256    // Placeholder for MVP
257}
258
259/// Get export count (placeholder)
260pub fn get_export_count() -> usize {
261    0
262}
263
264/// Configure sampling rate (placeholder)
265pub fn configure_sampling(_rate: f64) {
266    // Placeholder for MVP
267}
268
269/// Enable tracing (placeholder)
270pub fn enable_tracing() {
271    // Placeholder for MVP
272}
273
274/// Disable tracing (placeholder)
275pub fn disable_tracing() {
276    // Placeholder for MVP
277}
278
279/// OTel configuration
280#[derive(Debug, Clone)]
281pub struct OtelConfig {
282    /// Service name
283    pub service_name: String,
284    /// Exporter type
285    pub exporter_type: String,
286    /// Sampling rate
287    pub sampling_rate: f64,
288    /// Batch size
289    pub batch_size: usize,
290}
291
292/// Configure from file (placeholder)
293pub async fn configure_from_file(_path: &str) -> Result<(), String> {
294    // Placeholder for MVP
295    Ok(())
296}
297
298/// Get current config (placeholder)
299pub fn get_config() -> OtelConfig {
300    OtelConfig {
301        service_name: "allframe".to_string(),
302        exporter_type: "stdout".to_string(),
303        sampling_rate: 1.0,
304        batch_size: 512,
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_span_recorder() {
314        let recorder = SpanRecorder::new();
315
316        let span = Span {
317            span_id: "span-1".to_string(),
318            parent_span_id: None,
319            trace_id: "trace-1".to_string(),
320            name: "test".to_string(),
321            attributes: HashMap::new(),
322            status: "ok".to_string(),
323            error_message: String::new(),
324            duration_ms: 100.0,
325            layer: String::new(),
326        };
327
328        recorder.record(span.clone());
329        let spans = recorder.spans();
330
331        assert_eq!(spans.len(), 1);
332        assert_eq!(spans[0].span_id, "span-1");
333    }
334
335    #[test]
336    fn test_histogram() {
337        let hist = Histogram::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
338
339        assert_eq!(hist.count(), 5);
340        assert_eq!(hist.sum(), 15.0);
341        assert_eq!(hist.p50(), 3.0);
342    }
343}