## 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
    }