Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/realizar/src/observability.rs
Line
Count
Source
1
//! Observability Module for Production Monitoring
2
//!
3
//! Provides comprehensive observability features per spec ยง7:
4
//! - Trueno-DB metrics integration for time-series storage
5
//! - Renacer tracing for distributed request tracking
6
//! - A/B testing support for model comparison
7
//!
8
//! ## Example
9
//!
10
//! ```rust,ignore
11
//! use realizar::observability::{ObservabilityConfig, Observer, ABTest};
12
//!
13
//! let config = ObservabilityConfig::new()
14
//!     .with_trueno_db("trueno-db://metrics")
15
//!     .with_tracing(true);
16
//!
17
//! let observer = Observer::new(config);
18
//!
19
//! // Record inference metrics
20
//! observer.record_inference("model-v1", 150, 32, Duration::from_millis(45));
21
//!
22
//! // A/B testing
23
//! let ab_test = ABTest::new("model-comparison")
24
//!     .with_variant("control", "model-v1", 0.5)
25
//!     .with_variant("treatment", "model-v2", 0.5);
26
//! let variant = ab_test.select("user-123");
27
//! ```
28
29
use std::{
30
    collections::HashMap,
31
    sync::{
32
        atomic::{AtomicU64, Ordering},
33
        Arc, RwLock,
34
    },
35
    time::{Duration, SystemTime, UNIX_EPOCH},
36
};
37
38
use serde::{Deserialize, Serialize};
39
40
// ============================================================================
41
// W3C Trace Context (per OpenTelemetry specification)
42
// ============================================================================
43
44
/// W3C Trace Context for distributed tracing
45
/// Implements <https://www.w3.org/TR/trace-context/>
46
#[derive(Debug, Clone, Serialize, Deserialize)]
47
pub struct TraceContext {
48
    /// Trace ID (32 hex chars, 16 bytes)
49
    pub trace_id: String,
50
    /// Parent Span ID (16 hex chars, 8 bytes)
51
    pub parent_span_id: Option<String>,
52
    /// Trace flags (sampled, etc.)
53
    pub trace_flags: u8,
54
    /// Trace state (vendor-specific data)
55
    pub trace_state: Option<String>,
56
}
57
58
impl TraceContext {
59
    /// Create a new trace context with a fresh trace ID
60
    #[must_use]
61
5
    pub fn new() -> Self {
62
5
        Self {
63
5
            trace_id: generate_trace_id(),
64
5
            parent_span_id: None,
65
5
            trace_flags: 0x01, // Sampled
66
5
            trace_state: None,
67
5
        }
68
5
    }
69
70
    /// Create child context with new span ID
71
    #[must_use]
72
1
    pub fn child(&self, parent_span_id: &str) -> Self {
73
1
        Self {
74
1
            trace_id: self.trace_id.clone(),
75
1
            parent_span_id: Some(parent_span_id.to_string()),
76
1
            trace_flags: self.trace_flags,
77
1
            trace_state: self.trace_state.clone(),
78
1
        }
79
1
    }
80
81
    /// Parse from W3C traceparent header
82
    /// Format: {version}-{trace_id}-{parent_id}-{flags}
83
    /// Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
84
    #[must_use]
85
6
    pub fn from_traceparent(header: &str) -> Option<Self> {
86
6
        let parts: Vec<&str> = header.split('-').collect();
87
6
        if parts.len() != 4 {
88
2
            return None;
89
4
        }
90
91
4
        let version = parts[0];
92
4
        if version != "00" {
93
1
            return None; // Only version 00 supported
94
3
        }
95
96
3
        let trace_id = parts[1];
97
3
        let parent_span_id = parts[2];
98
3
        let flags = u8::from_str_radix(parts[3], 16).ok()
?0
;
99
100
        // Validate lengths
101
3
        if trace_id.len() != 32 || 
parent_span_id.len() != 162
{
102
1
            return None;
103
2
        }
104
105
2
        Some(Self {
106
2
            trace_id: trace_id.to_string(),
107
2
            parent_span_id: Some(parent_span_id.to_string()),
108
2
            trace_flags: flags,
109
2
            trace_state: None,
110
2
        })
111
6
    }
112
113
    /// Generate W3C traceparent header
114
    #[must_use]
115
1
    pub fn to_traceparent(&self, span_id: &str) -> String {
116
1
        format!("00-{}-{}-{:02x}", self.trace_id, span_id, self.trace_flags)
117
1
    }
118
119
    /// Set tracestate header value
120
    #[must_use]
121
1
    pub fn with_tracestate(mut self, state: impl Into<String>) -> Self {
122
1
        self.trace_state = Some(state.into());
123
1
        self
124
1
    }
125
126
    /// Check if trace is sampled
127
    #[must_use]
128
4
    pub fn is_sampled(&self) -> bool {
129
4
        self.trace_flags & 0x01 != 0
130
4
    }
131
132
    /// Set sampled flag
133
2
    pub fn set_sampled(&mut self, sampled: bool) {
134
2
        if sampled {
135
1
            self.trace_flags |= 0x01;
136
1
        } else {
137
1
            self.trace_flags &= !0x01;
138
1
        }
139
2
    }
140
}
141
142
impl Default for TraceContext {
143
1
    fn default() -> Self {
144
1
        Self::new()
145
1
    }
146
}
147
148
// ============================================================================
149
// Latency Histogram for Percentile Calculations
150
// ============================================================================
151
152
/// Histogram for tracking latency distributions and calculating percentiles
153
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
154
pub struct LatencyHistogram {
155
    /// Bucket boundaries in microseconds
156
    buckets: Vec<u64>,
157
    /// Count per bucket
158
    counts: Vec<u64>,
159
    /// Total count
160
    total: u64,
161
    /// Sum of all values (for mean calculation)
162
    sum: u64,
163
    /// Min value seen
164
    min: Option<u64>,
165
    /// Max value seen
166
    max: Option<u64>,
167
}
168
169
impl LatencyHistogram {
170
    /// Create histogram with default buckets (exponential: 1ms to 60s)
171
    #[must_use]
172
8
    pub fn new() -> Self {
173
        // Buckets: 1ms, 2ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s, 30s, 60s
174
8
        let buckets = vec![
175
            1_000,      // 1ms
176
            2_000,      // 2ms
177
            5_000,      // 5ms
178
            10_000,     // 10ms
179
            25_000,     // 25ms
180
            50_000,     // 50ms
181
            100_000,    // 100ms
182
            250_000,    // 250ms
183
            500_000,    // 500ms
184
            1_000_000,  // 1s
185
            2_500_000,  // 2.5s
186
            5_000_000,  // 5s
187
            10_000_000, // 10s
188
            30_000_000, // 30s
189
            60_000_000, // 60s
190
        ];
191
8
        let counts = vec![0; buckets.len() + 1]; // +1 for overflow bucket
192
8
        Self {
193
8
            buckets,
194
8
            counts,
195
8
            total: 0,
196
8
            sum: 0,
197
8
            min: None,
198
8
            max: None,
199
8
        }
200
8
    }
201
202
    /// Create histogram with custom buckets (in microseconds)
203
    #[must_use]
204
1
    pub fn with_buckets(mut buckets: Vec<u64>) -> Self {
205
1
        buckets.sort_unstable();
206
1
        let counts = vec![0; buckets.len() + 1];
207
1
        Self {
208
1
            buckets,
209
1
            counts,
210
1
            total: 0,
211
1
            sum: 0,
212
1
            min: None,
213
1
            max: None,
214
1
        }
215
1
    }
216
217
    /// Record a latency value in microseconds
218
114
    pub fn observe(&mut self, value_us: u64) {
219
114
        self.total += 1;
220
114
        self.sum += value_us;
221
222
        // Update min/max
223
114
        self.min = Some(self.min.map_or(value_us, |m| 
m107
.
min107
(
value_us107
)));
224
114
        self.max = Some(self.max.map_or(value_us, |m| 
m107
.
max107
(
value_us107
)));
225
226
        // Find bucket
227
643
        let 
bucket_idx114
=
self.buckets.iter()114
.
position114
(|&b| value_us <= b);
228
114
        match bucket_idx {
229
113
            Some(idx) => self.counts[idx] += 1,
230
1
            None => *self.counts.last_mut().unwrap_or(&mut 0) += 1,
231
        }
232
114
    }
233
234
    /// Record latency from Duration
235
1
    pub fn observe_duration(&mut self, duration: Duration) {
236
1
        self.observe(duration.as_micros() as u64);
237
1
    }
238
239
    /// Get percentile value (0-100)
240
    #[must_use]
241
9
    pub fn percentile(&self, p: f64) -> Option<u64> {
242
9
        if self.total == 0 || 
!5
(0.0..=100.0)5
.contains(&p) {
243
6
            return None;
244
3
        }
245
246
3
        let target = ((p / 100.0) * self.total as f64).ceil() as u64;
247
3
        let mut cumulative = 0u64;
248
249
20
        for (i, &count) in 
self.counts.iter()3
.
enumerate3
() {
250
20
            cumulative += count;
251
20
            if cumulative >= target {
252
3
                return if i < self.buckets.len() {
253
3
                    Some(self.buckets[i])
254
                } else {
255
0
                    self.max // Overflow bucket
256
                };
257
17
            }
258
        }
259
260
0
        self.max
261
9
    }
262
263
    /// Get p50 (median)
264
    #[must_use]
265
2
    pub fn p50(&self) -> Option<u64> {
266
2
        self.percentile(50.0)
267
2
    }
268
269
    /// Get p95
270
    #[must_use]
271
2
    pub fn p95(&self) -> Option<u64> {
272
2
        self.percentile(95.0)
273
2
    }
274
275
    /// Get p99
276
    #[must_use]
277
2
    pub fn p99(&self) -> Option<u64> {
278
2
        self.percentile(99.0)
279
2
    }
280
281
    /// Get mean latency
282
    #[must_use]
283
2
    pub fn mean(&self) -> Option<f64> {
284
2
        if self.total == 0 {
285
1
            None
286
        } else {
287
1
            Some(self.sum as f64 / self.total as f64)
288
        }
289
2
    }
290
291
    /// Get total count
292
    #[must_use]
293
4
    pub fn count(&self) -> u64 {
294
4
        self.total
295
4
    }
296
297
    /// Get min value
298
    #[must_use]
299
4
    pub fn min(&self) -> Option<u64> {
300
4
        self.min
301
4
    }
302
303
    /// Get max value
304
    #[must_use]
305
3
    pub fn max_val(&self) -> Option<u64> {
306
3
        self.max
307
3
    }
308
309
    /// Export as Prometheus histogram format
310
    #[must_use]
311
1
    pub fn to_prometheus(&self, name: &str, labels: &str) -> String {
312
        use std::fmt::Write;
313
1
        let mut output = String::new();
314
315
        // Bucket counters (cumulative)
316
1
        let mut cumulative = 0u64;
317
15
        for (i, &boundary) in 
self.buckets.iter()1
.
enumerate1
() {
318
15
            cumulative += self.counts[i];
319
15
            let le = boundary as f64 / 1_000_000.0; // Convert to seconds
320
15
            let _ = writeln!(
321
15
                output,
322
15
                "{name}_bucket{{le=\"{le:.6}\",{labels}}} {cumulative}"
323
15
            );
324
15
        }
325
        // +Inf bucket
326
1
        cumulative += self.counts.last().copied().unwrap_or(0);
327
1
        let _ = writeln!(output, "{name}_bucket{{le=\"+Inf\",{labels}}} {cumulative}");
328
329
        // Sum and count
330
1
        let sum_secs = self.sum as f64 / 1_000_000.0;
331
1
        let _ = writeln!(output, "{name}_sum{{{labels}}} {sum_secs:.6}");
332
1
        let _ = writeln!(output, "{name}_count{{{labels}}} {}", self.total);
333
334
1
        output
335
1
    }
336
}
337
338
// ============================================================================
339
// OpenTelemetry-Compatible Span Export
340
// ============================================================================
341
342
/// OpenTelemetry-compatible span for export
343
#[derive(Debug, Clone, Serialize, Deserialize)]
344
pub struct OtelSpan {
345
    /// Trace ID (hex string)
346
    #[serde(rename = "traceId")]
347
    pub trace_id: String,
348
    /// Span ID (hex string)
349
    #[serde(rename = "spanId")]
350
    pub span_id: String,
351
    /// Parent Span ID (optional)
352
    #[serde(rename = "parentSpanId", skip_serializing_if = "Option::is_none")]
353
    pub parent_span_id: Option<String>,
354
    /// Operation name
355
    #[serde(rename = "operationName")]
356
    pub operation_name: String,
357
    /// Service name
358
    #[serde(rename = "serviceName")]
359
    pub service_name: String,
360
    /// Start time (Unix epoch microseconds)
361
    #[serde(rename = "startTimeUnixNano")]
362
    pub start_time: u64,
363
    /// End time (Unix epoch microseconds)
364
    #[serde(rename = "endTimeUnixNano")]
365
    pub end_time: u64,
366
    /// Span kind
367
    #[serde(rename = "kind")]
368
    pub kind: SpanKind,
369
    /// Status
370
    pub status: OtelStatus,
371
    /// Attributes
372
    pub attributes: Vec<OtelAttribute>,
373
}
374
375
/// OpenTelemetry span kind
376
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
377
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
378
pub enum SpanKind {
379
    /// Internal operation
380
    #[default]
381
    Internal,
382
    /// Server-side of RPC
383
    Server,
384
    /// Client-side of RPC
385
    Client,
386
    /// Message producer
387
    Producer,
388
    /// Message consumer
389
    Consumer,
390
}
391
392
/// OpenTelemetry status
393
#[derive(Debug, Clone, Serialize, Deserialize)]
394
pub struct OtelStatus {
395
    /// Status code
396
    pub code: OtelStatusCode,
397
    /// Optional message
398
    #[serde(skip_serializing_if = "Option::is_none")]
399
    pub message: Option<String>,
400
}
401
402
/// OpenTelemetry status code
403
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
404
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
405
pub enum OtelStatusCode {
406
    /// Unset
407
    #[default]
408
    Unset,
409
    /// OK
410
    Ok,
411
    /// Error
412
    Error,
413
}
414
415
/// OpenTelemetry attribute
416
#[derive(Debug, Clone, Serialize, Deserialize)]
417
pub struct OtelAttribute {
418
    /// Attribute key
419
    pub key: String,
420
    /// Attribute value
421
    pub value: OtelValue,
422
}
423
424
/// OpenTelemetry attribute value
425
#[derive(Debug, Clone, Serialize, Deserialize)]
426
#[serde(untagged)]
427
pub enum OtelValue {
428
    /// String value
429
    String {
430
        /// The string value
431
        string_value: String,
432
    },
433
    /// Integer value
434
    Int {
435
        /// The integer value
436
        int_value: i64,
437
    },
438
    /// Float value
439
    Float {
440
        /// The double/float value
441
        double_value: f64,
442
    },
443
    /// Boolean value
444
    Bool {
445
        /// The boolean value
446
        bool_value: bool,
447
    },
448
}
449
450
impl From<&str> for OtelValue {
451
5
    fn from(s: &str) -> Self {
452
5
        OtelValue::String {
453
5
            string_value: s.to_string(),
454
5
        }
455
5
    }
456
}
457
458
impl From<String> for OtelValue {
459
1
    fn from(s: String) -> Self {
460
1
        OtelValue::String { string_value: s }
461
1
    }
462
}
463
464
impl From<i64> for OtelValue {
465
1
    fn from(v: i64) -> Self {
466
1
        OtelValue::Int { int_value: v }
467
1
    }
468
}
469
470
impl From<f64> for OtelValue {
471
1
    fn from(v: f64) -> Self {
472
1
        OtelValue::Float { double_value: v }
473
1
    }
474
}
475
476
impl From<bool> for OtelValue {
477
1
    fn from(v: bool) -> Self {
478
1
        OtelValue::Bool { bool_value: v }
479
1
    }
480
}
481
482
// ============================================================================
483
// OBS-001: Configuration
484
// ============================================================================
485
486
/// Observability configuration
487
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
488
pub struct ObservabilityConfig {
489
    /// Trueno-DB connection URI for metrics storage
490
    pub trueno_db_uri: Option<String>,
491
    /// Enable distributed tracing
492
    pub tracing_enabled: bool,
493
    /// Sampling rate for traces (0.0 - 1.0)
494
    pub trace_sample_rate: f64,
495
    /// Metrics flush interval in seconds
496
    pub flush_interval_secs: u64,
497
    /// Enable A/B testing
498
    pub ab_testing_enabled: bool,
499
}
500
501
impl ObservabilityConfig {
502
    /// Create new configuration
503
    #[must_use]
504
9
    pub fn new() -> Self {
505
9
        Self {
506
9
            trueno_db_uri: None,
507
9
            tracing_enabled: true,
508
9
            trace_sample_rate: 1.0,
509
9
            flush_interval_secs: 60,
510
9
            ab_testing_enabled: true,
511
9
        }
512
9
    }
513
514
    /// Set Trueno-DB URI
515
    #[must_use]
516
1
    pub fn with_trueno_db(mut self, uri: impl Into<String>) -> Self {
517
1
        self.trueno_db_uri = Some(uri.into());
518
1
        self
519
1
    }
520
521
    /// Enable/disable tracing
522
    #[must_use]
523
2
    pub fn with_tracing(mut self, enabled: bool) -> Self {
524
2
        self.tracing_enabled = enabled;
525
2
        self
526
2
    }
527
528
    /// Set trace sampling rate
529
    #[must_use]
530
2
    pub fn with_sample_rate(mut self, rate: f64) -> Self {
531
2
        self.trace_sample_rate = rate.clamp(0.0, 1.0);
532
2
        self
533
2
    }
534
535
    /// Set flush interval
536
    #[must_use]
537
0
    pub fn with_flush_interval(mut self, secs: u64) -> Self {
538
0
        self.flush_interval_secs = secs;
539
0
        self
540
0
    }
541
}
542
543
// ============================================================================
544
// OBS-002: Metrics Point
545
// ============================================================================
546
547
/// A single metrics data point
548
#[derive(Debug, Clone, Serialize, Deserialize)]
549
pub struct MetricPoint {
550
    /// Metric name
551
    pub name: String,
552
    /// Metric value
553
    pub value: f64,
554
    /// Timestamp (Unix epoch millis)
555
    pub timestamp: u64,
556
    /// Labels/tags
557
    pub labels: HashMap<String, String>,
558
}
559
560
impl MetricPoint {
561
    /// Create a new metric point
562
    #[must_use]
563
6
    pub fn new(name: impl Into<String>, value: f64) -> Self {
564
        Self {
565
6
            name: name.into(),
566
6
            value,
567
6
            timestamp: SystemTime::now()
568
6
                .duration_since(UNIX_EPOCH)
569
6
                .map(|d| d.as_millis() as u64)
570
6
                .unwrap_or(0),
571
6
            labels: HashMap::new(),
572
        }
573
6
    }
574
575
    /// Add a label
576
    #[must_use]
577
7
    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
578
7
        self.labels.insert(key.into(), value.into());
579
7
        self
580
7
    }
581
582
    /// Format for Trueno-DB line protocol
583
    #[must_use]
584
4
    pub fn to_line_protocol(&self) -> String {
585
4
        let labels_str = if self.labels.is_empty() {
586
0
            String::new()
587
        } else {
588
4
            let pairs: Vec<String> = self
589
4
                .labels
590
4
                .iter()
591
4
                .map(|(k, v)| format!("{k}={v}"))
592
4
                .collect();
593
4
            format!(",{}", pairs.join(","))
594
        };
595
596
4
        format!(
597
4
            "{}{} value={} {}",
598
            self.name, labels_str, self.value, self.timestamp
599
        )
600
4
    }
601
}
602
603
// ============================================================================
604
// OBS-003: Tracing Span
605
// ============================================================================
606
607
/// A tracing span for distributed request tracking
608
#[derive(Debug, Clone, Serialize, Deserialize)]
609
pub struct Span {
610
    /// Unique span ID
611
    pub span_id: String,
612
    /// Trace ID (shared across spans in same request)
613
    pub trace_id: String,
614
    /// Parent span ID (if any)
615
    pub parent_id: Option<String>,
616
    /// Operation name
617
    pub operation: String,
618
    /// Service name
619
    pub service: String,
620
    /// Start timestamp (Unix epoch micros)
621
    pub start_time: u64,
622
    /// Duration in microseconds
623
    pub duration_us: Option<u64>,
624
    /// Span status
625
    pub status: SpanStatus,
626
    /// Span attributes
627
    pub attributes: HashMap<String, String>,
628
}
629
630
/// Span status
631
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
632
pub enum SpanStatus {
633
    /// Span is in progress
634
    #[default]
635
    InProgress,
636
    /// Span completed successfully
637
    Ok,
638
    /// Span completed with error
639
    Error,
640
}
641
642
impl Span {
643
    /// Create a new span
644
    #[must_use]
645
17
    pub fn new(operation: impl Into<String>, trace_id: impl Into<String>) -> Self {
646
        Self {
647
17
            span_id: generate_id(),
648
17
            trace_id: trace_id.into(),
649
17
            parent_id: None,
650
17
            operation: operation.into(),
651
17
            service: "realizar".to_string(),
652
17
            start_time: SystemTime::now()
653
17
                .duration_since(UNIX_EPOCH)
654
17
                .map(|d| d.as_micros() as u64)
655
17
                .unwrap_or(0),
656
17
            duration_us: None,
657
17
            status: SpanStatus::InProgress,
658
17
            attributes: HashMap::new(),
659
        }
660
17
    }
661
662
    /// Create a child span
663
    #[must_use]
664
2
    pub fn child(&self, operation: impl Into<String>) -> Self {
665
2
        let mut span = Self::new(operation, self.trace_id.clone());
666
2
        span.parent_id = Some(self.span_id.clone());
667
2
        span
668
2
    }
669
670
    /// Set parent span
671
    #[must_use]
672
0
    pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
673
0
        self.parent_id = Some(parent_id.into());
674
0
        self
675
0
    }
676
677
    /// Add attribute
678
    #[must_use]
679
2
    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
680
2
        self.attributes.insert(key.into(), value.into());
681
2
        self
682
2
    }
683
684
    /// End the span with success
685
9
    pub fn end_ok(&mut self) {
686
9
        self.status = SpanStatus::Ok;
687
        self.duration_us = Some(
688
9
            SystemTime::now()
689
9
                .duration_since(UNIX_EPOCH)
690
9
                .map(|d| d.as_micros() as u64)
691
9
                .unwrap_or(0)
692
9
                .saturating_sub(self.start_time),
693
        );
694
9
    }
695
696
    /// End the span with error
697
2
    pub fn end_error(&mut self, error: impl Into<String>) {
698
2
        self.status = SpanStatus::Error;
699
2
        self.attributes.insert("error".to_string(), error.into());
700
        self.duration_us = Some(
701
2
            SystemTime::now()
702
2
                .duration_since(UNIX_EPOCH)
703
2
                .map(|d| d.as_micros() as u64)
704
2
                .unwrap_or(0)
705
2
                .saturating_sub(self.start_time),
706
        );
707
2
    }
708
709
    /// Get duration as Duration
710
    #[must_use]
711
0
    pub fn duration(&self) -> Option<Duration> {
712
0
        self.duration_us.map(Duration::from_micros)
713
0
    }
714
715
    /// Set span kind
716
    #[must_use]
717
1
    pub fn with_kind(mut self, kind: SpanKind) -> Self {
718
1
        self.attributes
719
1
            .insert("span.kind".to_string(), format!("{kind:?}"));
720
1
        self
721
1
    }
722
723
    /// Convert to OpenTelemetry-compatible span format
724
    #[must_use]
725
6
    pub fn to_otel(&self) -> OtelSpan {
726
6
        let end_time = self.start_time + self.duration_us.unwrap_or(0);
727
728
6
        let status = match self.status {
729
5
            SpanStatus::Ok => OtelStatus {
730
5
                code: OtelStatusCode::Ok,
731
5
                message: None,
732
5
            },
733
1
            SpanStatus::Error => OtelStatus {
734
1
                code: OtelStatusCode::Error,
735
1
                message: self.attributes.get("error").cloned(),
736
1
            },
737
0
            SpanStatus::InProgress => OtelStatus {
738
0
                code: OtelStatusCode::Unset,
739
0
                message: None,
740
0
            },
741
        };
742
743
6
        let attributes: Vec<OtelAttribute> = self
744
6
            .attributes
745
6
            .iter()
746
6
            .map(|(k, v)| OtelAttribute {
747
4
                key: k.clone(),
748
4
                value: OtelValue::from(v.as_str()),
749
4
            })
750
6
            .collect();
751
752
6
        let kind = self
753
6
            .attributes
754
6
            .get("span.kind")
755
6
            .map_or(SpanKind::Internal, |k| match 
k.as_str()1
{
756
1
                "Server" => SpanKind::Server,
757
0
                "Client" => SpanKind::Client,
758
0
                "Producer" => SpanKind::Producer,
759
0
                "Consumer" => SpanKind::Consumer,
760
0
                _ => SpanKind::Internal,
761
1
            });
762
763
6
        OtelSpan {
764
6
            trace_id: self.trace_id.clone(),
765
6
            span_id: self.span_id.clone(),
766
6
            parent_span_id: self.parent_id.clone(),
767
6
            operation_name: self.operation.clone(),
768
6
            service_name: self.service.clone(),
769
6
            start_time: self.start_time * 1000, // Convert to nanoseconds
770
6
            end_time: end_time * 1000,
771
6
            kind,
772
6
            status,
773
6
            attributes,
774
6
        }
775
6
    }
776
777
    /// Get trace context for propagation
778
    #[must_use]
779
1
    pub fn trace_context(&self) -> TraceContext {
780
1
        TraceContext {
781
1
            trace_id: self.trace_id.clone(),
782
1
            parent_span_id: Some(self.span_id.clone()),
783
1
            trace_flags: 0x01, // Sampled
784
1
            trace_state: None,
785
1
        }
786
1
    }
787
788
    /// Generate traceparent header for this span
789
    #[must_use]
790
1
    pub fn traceparent(&self) -> String {
791
1
        format!("00-{}-{}-01", self.trace_id, self.span_id)
792
1
    }
793
}
794
795
// ============================================================================
796
// OBS-004: A/B Testing
797
// ============================================================================
798
799
/// A/B test configuration
800
#[derive(Debug, Clone, Serialize, Deserialize)]
801
pub struct ABTest {
802
    /// Test name
803
    pub name: String,
804
    /// Test variants
805
    pub variants: Vec<ABVariant>,
806
    /// Whether test is active
807
    pub active: bool,
808
    /// Test start timestamp
809
    pub start_time: u64,
810
}
811
812
/// A/B test variant
813
#[derive(Debug, Clone, Serialize, Deserialize)]
814
pub struct ABVariant {
815
    /// Variant name
816
    pub name: String,
817
    /// Model to use for this variant
818
    pub model: String,
819
    /// Traffic weight (0.0 - 1.0)
820
    pub weight: f64,
821
}
822
823
impl ABTest {
824
    /// Create a new A/B test
825
    #[must_use]
826
4
    pub fn new(name: impl Into<String>) -> Self {
827
        Self {
828
4
            name: name.into(),
829
4
            variants: Vec::new(),
830
            active: true,
831
4
            start_time: SystemTime::now()
832
4
                .duration_since(UNIX_EPOCH)
833
4
                .map(|d| d.as_secs())
834
4
                .unwrap_or(0),
835
        }
836
4
    }
837
838
    /// Add a variant
839
    #[must_use]
840
8
    pub fn with_variant(
841
8
        mut self,
842
8
        name: impl Into<String>,
843
8
        model: impl Into<String>,
844
8
        weight: f64,
845
8
    ) -> Self {
846
8
        self.variants.push(ABVariant {
847
8
            name: name.into(),
848
8
            model: model.into(),
849
8
            weight: weight.clamp(0.0, 1.0),
850
8
        });
851
8
        self
852
8
    }
853
854
    /// Select a variant for a user (deterministic based on user ID)
855
    #[must_use]
856
1.00k
    pub fn select(&self, user_id: &str) -> Option<&ABVariant> {
857
1.00k
        if !self.active || self.variants.is_empty() {
858
0
            return None;
859
1.00k
        }
860
861
        // Normalize weights
862
1.00k
        let total_weight: f64 = self.variants.iter().map(|v| v.weight).sum();
863
1.00k
        if total_weight <= 0.0 {
864
0
            return self.variants.first();
865
1.00k
        }
866
867
        // Deterministic hash-based selection
868
1.00k
        let hash = simple_hash(user_id);
869
1.00k
        let selection = (hash as f64 / u64::MAX as f64) * total_weight;
870
871
1.00k
        let mut cumulative = 0.0;
872
1.51k
        for variant in &self.variants {
873
1.51k
            cumulative += variant.weight;
874
1.51k
            if selection < cumulative {
875
1.00k
                return Some(variant);
876
512
            }
877
        }
878
879
0
        self.variants.last()
880
1.00k
    }
881
882
    /// Check if test is valid (weights sum to ~1.0)
883
    #[must_use]
884
2
    pub fn is_valid(&self) -> bool {
885
2
        if self.variants.is_empty() {
886
0
            return false;
887
2
        }
888
2
        let total: f64 = self.variants.iter().map(|v| v.weight).sum();
889
2
        (total - 1.0).abs() < 0.001
890
2
    }
891
}
892
893
/// A/B test result tracking
894
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
895
pub struct ABTestResult {
896
    /// Test name
897
    pub test_name: String,
898
    /// Results per variant
899
    pub variants: HashMap<String, VariantResult>,
900
}
901
902
/// Results for a single variant
903
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
904
pub struct VariantResult {
905
    /// Number of requests
906
    pub requests: u64,
907
    /// Number of successes
908
    pub successes: u64,
909
    /// Total latency (ms)
910
    pub total_latency_ms: u64,
911
    /// Total tokens generated
912
    pub total_tokens: u64,
913
}
914
915
impl VariantResult {
916
    /// Calculate success rate
917
    #[must_use]
918
1
    pub fn success_rate(&self) -> f64 {
919
1
        if self.requests == 0 {
920
0
            0.0
921
        } else {
922
1
            self.successes as f64 / self.requests as f64
923
        }
924
1
    }
925
926
    /// Calculate average latency
927
    #[must_use]
928
1
    pub fn avg_latency_ms(&self) -> f64 {
929
1
        if self.requests == 0 {
930
0
            0.0
931
        } else {
932
1
            self.total_latency_ms as f64 / self.requests as f64
933
        }
934
1
    }
935
936
    /// Calculate tokens per request
937
    #[must_use]
938
1
    pub fn tokens_per_request(&self) -> f64 {
939
1
        if self.requests == 0 {
940
0
            0.0
941
        } else {
942
1
            self.total_tokens as f64 / self.requests as f64
943
        }
944
1
    }
945
}
946
947
// ============================================================================
948
// OBS-005: Observer (Main Interface)
949
// ============================================================================
950
951
/// Central observability interface
952
#[derive(Debug)]
953
pub struct Observer {
954
    /// Configuration
955
    config: ObservabilityConfig,
956
    /// Metrics buffer
957
    metrics_buffer: Arc<RwLock<Vec<MetricPoint>>>,
958
    /// Spans buffer
959
    spans_buffer: Arc<RwLock<Vec<Span>>>,
960
    /// A/B test results
961
    ab_results: Arc<RwLock<HashMap<String, ABTestResult>>>,
962
    /// Request counter
963
    request_counter: AtomicU64,
964
}
965
966
impl Observer {
967
    /// Create a new observer
968
    #[must_use]
969
7
    pub fn new(config: ObservabilityConfig) -> Self {
970
7
        Self {
971
7
            config,
972
7
            metrics_buffer: Arc::new(RwLock::new(Vec::new())),
973
7
            spans_buffer: Arc::new(RwLock::new(Vec::new())),
974
7
            ab_results: Arc::new(RwLock::new(HashMap::new())),
975
7
            request_counter: AtomicU64::new(0),
976
7
        }
977
7
    }
978
979
    /// Create with default configuration
980
    #[must_use]
981
5
    pub fn default_observer() -> Self {
982
5
        Self::new(ObservabilityConfig::new())
983
5
    }
984
985
    /// Record an inference metric
986
1
    pub fn record_inference(&self, model: &str, tokens: usize, latency_ms: u64) {
987
        // Tokens metric
988
1
        self.record_metric(
989
1
            MetricPoint::new("realizar_tokens_total", tokens as f64).with_label("model", model),
990
        );
991
992
        // Latency metric
993
1
        self.record_metric(
994
1
            MetricPoint::new("realizar_latency_ms", latency_ms as f64).with_label("model", model),
995
        );
996
997
        // Request count
998
1
        self.record_metric(
999
1
            MetricPoint::new("realizar_requests_total", 1.0).with_label("model", model),
1000
        );
1001
1
    }
1002
1003
    /// Record a custom metric
1004
4
    pub fn record_metric(&self, metric: MetricPoint) {
1005
4
        if let Ok(mut buffer) = self.metrics_buffer.write() {
1006
4
            buffer.push(metric);
1007
4
        
}0
1008
4
    }
1009
1010
    /// Create a new trace
1011
    #[must_use]
1012
3
    pub fn start_trace(&self, operation: &str) -> Span {
1013
3
        let trace_id = generate_id();
1014
3
        Span::new(operation, trace_id)
1015
3
    }
1016
1017
    /// Record a completed span
1018
3
    pub fn record_span(&self, span: Span) {
1019
3
        if !self.config.tracing_enabled {
1020
1
            return;
1021
2
        }
1022
1023
        // Sample based on rate
1024
2
        if self.config.trace_sample_rate < 1.0 {
1025
1
            let sample = simple_hash(&span.span_id) as f64 / u64::MAX as f64;
1026
1
            if sample > self.config.trace_sample_rate {
1027
1
                return;
1028
0
            }
1029
1
        }
1030
1031
1
        if let Ok(mut buffer) = self.spans_buffer.write() {
1032
1
            buffer.push(span);
1033
1
        
}0
1034
3
    }
1035
1036
    /// Record A/B test result
1037
3
    pub fn record_ab_result(
1038
3
        &self,
1039
3
        test_name: &str,
1040
3
        variant_name: &str,
1041
3
        success: bool,
1042
3
        latency_ms: u64,
1043
3
        tokens: u64,
1044
3
    ) {
1045
3
        if !self.config.ab_testing_enabled {
1046
0
            return;
1047
3
        }
1048
1049
3
        if let Ok(mut results) = self.ab_results.write() {
1050
3
            let test_result =
1051
3
                results
1052
3
                    .entry(test_name.to_string())
1053
3
                    .or_insert_with(|| ABTestResult {
1054
1
                        test_name: test_name.to_string(),
1055
1
                        variants: HashMap::new(),
1056
1
                    });
1057
1058
3
            let variant_result = test_result
1059
3
                .variants
1060
3
                .entry(variant_name.to_string())
1061
3
                .or_default();
1062
1063
3
            variant_result.requests += 1;
1064
3
            if success {
1065
2
                variant_result.successes += 1;
1066
2
            
}1
1067
3
            variant_result.total_latency_ms += latency_ms;
1068
3
            variant_result.total_tokens += tokens;
1069
0
        }
1070
3
    }
1071
1072
    /// Get A/B test results
1073
    #[must_use]
1074
1
    pub fn get_ab_results(&self, test_name: &str) -> Option<ABTestResult> {
1075
1
        self.ab_results
1076
1
            .read()
1077
1
            .ok()
1078
1
            .and_then(|r| r.get(test_name).cloned())
1079
1
    }
1080
1081
    /// Flush metrics to Trueno-DB (returns line protocol)
1082
1
    pub fn flush_metrics(&self) -> Vec<String> {
1083
1
        let metrics = if let Ok(mut buffer) = self.metrics_buffer.write() {
1084
1
            std::mem::take(&mut *buffer)
1085
        } else {
1086
0
            Vec::new()
1087
        };
1088
1089
1
        metrics.iter().map(MetricPoint::to_line_protocol).collect()
1090
1
    }
1091
1092
    /// Flush spans
1093
3
    pub fn flush_spans(&self) -> Vec<Span> {
1094
3
        if let Ok(mut buffer) = self.spans_buffer.write() {
1095
3
            std::mem::take(&mut *buffer)
1096
        } else {
1097
0
            Vec::new()
1098
        }
1099
3
    }
1100
1101
    /// Get next request ID
1102
3
    pub fn next_request_id(&self) -> u64 {
1103
3
        self.request_counter.fetch_add(1, Ordering::SeqCst)
1104
3
    }
1105
1106
    /// Generate Prometheus-format metrics
1107
    #[must_use]
1108
1
    pub fn prometheus_metrics(&self) -> String {
1109
        use std::fmt::Write;
1110
1
        let metrics = if let Ok(buffer) = self.metrics_buffer.read() {
1111
1
            buffer.clone()
1112
        } else {
1113
0
            Vec::new()
1114
        };
1115
1116
1
        let mut output = String::new();
1117
1
        let mut by_name: HashMap<String, Vec<&MetricPoint>> = HashMap::new();
1118
1119
2
        for 
metric1
in &metrics {
1120
1
            by_name.entry(metric.name.clone()).or_default().push(metric);
1121
1
        }
1122
1123
2
        for (
name1
,
points1
) in by_name {
1124
1
            let _ = writeln!(output, "# TYPE {name} gauge");
1125
2
            for 
point1
in points {
1126
1
                let labels = if point.labels.is_empty() {
1127
0
                    String::new()
1128
                } else {
1129
1
                    let pairs: Vec<String> = point
1130
1
                        .labels
1131
1
                        .iter()
1132
1
                        .map(|(k, v)| format!("{k}=\"{v}\""))
1133
1
                        .collect();
1134
1
                    format!("{{{}}}", pairs.join(","))
1135
                };
1136
1
                let _ = writeln!(output, "{name}{labels} {}", point.value);
1137
            }
1138
        }
1139
1140
1
        output
1141
1
    }
1142
}
1143
1144
impl Default for Observer {
1145
0
    fn default() -> Self {
1146
0
        Self::default_observer()
1147
0
    }
1148
}
1149
1150
// ============================================================================
1151
// OBS-006: Helper Functions
1152
// ============================================================================
1153
1154
/// Generate a unique ID (16 hex chars / 8 bytes for span ID)
1155
22
fn generate_id() -> String {
1156
    use std::{
1157
        collections::hash_map::RandomState,
1158
        hash::{BuildHasher, Hasher},
1159
    };
1160
1161
22
    let hasher_state = RandomState::new();
1162
22
    let mut hasher = hasher_state.build_hasher();
1163
22
    hasher.write_u64(
1164
22
        SystemTime::now()
1165
22
            .duration_since(UNIX_EPOCH)
1166
22
            .map(|d| d.as_nanos() as u64)
1167
22
            .unwrap_or(0),
1168
    );
1169
22
    format!("{:016x}", hasher.finish())
1170
22
}
1171
1172
/// Generate a trace ID (32 hex chars / 16 bytes per W3C spec)
1173
7
fn generate_trace_id() -> String {
1174
    use std::{
1175
        collections::hash_map::RandomState,
1176
        hash::{BuildHasher, Hasher},
1177
    };
1178
1179
7
    let hasher_state = RandomState::new();
1180
7
    let now = SystemTime::now()
1181
7
        .duration_since(UNIX_EPOCH)
1182
7
        .map(|d| d.as_nanos() as u64)
1183
7
        .unwrap_or(0);
1184
1185
7
    let mut hasher1 = hasher_state.build_hasher();
1186
7
    hasher1.write_u64(now);
1187
7
    let high = hasher1.finish();
1188
1189
7
    let mut hasher2 = hasher_state.build_hasher();
1190
7
    hasher2.write_u64(now.wrapping_add(1));
1191
7
    let low = hasher2.finish();
1192
1193
7
    format!("{high:016x}{low:016x}")
1194
7
}
1195
1196
/// Simple hash function for deterministic selection (FNV-1a variant)
1197
1.00k
fn simple_hash(input: &str) -> u64 {
1198
    const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
1199
    const FNV_PRIME: u64 = 0x0100_0000_01b3;
1200
1201
1.00k
    let mut hash: u64 = FNV_OFFSET;
1202
7.95k
    for byte in 
input1.00k
.
bytes1.00k
() {
1203
7.95k
        hash ^= u64::from(byte);
1204
7.95k
        hash = hash.wrapping_mul(FNV_PRIME);
1205
7.95k
    }
1206
1.00k
    hash
1207
1.00k
}
1208
1209
// ============================================================================
1210
// Tests
1211
// ============================================================================
1212
1213
#[cfg(test)]
1214
mod tests {
1215
    use super::*;
1216
1217
    #[test]
1218
1
    fn test_observability_config_default() {
1219
1
        let config = ObservabilityConfig::new();
1220
1
        assert!(config.tracing_enabled);
1221
1
        assert!((config.trace_sample_rate - 1.0).abs() < 0.001);
1222
1
    }
1223
1224
    #[test]
1225
1
    fn test_observability_config_builder() {
1226
1
        let config = ObservabilityConfig::new()
1227
1
            .with_trueno_db("trueno-db://localhost:5432")
1228
1
            .with_tracing(false)
1229
1
            .with_sample_rate(0.5);
1230
1231
1
        assert_eq!(
1232
            config.trueno_db_uri,
1233
1
            Some("trueno-db://localhost:5432".to_string())
1234
        );
1235
1
        assert!(!config.tracing_enabled);
1236
1
        assert!((config.trace_sample_rate - 0.5).abs() < 0.001);
1237
1
    }
1238
1239
    #[test]
1240
1
    fn test_metric_point() {
1241
1
        let metric = MetricPoint::new("test_metric", 42.0)
1242
1
            .with_label("model", "llama3")
1243
1
            .with_label("version", "1.0");
1244
1245
1
        assert_eq!(metric.name, "test_metric");
1246
1
        assert!((metric.value - 42.0).abs() < 0.001);
1247
1
        assert_eq!(metric.labels.get("model"), Some(&"llama3".to_string()));
1248
1
    }
1249
1250
    #[test]
1251
1
    fn test_metric_line_protocol() {
1252
1
        let metric = MetricPoint::new("cpu_usage", 75.5).with_label("host", "server1");
1253
1254
1
        let line = metric.to_line_protocol();
1255
1
        assert!(line.contains("cpu_usage"));
1256
1
        assert!(line.contains("host=server1"));
1257
1
        assert!(line.contains("75.5"));
1258
1
    }
1259
1260
    #[test]
1261
1
    fn test_span_creation() {
1262
1
        let span = Span::new("inference", "trace-123");
1263
1264
1
        assert!(!span.span_id.is_empty());
1265
1
        assert_eq!(span.trace_id, "trace-123");
1266
1
        assert_eq!(span.operation, "inference");
1267
1
        assert_eq!(span.status, SpanStatus::InProgress);
1268
1
    }
1269
1270
    #[test]
1271
1
    fn test_span_child() {
1272
1
        let parent = Span::new("request", "trace-456");
1273
1
        let child = parent.child("tokenize");
1274
1275
1
        assert_eq!(child.trace_id, parent.trace_id);
1276
1
        assert_eq!(child.parent_id, Some(parent.span_id.clone()));
1277
1
        assert_eq!(child.operation, "tokenize");
1278
1
    }
1279
1280
    #[test]
1281
1
    fn test_span_end_ok() {
1282
1
        let mut span = Span::new("test", "trace");
1283
1
        std::thread::sleep(Duration::from_millis(10));
1284
1
        span.end_ok();
1285
1286
1
        assert_eq!(span.status, SpanStatus::Ok);
1287
1
        assert!(span.duration_us.is_some());
1288
1
        assert!(span.duration_us.expect("test") >= 10000); // At least 10ms
1289
1
    }
1290
1291
    #[test]
1292
1
    fn test_span_end_error() {
1293
1
        let mut span = Span::new("test", "trace");
1294
1
        span.end_error("Something went wrong");
1295
1296
1
        assert_eq!(span.status, SpanStatus::Error);
1297
1
        assert_eq!(
1298
1
            span.attributes.get("error"),
1299
1
            Some(&"Something went wrong".to_string())
1300
        );
1301
1
    }
1302
1303
    #[test]
1304
1
    fn test_ab_test_creation() {
1305
1
        let test = ABTest::new("model-comparison")
1306
1
            .with_variant("control", "model-v1", 0.5)
1307
1
            .with_variant("treatment", "model-v2", 0.5);
1308
1309
1
        assert_eq!(test.name, "model-comparison");
1310
1
        assert_eq!(test.variants.len(), 2);
1311
1
        assert!(test.is_valid());
1312
1
    }
1313
1314
    #[test]
1315
1
    fn test_ab_test_selection_deterministic() {
1316
1
        let test = ABTest::new("test")
1317
1
            .with_variant("a", "model-a", 0.5)
1318
1
            .with_variant("b", "model-b", 0.5);
1319
1320
        // Same user should always get same variant
1321
1
        let variant1 = test.select("user-123");
1322
1
        let variant2 = test.select("user-123");
1323
1324
1
        assert_eq!(variant1.map(|v| &v.name), variant2.map(|v| &v.name));
1325
1
    }
1326
1327
    #[test]
1328
1
    fn test_ab_test_selection_distribution() {
1329
1
        let test = ABTest::new("test")
1330
1
            .with_variant("a", "model-a", 0.5)
1331
1
            .with_variant("b", "model-b", 0.5);
1332
1333
1
        let mut count_a = 0;
1334
1
        let mut count_b = 0;
1335
1336
1.00k
        for 
i1.00k
in 0..1000 {
1337
1.00k
            let user_id = format!("user-{i}");
1338
1.00k
            if let Some(variant) = test.select(&user_id) {
1339
1.00k
                if variant.name == "a" {
1340
490
                    count_a += 1;
1341
510
                } else {
1342
510
                    count_b += 1;
1343
510
                }
1344
0
            }
1345
        }
1346
1347
        // Should be roughly 50/50 (within 10% tolerance)
1348
1
        let ratio = count_a as f64 / (count_a + count_b) as f64;
1349
1
        assert!(ratio > 0.4 && ratio < 0.6);
1350
1
    }
1351
1352
    #[test]
1353
1
    fn test_ab_test_invalid_weights() {
1354
1
        let test = ABTest::new("test")
1355
1
            .with_variant("a", "model-a", 0.3)
1356
1
            .with_variant("b", "model-b", 0.3);
1357
1358
1
        assert!(!test.is_valid()); // Weights sum to 0.6, not 1.0
1359
1
    }
1360
1361
    #[test]
1362
1
    fn test_variant_result_calculations() {
1363
1
        let result = VariantResult {
1364
1
            requests: 100,
1365
1
            successes: 90,
1366
1
            total_latency_ms: 5000,
1367
1
            total_tokens: 10000,
1368
1
        };
1369
1370
1
        assert!((result.success_rate() - 0.9).abs() < 0.001);
1371
1
        assert!((result.avg_latency_ms() - 50.0).abs() < 0.001);
1372
1
        assert!((result.tokens_per_request() - 100.0).abs() < 0.001);
1373
1
    }
1374
1375
    #[test]
1376
1
    fn test_observer_record_inference() {
1377
1
        let observer = Observer::default_observer();
1378
1
        observer.record_inference("llama3", 100, 50);
1379
1380
1
        let metrics = observer.flush_metrics();
1381
1
        assert!(!metrics.is_empty());
1382
1
    }
1383
1384
    #[test]
1385
1
    fn test_observer_record_span() {
1386
1
        let observer = Observer::default_observer();
1387
1
        let mut span = observer.start_trace("test-op");
1388
1
        span.end_ok();
1389
1
        observer.record_span(span);
1390
1391
1
        let spans = observer.flush_spans();
1392
1
        assert_eq!(spans.len(), 1);
1393
1
        assert_eq!(spans[0].operation, "test-op");
1394
1
    }
1395
1396
    #[test]
1397
1
    fn test_observer_ab_results() {
1398
1
        let observer = Observer::default_observer();
1399
1400
1
        observer.record_ab_result("test", "control", true, 50, 100);
1401
1
        observer.record_ab_result("test", "control", true, 60, 120);
1402
1
        observer.record_ab_result("test", "treatment", false, 40, 80);
1403
1404
1
        let results = observer.get_ab_results("test").expect("test");
1405
1
        let control = results.variants.get("control").expect("test");
1406
1
        let treatment = results.variants.get("treatment").expect("test");
1407
1408
1
        assert_eq!(control.requests, 2);
1409
1
        assert_eq!(control.successes, 2);
1410
1
        assert_eq!(treatment.requests, 1);
1411
1
        assert_eq!(treatment.successes, 0);
1412
1
    }
1413
1414
    #[test]
1415
1
    fn test_observer_prometheus_format() {
1416
1
        let observer = Observer::default_observer();
1417
1
        observer.record_metric(MetricPoint::new("test_metric", 42.0).with_label("env", "prod"));
1418
1419
1
        let prom = observer.prometheus_metrics();
1420
1
        assert!(prom.contains("test_metric"));
1421
1
        assert!(prom.contains("env=\"prod\""));
1422
1
        assert!(prom.contains("42"));
1423
1
    }
1424
1425
    #[test]
1426
1
    fn test_observer_request_id() {
1427
1
        let observer = Observer::default_observer();
1428
1429
1
        let id1 = observer.next_request_id();
1430
1
        let id2 = observer.next_request_id();
1431
1
        let id3 = observer.next_request_id();
1432
1433
1
        assert_eq!(id1, 0);
1434
1
        assert_eq!(id2, 1);
1435
1
        assert_eq!(id3, 2);
1436
1
    }
1437
1438
    #[test]
1439
1
    fn test_span_status_default() {
1440
1
        let status = SpanStatus::default();
1441
1
        assert_eq!(status, SpanStatus::InProgress);
1442
1
    }
1443
1444
    #[test]
1445
1
    fn test_generate_id_unique() {
1446
1
        let id1 = generate_id();
1447
1
        let id2 = generate_id();
1448
1449
        // IDs should be unique (though not guaranteed with fast calls)
1450
1
        assert_eq!(id1.len(), 16);
1451
1
        assert_eq!(id2.len(), 16);
1452
1
    }
1453
1454
    #[test]
1455
1
    fn test_simple_hash_deterministic() {
1456
1
        let hash1 = simple_hash("test-input");
1457
1
        let hash2 = simple_hash("test-input");
1458
1
        let hash3 = simple_hash("different-input");
1459
1460
1
        assert_eq!(hash1, hash2);
1461
1
        assert_ne!(hash1, hash3);
1462
1
    }
1463
1464
    #[test]
1465
1
    fn test_observer_sampling() {
1466
1
        let config = ObservabilityConfig::new().with_sample_rate(0.0);
1467
1
        let observer = Observer::new(config);
1468
1469
1
        let mut span = observer.start_trace("test");
1470
1
        span.end_ok();
1471
1
        observer.record_span(span);
1472
1473
        // With 0% sampling, no spans should be recorded
1474
1
        let spans = observer.flush_spans();
1475
1
        assert!(spans.is_empty());
1476
1
    }
1477
1478
    #[test]
1479
1
    fn test_observer_tracing_disabled() {
1480
1
        let config = ObservabilityConfig::new().with_tracing(false);
1481
1
        let observer = Observer::new(config);
1482
1483
1
        let mut span = observer.start_trace("test");
1484
1
        span.end_ok();
1485
1
        observer.record_span(span);
1486
1487
1
        let spans = observer.flush_spans();
1488
1
        assert!(spans.is_empty());
1489
1
    }
1490
1491
    // =========================================================================
1492
    // W3C Trace Context Tests
1493
    // =========================================================================
1494
1495
    #[test]
1496
1
    fn test_trace_context_new() {
1497
1
        let ctx = TraceContext::new();
1498
1
        assert_eq!(ctx.trace_id.len(), 32);
1499
1
        assert!(ctx.parent_span_id.is_none());
1500
1
        assert_eq!(ctx.trace_flags, 0x01); // Sampled by default
1501
1
        assert!(ctx.trace_state.is_none());
1502
1
    }
1503
1504
    #[test]
1505
1
    fn test_trace_context_child() {
1506
1
        let parent_ctx = TraceContext::new();
1507
1
        let child_ctx = parent_ctx.child("abcdef0123456789");
1508
1509
1
        assert_eq!(child_ctx.trace_id, parent_ctx.trace_id);
1510
1
        assert_eq!(
1511
            child_ctx.parent_span_id,
1512
1
            Some("abcdef0123456789".to_string())
1513
        );
1514
1
        assert_eq!(child_ctx.trace_flags, parent_ctx.trace_flags);
1515
1
    }
1516
1517
    #[test]
1518
1
    fn test_trace_context_from_traceparent_valid() {
1519
1
        let header = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
1520
1
        let ctx = TraceContext::from_traceparent(header).expect("test");
1521
1522
1
        assert_eq!(ctx.trace_id, "0af7651916cd43dd8448eb211c80319c");
1523
1
        assert_eq!(ctx.parent_span_id, Some("b7ad6b7169203331".to_string()));
1524
1
        assert_eq!(ctx.trace_flags, 0x01);
1525
1
    }
1526
1527
    #[test]
1528
1
    fn test_trace_context_from_traceparent_not_sampled() {
1529
1
        let header = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00";
1530
1
        let ctx = TraceContext::from_traceparent(header).expect("test");
1531
1532
1
        assert_eq!(ctx.trace_flags, 0x00);
1533
1
        assert!(!ctx.is_sampled());
1534
1
    }
1535
1536
    #[test]
1537
1
    fn test_trace_context_from_traceparent_invalid_version() {
1538
1
        let header = "01-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
1539
1
        assert!(TraceContext::from_traceparent(header).is_none());
1540
1
    }
1541
1542
    #[test]
1543
1
    fn test_trace_context_from_traceparent_invalid_format() {
1544
1
        assert!(TraceContext::from_traceparent("invalid").is_none());
1545
1
        assert!(TraceContext::from_traceparent("00-abc-def-01").is_none());
1546
1
        assert!(TraceContext::from_traceparent("").is_none());
1547
1
    }
1548
1549
    #[test]
1550
1
    fn test_trace_context_to_traceparent() {
1551
1
        let ctx = TraceContext {
1552
1
            trace_id: "0af7651916cd43dd8448eb211c80319c".to_string(),
1553
1
            parent_span_id: None,
1554
1
            trace_flags: 0x01,
1555
1
            trace_state: None,
1556
1
        };
1557
1558
1
        let traceparent = ctx.to_traceparent("b7ad6b7169203331");
1559
1
        assert_eq!(
1560
            traceparent,
1561
            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1562
        );
1563
1
    }
1564
1565
    #[test]
1566
1
    fn test_trace_context_with_tracestate() {
1567
1
        let ctx = TraceContext::new().with_tracestate("vendor=value");
1568
1
        assert_eq!(ctx.trace_state, Some("vendor=value".to_string()));
1569
1
    }
1570
1571
    #[test]
1572
1
    fn test_trace_context_sampled_flag() {
1573
1
        let mut ctx = TraceContext::new();
1574
1
        assert!(ctx.is_sampled());
1575
1576
1
        ctx.set_sampled(false);
1577
1
        assert!(!ctx.is_sampled());
1578
1
        assert_eq!(ctx.trace_flags, 0x00);
1579
1580
1
        ctx.set_sampled(true);
1581
1
        assert!(ctx.is_sampled());
1582
1
        assert_eq!(ctx.trace_flags, 0x01);
1583
1
    }
1584
1585
    #[test]
1586
1
    fn test_trace_context_default() {
1587
1
        let ctx = TraceContext::default();
1588
1
        assert_eq!(ctx.trace_id.len(), 32);
1589
1
    }
1590
1591
    // =========================================================================
1592
    // Latency Histogram Tests
1593
    // =========================================================================
1594
1595
    #[test]
1596
1
    fn test_latency_histogram_new() {
1597
1
        let hist = LatencyHistogram::new();
1598
1
        assert_eq!(hist.count(), 0);
1599
1
        assert!(hist.min().is_none());
1600
1
        assert!(hist.max_val().is_none());
1601
1
        assert!(hist.mean().is_none());
1602
1
    }
1603
1604
    #[test]
1605
1
    fn test_latency_histogram_observe() {
1606
1
        let mut hist = LatencyHistogram::new();
1607
1
        hist.observe(1000); // 1ms
1608
1
        hist.observe(5000); // 5ms
1609
1
        hist.observe(10000); // 10ms
1610
1611
1
        assert_eq!(hist.count(), 3);
1612
1
        assert_eq!(hist.min(), Some(1000));
1613
1
        assert_eq!(hist.max_val(), Some(10000));
1614
1
    }
1615
1616
    #[test]
1617
1
    fn test_latency_histogram_mean() {
1618
1
        let mut hist = LatencyHistogram::new();
1619
1
        hist.observe(1000);
1620
1
        hist.observe(2000);
1621
1
        hist.observe(3000);
1622
1623
1
        let mean = hist.mean().expect("test");
1624
1
        assert!((mean - 2000.0).abs() < 0.001);
1625
1
    }
1626
1627
    #[test]
1628
1
    fn test_latency_histogram_observe_duration() {
1629
1
        let mut hist = LatencyHistogram::new();
1630
1
        hist.observe_duration(Duration::from_millis(5));
1631
1632
1
        assert_eq!(hist.count(), 1);
1633
1
        assert_eq!(hist.min(), Some(5000)); // 5ms = 5000us
1634
1
    }
1635
1636
    #[test]
1637
1
    fn test_latency_histogram_percentiles() {
1638
1
        let mut hist = LatencyHistogram::new();
1639
        // Add 100 observations: 1ms, 2ms, 3ms, ... 100ms
1640
101
        for 
i100
in 1..=100 {
1641
100
            hist.observe(i * 1000);
1642
100
        }
1643
1644
        // p50 should be around 50ms
1645
1
        let p50 = hist.p50().expect("test");
1646
1
        assert!(p50 >= 25_000 && p50 <= 100_000);
1647
1648
        // p95 should be around 95ms
1649
1
        let p95 = hist.p95().expect("test");
1650
1
        assert!(p95 >= 50000);
1651
1652
        // p99 should be around 99ms
1653
1
        let p99 = hist.p99().expect("test");
1654
1
        assert!(p99 >= 50000);
1655
1
    }
1656
1657
    #[test]
1658
1
    fn test_latency_histogram_percentile_empty() {
1659
1
        let hist = LatencyHistogram::new();
1660
1
        assert!(hist.percentile(50.0).is_none());
1661
1
        assert!(hist.p50().is_none());
1662
1
        assert!(hist.p95().is_none());
1663
1
        assert!(hist.p99().is_none());
1664
1
    }
1665
1666
    #[test]
1667
1
    fn test_latency_histogram_percentile_invalid() {
1668
1
        let mut hist = LatencyHistogram::new();
1669
1
        hist.observe(1000);
1670
1671
1
        assert!(hist.percentile(-1.0).is_none());
1672
1
        assert!(hist.percentile(101.0).is_none());
1673
1
    }
1674
1675
    #[test]
1676
1
    fn test_latency_histogram_custom_buckets() {
1677
1
        let buckets = vec![100, 500, 1000, 5000, 10000];
1678
1
        let mut hist = LatencyHistogram::with_buckets(buckets);
1679
1680
1
        hist.observe(50); // bucket 0 (<=100)
1681
1
        hist.observe(200); // bucket 1 (<=500)
1682
1
        hist.observe(750); // bucket 2 (<=1000)
1683
1
        hist.observe(20000); // overflow bucket
1684
1685
1
        assert_eq!(hist.count(), 4);
1686
1
        assert_eq!(hist.min(), Some(50));
1687
1
        assert_eq!(hist.max_val(), Some(20000));
1688
1
    }
1689
1690
    #[test]
1691
1
    fn test_latency_histogram_to_prometheus() {
1692
1
        let mut hist = LatencyHistogram::new();
1693
1
        hist.observe(1000); // 1ms
1694
1
        hist.observe(50000); // 50ms
1695
1696
1
        let prom = hist.to_prometheus("request_latency", "service=\"api\"");
1697
1698
1
        assert!(prom.contains("request_latency_bucket"));
1699
1
        assert!(prom.contains("le="));
1700
1
        assert!(prom.contains("service=\"api\""));
1701
1
        assert!(prom.contains("request_latency_sum"));
1702
1
        assert!(prom.contains("request_latency_count"));
1703
1
        assert!(prom.contains("} 2")); // count = 2
1704
1
    }
1705
1706
    // =========================================================================
1707
    // OpenTelemetry Export Tests
1708
    // =========================================================================
1709
1710
    #[test]
1711
1
    fn test_span_to_otel_ok() {
1712
1
        let mut span = Span::new("test-op", "trace123456789012345678901234");
1713
1
        span.end_ok();
1714
1715
1
        let otel = span.to_otel();
1716
1717
1
        assert_eq!(otel.trace_id, "trace123456789012345678901234");
1718
1
        assert_eq!(otel.operation_name, "test-op");
1719
1
        assert_eq!(otel.service_name, "realizar");
1720
1
        assert_eq!(otel.status.code, OtelStatusCode::Ok);
1721
1
        assert!(otel.status.message.is_none());
1722
1
    }
1723
1724
    #[test]
1725
1
    fn test_span_to_otel_error() {
1726
1
        let mut span = Span::new("failing-op", "trace123456789012345678901234");
1727
1
        span.end_error("Connection timeout");
1728
1729
1
        let otel = span.to_otel();
1730
1731
1
        assert_eq!(otel.status.code, OtelStatusCode::Error);
1732
1
        assert_eq!(otel.status.message, Some("Connection timeout".to_string()));
1733
1
    }
1734
1735
    #[test]
1736
1
    fn test_span_to_otel_with_parent() {
1737
1
        let parent = Span::new("parent-op", "trace123456789012345678901234");
1738
1
        let mut child = parent.child("child-op");
1739
1
        child.end_ok();
1740
1741
1
        let otel = child.to_otel();
1742
1743
1
        assert_eq!(otel.parent_span_id, Some(parent.span_id.clone()));
1744
1
    }
1745
1746
    #[test]
1747
1
    fn test_span_to_otel_with_attributes() {
1748
1
        let mut span = Span::new("test-op", "trace123456789012345678901234")
1749
1
            .with_attribute("model", "llama3")
1750
1
            .with_attribute("tokens", "256");
1751
1
        span.end_ok();
1752
1753
1
        let otel = span.to_otel();
1754
1755
1
        assert!(otel.attributes.iter().any(|a| a.key == "model"));
1756
2
        
assert!1
(
otel.attributes.iter()1
.
any1
(|a| a.key == "tokens"));
1757
1
    }
1758
1759
    #[test]
1760
1
    fn test_span_to_otel_with_kind() {
1761
1
        let mut span =
1762
1
            Span::new("server-op", "trace123456789012345678901234").with_kind(SpanKind::Server);
1763
1
        span.end_ok();
1764
1765
1
        let otel = span.to_otel();
1766
1
        assert_eq!(otel.kind, SpanKind::Server);
1767
1
    }
1768
1769
    #[test]
1770
1
    fn test_span_to_otel_timestamps() {
1771
1
        let mut span = Span::new("test-op", "trace123456789012345678901234");
1772
1
        std::thread::sleep(Duration::from_millis(5));
1773
1
        span.end_ok();
1774
1775
1
        let otel = span.to_otel();
1776
1777
        // End time should be >= start time
1778
1
        assert!(otel.end_time >= otel.start_time);
1779
        // Timestamps should be in nanoseconds
1780
1
        assert!(otel.start_time > 0);
1781
1
    }
1782
1783
    #[test]
1784
1
    fn test_span_trace_context() {
1785
1
        let span = Span::new("test-op", "0af7651916cd43dd8448eb211c80319c");
1786
1
        let ctx = span.trace_context();
1787
1788
1
        assert_eq!(ctx.trace_id, "0af7651916cd43dd8448eb211c80319c");
1789
1
        assert_eq!(ctx.parent_span_id, Some(span.span_id.clone()));
1790
1
        assert_eq!(ctx.trace_flags, 0x01);
1791
1
    }
1792
1793
    #[test]
1794
1
    fn test_span_traceparent() {
1795
1
        let span = Span::new("test-op", "0af7651916cd43dd8448eb211c80319c");
1796
1
        let traceparent = span.traceparent();
1797
1798
1
        assert!(traceparent.starts_with("00-"));
1799
1
        assert!(traceparent.contains("0af7651916cd43dd8448eb211c80319c"));
1800
1
        assert!(traceparent.ends_with("-01"));
1801
1802
        // Should have format: 00-{trace_id}-{span_id}-01
1803
1
        let parts: Vec<&str> = traceparent.split('-').collect();
1804
1
        assert_eq!(parts.len(), 4);
1805
1
        assert_eq!(parts[0], "00");
1806
1
        assert_eq!(parts[1], "0af7651916cd43dd8448eb211c80319c");
1807
1
        assert_eq!(parts[3], "01");
1808
1
    }
1809
1810
    // =========================================================================
1811
    // OtelValue Tests
1812
    // =========================================================================
1813
1814
    #[test]
1815
1
    fn test_otel_value_from_str() {
1816
1
        let val: OtelValue = "test".into();
1817
1
        match val {
1818
1
            OtelValue::String { string_value } => assert_eq!(string_value, "test"),
1819
0
            _ => panic!("Expected String variant"),
1820
        }
1821
1
    }
1822
1823
    #[test]
1824
1
    fn test_otel_value_from_string() {
1825
1
        let val: OtelValue = String::from("test").into();
1826
1
        match val {
1827
1
            OtelValue::String { string_value } => assert_eq!(string_value, "test"),
1828
0
            _ => panic!("Expected String variant"),
1829
        }
1830
1
    }
1831
1832
    #[test]
1833
1
    fn test_otel_value_from_i64() {
1834
1
        let val: OtelValue = 42i64.into();
1835
1
        match val {
1836
1
            OtelValue::Int { int_value } => assert_eq!(int_value, 42),
1837
0
            _ => panic!("Expected Int variant"),
1838
        }
1839
1
    }
1840
1841
    #[test]
1842
1
    fn test_otel_value_from_f64() {
1843
1
        let val: OtelValue = 3.14f64.into();
1844
1
        match val {
1845
1
            OtelValue::Float { double_value } => assert!((double_value - 3.14).abs() < 0.001),
1846
0
            _ => panic!("Expected Float variant"),
1847
        }
1848
1
    }
1849
1850
    #[test]
1851
1
    fn test_otel_value_from_bool() {
1852
1
        let val: OtelValue = true.into();
1853
1
        match val {
1854
1
            OtelValue::Bool { bool_value } => assert!(bool_value),
1855
0
            _ => panic!("Expected Bool variant"),
1856
        }
1857
1
    }
1858
1859
    // =========================================================================
1860
    // SpanKind Tests
1861
    // =========================================================================
1862
1863
    #[test]
1864
1
    fn test_span_kind_default() {
1865
1
        let kind = SpanKind::default();
1866
1
        assert_eq!(kind, SpanKind::Internal);
1867
1
    }
1868
1869
    #[test]
1870
1
    fn test_otel_status_code_default() {
1871
1
        let code = OtelStatusCode::default();
1872
1
        assert_eq!(code, OtelStatusCode::Unset);
1873
1
    }
1874
1875
    // =========================================================================
1876
    // Generate Trace ID Tests
1877
    // =========================================================================
1878
1879
    #[test]
1880
1
    fn test_generate_trace_id_length() {
1881
1
        let id = generate_trace_id();
1882
1
        assert_eq!(id.len(), 32); // 16 bytes = 32 hex chars
1883
1
    }
1884
1885
    #[test]
1886
1
    fn test_generate_trace_id_hex() {
1887
1
        let id = generate_trace_id();
1888
32
        
assert!1
(
id.chars()1
.
all1
(|c| c.is_ascii_hexdigit()));
1889
1
    }
1890
}