Skip to main content

sz_orm_tracing/
lib.rs

1//! # SZ-ORM Tracing — Distributed tracing
2//!
3//! Provides Span/Tracer abstractions for distributed tracing, with OTLP exporter
4//! reporting support, for collection and visualization of cross-service call chains.
5//!
6//! ## Main types
7//!
8//! - [`Span`] — single trace segment
9//! - `Tracer` — tracer and exporter
10//!
11//! ## Sampling and context propagation (`sampling` module)
12//!
13//! - [`sampling::Sampler`] / [`sampling::TraceIdRatioSampler`] — sampling strategies
14//! - [`sampling::Baggage`] / [`sampling::BaggagePropagator`] — W3C Baggage propagation
15//! - [`sampling::BatchSpanExporter`] — batch Span export
16
17pub mod sampling;
18
19use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::sync::{Arc, RwLock};
23use std::time::Duration;
24
25pub use sampling::{
26    AlwaysOffSampler, AlwaysOnSampler, Baggage, BaggagePropagator, BatchConfig, BatchSpanExporter,
27    ParentBasedSampler, Sampler, SamplingDecision, TraceIdRatioSampler,
28};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Span {
32    pub trace_id: String,
33    pub span_id: String,
34    pub parent_id: Option<String>,
35    pub operation_name: String,
36    pub service_name: String,
37    pub start_time: i64,
38    pub end_time: Option<i64>,
39    pub tags: HashMap<String, String>,
40    pub logs: Vec<SpanLog>,
41}
42
43impl Span {
44    pub fn new(
45        trace_id: impl Into<String>,
46        span_id: impl Into<String>,
47        operation_name: impl Into<String>,
48    ) -> Self {
49        Self {
50            trace_id: trace_id.into(),
51            span_id: span_id.into(),
52            parent_id: None,
53            operation_name: operation_name.into(),
54            service_name: String::new(),
55            start_time: current_timestamp(),
56            end_time: None,
57            tags: HashMap::new(),
58            logs: Vec::new(),
59        }
60    }
61
62    pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
63        self.parent_id = Some(parent_id.into());
64        self
65    }
66
67    pub fn with_service(mut self, service_name: impl Into<String>) -> Self {
68        self.service_name = service_name.into();
69        self
70    }
71
72    pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
73        self.tags.insert(key.into(), value.into());
74        self
75    }
76
77    pub fn finish(&mut self) {
78        self.end_time = Some(current_timestamp());
79    }
80
81    pub fn duration(&self) -> Option<i64> {
82        self.end_time.map(|end| end - self.start_time)
83    }
84
85    pub fn add_log(&mut self, message: impl Into<String>) {
86        self.logs.push(SpanLog {
87            timestamp: current_timestamp(),
88            message: message.into(),
89            fields: HashMap::new(),
90        });
91    }
92
93    pub fn trace_id(&self) -> &str {
94        &self.trace_id
95    }
96
97    pub fn span_id(&self) -> &str {
98        &self.span_id
99    }
100
101    pub fn parent_id(&self) -> Option<&str> {
102        self.parent_id.as_deref()
103    }
104
105    pub fn operation_name(&self) -> &str {
106        &self.operation_name
107    }
108
109    pub fn service_name(&self) -> &str {
110        &self.service_name
111    }
112
113    pub fn tags(&self) -> &HashMap<String, String> {
114        &self.tags
115    }
116
117    pub fn logs(&self) -> &[SpanLog] {
118        &self.logs
119    }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SpanLog {
124    pub timestamp: i64,
125    pub message: String,
126    pub fields: HashMap<String, String>,
127}
128
129pub trait Tracer: Send + Sync {
130    fn start_span(&self, operation_name: &str) -> Span;
131    fn end_span(&self, span: Span);
132    fn inject(&self, span: &Span) -> HashMap<String, String>;
133    fn extract(&self, headers: &HashMap<String, String>) -> Option<Span>;
134}
135
136pub struct SzTracer {
137    spans: Arc<RwLock<Vec<Span>>>,
138    service_name: String,
139}
140
141impl SzTracer {
142    pub fn new(service_name: impl Into<String>) -> Self {
143        Self {
144            spans: Arc::new(RwLock::new(Vec::new())),
145            service_name: service_name.into(),
146        }
147    }
148
149    pub fn generate_trace_id() -> String {
150        format!("{:032x}", rand_u64())
151    }
152
153    pub fn generate_span_id() -> String {
154        format!("{:016x}", rand_u64())
155    }
156
157    pub fn get_spans(&self) -> Vec<Span> {
158        self.spans
159            .read()
160            .map_err(|e| TracingError::Internal(e.to_string()))
161            .unwrap()
162            .clone()
163    }
164
165    pub fn clear(&self) {
166        self.spans
167            .write()
168            .map_err(|e| TracingError::Internal(e.to_string()))
169            .unwrap()
170            .clear();
171    }
172}
173
174impl Default for SzTracer {
175    fn default() -> Self {
176        Self::new("unknown")
177    }
178}
179
180impl Tracer for SzTracer {
181    fn start_span(&self, operation_name: &str) -> Span {
182        Span::new(
183            Self::generate_trace_id(),
184            Self::generate_span_id(),
185            operation_name,
186        )
187        .with_service(&self.service_name)
188    }
189
190    fn end_span(&self, mut span: Span) {
191        span.finish();
192
193        if let Ok(mut spans) = self.spans.write() {
194            spans.push(span);
195        }
196    }
197
198    /// Injects the W3C TraceContext `traceparent` header
199    ///
200    /// v0.2.2 fixes P2-2: upgrades from custom `trace-id`/`span-id`/`parent-span-id`
201    /// headers to the W3C TraceContext standard `traceparent` header.
202    ///
203    /// Format: `00-<trace_id>-<span_id>-<trace_flags>`
204    /// - `00`: version number (fixed to `00` by the W3C specification)
205    /// - `trace_id`: 32-char hex (16 bytes)
206    /// - `span_id`: 16-char hex (8 bytes)
207    /// - `trace_flags`: 2-char hex (`01` means sampled, `00` means not sampled)
208    ///
209    /// The `parent-span-id` header is retained for backward compatibility with older extract.
210    fn inject(&self, span: &Span) -> HashMap<String, String> {
211        let mut headers = HashMap::new();
212
213        // W3C TraceContext: traceparent = version-trace_id-span_id-flags
214        let traceparent = format!("00-{}-{}-01", span.trace_id, span.span_id);
215        headers.insert("traceparent".to_string(), traceparent);
216
217        if let Some(ref parent_id) = span.parent_id {
218            headers.insert("parent-span-id".to_string(), parent_id.clone());
219        }
220
221        headers
222    }
223
224    /// Extracts a Span from headers
225    ///
226    /// v0.2.2 fixes P2-2: prioritizes parsing the W3C TraceContext `traceparent` header;
227    /// if it does not exist, falls back to legacy custom headers (`trace-id`/`span-id`).
228    ///
229    /// W3C `traceparent` format: `00-<trace_id>-<span_id>-<trace_flags>`
230    /// - The version number must be `00`
231    /// - trace_id must be 32-char hex
232    /// - span_id must be 16-char hex
233    /// - trace_flags must be 2-char hex
234    fn extract(&self, headers: &HashMap<String, String>) -> Option<Span> {
235        // 优先尝试 W3C traceparent
236        if let Some(traceparent) = headers.get("traceparent") {
237            if let Some(span) = Self::parse_traceparent(traceparent) {
238                let mut span = span.with_service(&self.service_name);
239
240                // 同时检查 legacy parent-span-id header
241                if let Some(parent_id) = headers.get("parent-span-id") {
242                    span = span.with_parent(parent_id.clone());
243                }
244
245                return Some(span);
246            }
247        }
248
249        // 回退到 legacy header(向后兼容)
250        let trace_id = headers.get("trace-id")?;
251        let span_id = headers.get("span-id")?;
252
253        let mut span = Span::new(trace_id.clone(), span_id.clone(), "extracted");
254
255        if let Some(parent_id) = headers.get("parent-span-id") {
256            span = span.with_parent(parent_id.clone());
257        }
258
259        span = span.with_service(&self.service_name);
260
261        Some(span)
262    }
263}
264
265impl SzTracer {
266    /// Parses the W3C TraceContext `traceparent` header
267    ///
268    /// Format: `00-<trace_id>-<span_id>-<trace_flags>`
269    /// - The version number must be `00`
270    /// - trace_id must be 32-char hex (not all zeros)
271    /// - span_id must be 16-char hex (not all zeros)
272    /// - trace_flags must be 2-char hex
273    ///
274    /// Returns `Some(Span)` on successful parsing, `None` if the format is invalid.
275    fn parse_traceparent(traceparent: &str) -> Option<Span> {
276        let parts: Vec<&str> = traceparent.split('-').collect();
277        if parts.len() != 4 {
278            return None;
279        }
280
281        let version = parts[0];
282        let trace_id = parts[1];
283        let span_id = parts[2];
284        let trace_flags = parts[3];
285
286        // 版本号必须是 2 字符 hex,且 W3C 规范当前固定为 "00"
287        if version.len() != 2 || !version.chars().all(|c| c.is_ascii_hexdigit()) {
288            return None;
289        }
290
291        // trace_id 必须是 32 字符 hex,且不能全为 0
292        if trace_id.len() != 32
293            || !trace_id.chars().all(|c| c.is_ascii_hexdigit())
294            || trace_id.chars().all(|c| c == '0')
295        {
296            return None;
297        }
298
299        // span_id 必须是 16 字符 hex,且不能全为 0
300        if span_id.len() != 16
301            || !span_id.chars().all(|c| c.is_ascii_hexdigit())
302            || span_id.chars().all(|c| c == '0')
303        {
304            return None;
305        }
306
307        // trace_flags 必须是 2 字符 hex
308        if trace_flags.len() != 2 || !trace_flags.chars().all(|c| c.is_ascii_hexdigit()) {
309            return None;
310        }
311
312        Some(Span::new(
313            trace_id.to_string(),
314            span_id.to_string(),
315            "extracted",
316        ))
317    }
318
319    /// Injects legacy custom headers (backward compatibility)
320    ///
321    /// v0.2.2 fixes P2-2: retains the old header format for compatibility with old clients.
322    /// New code should use [`Tracer::inject`] (W3C TraceContext).
323    pub fn inject_legacy(&self, span: &Span) -> HashMap<String, String> {
324        let mut headers = HashMap::new();
325        headers.insert("trace-id".to_string(), span.trace_id.to_string());
326        headers.insert("span-id".to_string(), span.span_id.to_string());
327
328        if let Some(ref parent_id) = span.parent_id {
329            headers.insert("parent-span-id".to_string(), parent_id.clone());
330        }
331
332        headers
333    }
334
335    /// Extracts a Span only from legacy headers (backward compatibility)
336    ///
337    /// v0.2.2 fixes P2-2: retains the old header parsing for compatibility with old clients.
338    /// New code should use [`Tracer::extract`] (automatically prioritizes W3C, falls back to legacy).
339    pub fn extract_legacy(&self, headers: &HashMap<String, String>) -> Option<Span> {
340        let trace_id = headers.get("trace-id")?;
341        let span_id = headers.get("span-id")?;
342
343        let mut span = Span::new(trace_id.clone(), span_id.clone(), "extracted");
344
345        if let Some(parent_id) = headers.get("parent-span-id") {
346            span = span.with_parent(parent_id.clone());
347        }
348
349        span = span.with_service(&self.service_name);
350
351        Some(span)
352    }
353}
354
355/// A compatibility wrapper that exposes the same [`Tracer`] interface as the
356/// OpenTelemetry SDK but is implemented internally by delegating every call
357/// to a [`SzTracer`].
358///
359/// # What this is (and is not)
360///
361/// This type exists so that downstream code can be written against an
362/// "otel-style" tracer name without pulling in the real `opentelemetry`
363/// crate as a dependency. It is **not** a real OpenTelemetry implementation:
364///
365/// - It does **not** export spans to an OTLP / Jaeger / Zipkin collector.
366/// - v0.2.2 fixes P2-2: **now implements W3C TraceContext `traceparent` header propagation**
367///   (`00-<trace_id>-<span_id>-<trace_flags>`), while retaining the `parent-span-id`
368///   header to convey the parent span relationship. `extract` prioritizes W3C parsing
369///   and falls back to legacy.
370/// - It does **not** perform sampling, baggage propagation, or context
371///   extraction across `async` boundaries.
372/// - Span IDs are generated from `std::collections::hash_map::RandomState`
373///   rather than per the OpenTelemetry specification (16 hex chars / 8 bytes
374///   random).
375///
376/// # When to use which
377///
378/// - Use [`SzTracer`] directly for in-process span collection and W3C
379///   TraceContext header propagation.
380/// - Use `OtelTracer` when an existing code base expects a tracer whose name
381///   signals OpenTelemetry-compatibility, but you have already decided to back
382///   it with SzTracer semantics.
383/// - For production distributed tracing across service boundaries, depend on
384///   the real `opentelemetry` SDK and use its `Tracer` implementation instead.
385///
386/// Both [`SzTracer`] and `OtelTracer` satisfy the [`Tracer`] trait, so they
387/// can be swapped at the type level without changing call sites.
388pub struct OtelTracer {
389    tracer: SzTracer,
390}
391
392impl OtelTracer {
393    /// Wraps a freshly-built [`SzTracer`] tagged with `service_name`.
394    pub fn new(service_name: impl Into<String>) -> Self {
395        Self {
396            tracer: SzTracer::new(service_name),
397        }
398    }
399
400    /// Provides read access to the underlying [`SzTracer`] so callers can
401    /// inspect accumulated spans or clear them between tests.
402    pub fn inner(&self) -> &SzTracer {
403        &self.tracer
404    }
405}
406
407impl Tracer for OtelTracer {
408    fn start_span(&self, operation_name: &str) -> Span {
409        self.tracer.start_span(operation_name)
410    }
411
412    fn end_span(&self, span: Span) {
413        self.tracer.end_span(span)
414    }
415
416    fn inject(&self, span: &Span) -> HashMap<String, String> {
417        self.tracer.inject(span)
418    }
419
420    fn extract(&self, headers: &HashMap<String, String>) -> Option<Span> {
421        self.tracer.extract(headers)
422    }
423}
424
425fn current_timestamp() -> i64 {
426    use std::time::{SystemTime, UNIX_EPOCH};
427    SystemTime::now()
428        .duration_since(UNIX_EPOCH)
429        .unwrap_or_default()
430        .as_millis() as i64
431}
432
433fn rand_u64() -> u64 {
434    use std::collections::hash_map::RandomState;
435    use std::hash::{BuildHasher, Hasher};
436    RandomState::new().build_hasher().finish()
437}
438
439#[derive(Debug)]
440pub enum TracingError {
441    SpanNotFound(String),
442    InvalidTraceId(String),
443    Internal(String),
444    /// OTLP exporter initialization failed (feature = "otlp")
445    #[cfg(feature = "otlp")]
446    OtlpInitFailed(String),
447}
448
449impl std::fmt::Display for TracingError {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        match self {
452            TracingError::SpanNotFound(id) => write!(f, "Span not found: {}", id),
453            TracingError::InvalidTraceId(id) => write!(f, "Invalid trace id: {}", id),
454            TracingError::Internal(msg) => write!(f, "Tracing internal error: {}", msg),
455            #[cfg(feature = "otlp")]
456            TracingError::OtlpInitFailed(msg) => write!(f, "OTLP init failed: {}", msg),
457        }
458    }
459}
460
461impl std::error::Error for TracingError {}
462
463impl serde::Serialize for TracingError {
464    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
465    where
466        S: serde::Serializer,
467    {
468        serializer.serialize_str(&self.to_string())
469    }
470}
471
472// ===================== L4 SLA Monitoring =====================
473
474/// Latency percentile histogram.
475///
476/// Implemented with a sorted array, records all observed latency samples and supports
477/// arbitrary percentile queries. Suitable for financial-grade SLA monitoring scenarios
478/// where the sample size is controllable; for very large-scale samples, consider
479/// approximate algorithms such as T-Digest to reduce memory footprint.
480#[derive(Debug, Clone)]
481pub struct LatencyHistogram {
482    samples: Vec<Duration>,
483    sum: Duration,
484}
485
486impl LatencyHistogram {
487    pub fn new(_buckets: Vec<Duration>) -> Self {
488        Self {
489            samples: Vec::new(),
490            sum: Duration::ZERO,
491        }
492    }
493
494    pub fn record(&mut self, duration: Duration) {
495        self.sum += duration;
496        let pos = self.samples.partition_point(|d| *d < duration);
497        self.samples.insert(pos, duration);
498    }
499
500    pub fn percentile(&self, p: f64) -> Option<Duration> {
501        if !(0.0..=100.0).contains(&p) || self.samples.is_empty() {
502            return None;
503        }
504        let n = self.samples.len();
505        // 最近排名法(Nearest Rank):rank = ceil(p/100 * n),至少为 1。
506        // 这是 SLA 监控的标准方法(Google SRE 推荐用法),保证
507        // p=0 返回最小值,p=100 返回最大值,且对高分位数偏保守。
508        let rank = ((p / 100.0) * n as f64).ceil() as usize;
509        let rank = rank.max(1).min(n);
510        Some(self.samples[rank - 1])
511    }
512
513    pub fn count(&self) -> usize {
514        self.samples.len()
515    }
516
517    pub fn mean(&self) -> Option<Duration> {
518        if self.samples.is_empty() {
519            None
520        } else {
521            Some(self.sum / self.samples.len() as u32)
522        }
523    }
524}
525
526/// Error rate counter, based on a sliding time window for error rate statistics.
527///
528/// Samples outside the window are evicted on the next [`record`](Self::record) call,
529/// ensuring `rate()` is always computed from the latest samples within the window.
530#[derive(Debug)]
531pub struct ErrorRateCounter {
532    window: Duration,
533    samples: Vec<(std::time::Instant, bool)>,
534}
535
536impl ErrorRateCounter {
537    pub fn new(window: Duration) -> Self {
538        Self {
539            window,
540            samples: Vec::new(),
541        }
542    }
543
544    pub fn record(&mut self, success: bool) {
545        let now = std::time::Instant::now();
546        let cutoff = now - self.window;
547        // 驱逐窗口外的样本(保留 cutoff 之后到达的样本)。
548        self.samples.retain(|(ts, _)| *ts >= cutoff);
549        self.samples.push((now, success));
550    }
551
552    pub fn rate(&self) -> f64 {
553        if self.samples.is_empty() {
554            return 0.0;
555        }
556        let errors = self.samples.iter().filter(|(_, ok)| !ok).count() as f64;
557        errors / self.samples.len() as f64
558    }
559
560    pub fn total(&self) -> usize {
561        self.samples.len()
562    }
563
564    pub fn errors(&self) -> usize {
565        self.samples.iter().filter(|(_, ok)| !ok).count()
566    }
567}
568
569/// Error budget, based on the SLO target and a sliding time window.
570///
571/// Core model: each error consumes `(1 - slo_target)` units of budget, and the budget
572/// capacity is 1.0. Therefore the number of errors required to exhaust the budget is
573/// `1 / (1 - slo_target)` (e.g., 1000 errors when SLO=0.999). This model assumes the
574/// baseline traffic within the window is `1/(1-SLO)` requests; it is a simplified model
575/// commonly used in financial-grade SLA monitoring, suitable for error budget tracking
576/// independent of traffic statistics.
577///
578/// `consume` calls outside the window are evicted on the next record.
579#[derive(Debug)]
580pub struct ErrorBudget {
581    slo_target: f64,
582    window: Duration,
583    samples: Vec<(std::time::Instant, usize)>,
584}
585
586impl ErrorBudget {
587    pub fn new(slo_target: f64, window: Duration) -> Self {
588        Self {
589            slo_target,
590            window,
591            samples: Vec::new(),
592        }
593    }
594
595    pub fn consume(&mut self, error_count: usize) {
596        let now = std::time::Instant::now();
597        let cutoff = now - self.window;
598        self.samples.retain(|(ts, _)| *ts >= cutoff);
599        if error_count > 0 {
600            self.samples.push((now, error_count));
601        }
602    }
603
604    fn total_errors_in_window(&self) -> usize {
605        let now = std::time::Instant::now();
606        let cutoff = now - self.window;
607        self.samples
608            .iter()
609            .filter(|(ts, _)| *ts >= cutoff)
610            .map(|(_, n)| *n)
611            .sum()
612    }
613
614    pub fn remaining(&self) -> f64 {
615        let total_errors = self.total_errors_in_window();
616        if total_errors == 0 {
617            return 1.0;
618        }
619        let error_budget = 1.0 - self.slo_target;
620        if error_budget <= 0.0 {
621            // SLO = 1.0 意味着不允许任何错误,有错误即耗尽。
622            return 0.0;
623        }
624        let consumed = total_errors as f64 * error_budget;
625        (1.0 - consumed).clamp(0.0, 1.0)
626    }
627
628    pub fn is_exhausted(&self) -> bool {
629        self.remaining() == 0.0
630    }
631}
632
633/// Alert level.
634#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
635pub enum AlertLevel {
636    Info,
637    Warning,
638    Critical,
639}
640
641/// Alert event, produced by [`SaturationGauge`] / [`SlaMonitor`] and other components
642/// when an anomaly is detected, and dispatched downstream (logs, webhooks, etc.) via [`AlertHook`].
643#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
644pub struct Alert {
645    pub level: AlertLevel,
646    pub message: String,
647    pub timestamp: DateTime<Utc>,
648    pub operation: Option<String>,
649}
650
651/// Saturation alert gauge.
652///
653/// Tracks a single saturation value (conventionally in the range `[0.0, 1.0]`); when the
654/// value `>= threshold`, it is considered saturated and triggers an [`AlertLevel::Critical`] alert.
655#[derive(Debug)]
656pub struct SaturationGauge {
657    threshold: f64,
658    value: f64,
659}
660
661impl SaturationGauge {
662    pub fn new(threshold: f64) -> Self {
663        Self {
664            threshold,
665            value: 0.0,
666        }
667    }
668
669    pub fn set(&mut self, value: f64) {
670        self.value = value;
671    }
672
673    pub fn is_saturated(&self) -> bool {
674        self.value >= self.threshold
675    }
676
677    pub fn check_alert(&self) -> Option<Alert> {
678        if self.is_saturated() {
679            Some(Alert {
680                level: AlertLevel::Critical,
681                message: format!(
682                    "saturation {:.2} exceeded threshold {:.2}",
683                    self.value, self.threshold
684                ),
685                timestamp: Utc::now(),
686                operation: None,
687            })
688        } else {
689            None
690        }
691    }
692}
693
694/// Alert dispatch hook. Implementations can write alerts to logs, send them to a webhook,
695/// push them to IM, etc.
696///
697/// Must implement `Send + Sync` so it can be shared as `Arc<dyn AlertHook>` in multi-threaded
698/// environments.
699pub trait AlertHook: Send + Sync {
700    /// Handles an alert event. Returns `Ok(())` on success, or an error description string on failure.
701    fn notify(&self, alert: &Alert) -> Result<(), String>;
702}
703
704/// A simple implementation that writes alerts to the standard error stream.
705///
706/// Does not depend on the external `log` crate, avoiding extra configuration in test or
707/// minimal deployment scenarios.
708pub struct LogAlertHook;
709
710impl LogAlertHook {
711    pub fn new() -> Self {
712        Self
713    }
714}
715
716impl Default for LogAlertHook {
717    fn default() -> Self {
718        Self::new()
719    }
720}
721
722impl AlertHook for LogAlertHook {
723    fn notify(&self, alert: &Alert) -> Result<(), String> {
724        eprintln!(
725            "[SLA ALERT] level={:?} op={:?} ts={} msg={}",
726            alert.level, alert.operation, alert.timestamp, alert.message
727        );
728        Ok(())
729    }
730}
731
732/// In-memory alert hook (**not a real webhook**): stores alerts in memory without any network sends.
733///
734/// The type name is deliberately changed to `InMemoryAlertHook` (the original `WebhookAlertHook`
735/// name suggested HTTP calls, but it actually only pushes to a Vec, which would mislead users).
736/// The `identifier` field is used only for test assertions and debugging, and **is not used for
737/// any HTTP request**.
738///
739/// For real webhook push, implement a custom `AlertHook` and use `reqwest` or `hyper` to make
740/// HTTP requests in `notify`.
741///
742/// Used for testing and local development environments; dispatched alerts can be inspected via
743/// [`sent_alerts`](Self::sent_alerts).
744pub struct InMemoryAlertHook {
745    identifier: String,
746    sent: RwLock<Vec<Alert>>,
747}
748
749impl InMemoryAlertHook {
750    /// Creates an in-memory alert hook. `identifier` is only a debug label and does not participate in network calls.
751    pub fn new(identifier: String) -> Self {
752        Self {
753            identifier,
754            sent: RwLock::new(Vec::new()),
755        }
756    }
757
758    /// Returns a snapshot (copy) of the alerts "sent" so far.
759    pub fn sent_alerts(&self) -> Vec<Alert> {
760        self.sent
761            .read()
762            .map(|guard| guard.clone())
763            .unwrap_or_default()
764    }
765
766    /// Exposes the configured identifier for caller debugging or assertions.
767    pub fn identifier(&self) -> &str {
768        &self.identifier
769    }
770
771    /// Backward compatibility: returns the identifier (alias of the original url field).
772    ///
773    /// # Deprecated
774    ///
775    /// This method is retained only to reduce breaking changes; new code should use [`identifier`](Self::identifier).
776    #[deprecated(
777        since = "1.2.0",
778        note = "use `identifier()` instead; this hook does not perform HTTP"
779    )]
780    pub fn url(&self) -> &str {
781        &self.identifier
782    }
783}
784
785impl AlertHook for InMemoryAlertHook {
786    fn notify(&self, alert: &Alert) -> Result<(), String> {
787        match self.sent.write() {
788            Ok(mut guard) => {
789                guard.push(alert.clone());
790                Ok(())
791            }
792            Err(e) => Err(format!("in-memory alert hook lock poisoned: {e}")),
793        }
794    }
795}
796
797/// SLA statistics aggregation for a single operation (thread-safety provided by the `RwLock` of [`SlaMonitor`]).
798///
799/// Maintains both the latency histogram and the cumulative error count, avoiding the loss of
800/// historical samples that [`ErrorRateCounter`]'s sliding window would cause in long-cycle SLA reports.
801struct OperationStats {
802    latency: LatencyHistogram,
803    total_count: usize,
804    error_count: usize,
805}
806
807impl OperationStats {
808    fn new() -> Self {
809        Self {
810            latency: LatencyHistogram::new(Vec::new()),
811            total_count: 0,
812            error_count: 0,
813        }
814    }
815
816    fn observe(&mut self, duration: Duration, success: bool) {
817        self.latency.record(duration);
818        self.total_count += 1;
819        if !success {
820            self.error_count += 1;
821        }
822    }
823
824    fn error_rate(&self) -> f64 {
825        if self.total_count == 0 {
826            0.0
827        } else {
828            self.error_count as f64 / self.total_count as f64
829        }
830    }
831}
832
833/// SLA monitoring report snapshot, generated by [`SlaMonitor::report`].
834#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
835pub struct SlaReport {
836    /// P50 latency (milliseconds).
837    pub p50_ms: f64,
838    /// P95 latency (milliseconds).
839    pub p95_ms: f64,
840    /// P99 latency (milliseconds).
841    pub p99_ms: f64,
842    /// Error rate, in the range `[0.0, 1.0]`.
843    pub error_rate: f64,
844    /// Total number of observations.
845    pub total_count: usize,
846    /// SLO target success rate (e.g., `0.999`).
847    pub slo_target: f64,
848    /// Remaining error budget, in the range `[0.0, 1.0]`; `1.0` means the budget is intact.
849    pub error_budget_remaining: f64,
850    /// Saturation, in the range `[0.0, 1.0]`; `1.0` means the error budget is exhausted.
851    pub saturation: f64,
852}
853
854/// SLA monitor, aggregates latency and error statistics by operation name, and generates [`SlaReport`].
855///
856/// Uses `RwLock<HashMap>` internally for thread safety; `observe` provides interior mutability via
857/// `&self`, so it can be called concurrently from multiple threads via `Arc<SlaMonitor>`.
858pub struct SlaMonitor {
859    slo_target: f64,
860    operations: RwLock<HashMap<String, OperationStats>>,
861}
862
863impl SlaMonitor {
864    pub fn new(slo_target: f64) -> Self {
865        Self {
866            slo_target,
867            operations: RwLock::new(HashMap::new()),
868        }
869    }
870
871    /// 记录一次操作观测:延迟与成功/失败。
872    ///
873    /// 使用 `&self` 而非 `&mut self`,以便通过 `Arc<SlaMonitor>` 在多线程
874    /// 中并发写入(与项目现有 `SzTracer::end_span` 模式一致)。
875    pub fn observe(&self, operation: &str, duration: Duration, success: bool) {
876        if let Ok(mut ops) = self.operations.write() {
877            let stats = ops
878                .entry(operation.to_string())
879                .or_insert_with(OperationStats::new);
880            stats.observe(duration, success);
881        }
882    }
883
884    pub fn report(&self, operation: &str) -> Option<SlaReport> {
885        let ops = self.operations.read().ok()?;
886        let stats = ops.get(operation)?;
887        let error_rate = stats.error_rate();
888        let error_budget = 1.0 - self.slo_target;
889        let (saturation, error_budget_remaining) = if error_budget <= 0.0 {
890            // SLO = 1.0 意味着不允许任何错误:有错误即耗尽预算。
891            if error_rate == 0.0 {
892                (0.0, 1.0)
893            } else {
894                (1.0, 0.0)
895            }
896        } else {
897            let sat = (error_rate / error_budget).clamp(0.0, 1.0);
898            (sat, 1.0 - sat)
899        };
900        let ms = |p: f64| {
901            stats
902                .latency
903                .percentile(p)
904                .map(|d| d.as_nanos() as f64 / 1_000_000.0)
905                .unwrap_or(0.0)
906        };
907        Some(SlaReport {
908            p50_ms: ms(50.0),
909            p95_ms: ms(95.0),
910            p99_ms: ms(99.0),
911            error_rate,
912            total_count: stats.total_count,
913            slo_target: self.slo_target,
914            error_budget_remaining,
915            saturation,
916        })
917    }
918
919    pub fn operations(&self) -> Vec<String> {
920        self.operations
921            .read()
922            .map(|ops| ops.keys().cloned().collect())
923            .unwrap_or_default()
924    }
925}
926
927#[cfg(test)]
928mod tests {
929    use super::*;
930
931    #[test]
932    fn test_span_new() {
933        let span = Span::new("trace1", "span1", "operation1");
934        assert_eq!(span.trace_id, "trace1");
935        assert_eq!(span.span_id, "span1");
936        assert_eq!(span.operation_name, "operation1");
937        assert!(span.end_time.is_none());
938    }
939
940    #[test]
941    fn test_span_with_parent() {
942        let span = Span::new("trace1", "span1", "op").with_parent("parent1");
943        assert_eq!(span.parent_id, Some("parent1".to_string()));
944    }
945
946    #[test]
947    fn test_span_with_service() {
948        let span = Span::new("trace1", "span1", "op").with_service("my-service");
949        assert_eq!(span.service_name, "my-service");
950    }
951
952    #[test]
953    fn test_span_with_tag() {
954        let span = Span::new("trace1", "span1", "op").with_tag("key", "value");
955        assert_eq!(span.tags.get("key"), Some(&"value".to_string()));
956    }
957
958    #[test]
959    fn test_span_finish() {
960        let mut span = Span::new("trace1", "span1", "op");
961        span.finish();
962        assert!(span.end_time.is_some());
963        assert!(span.duration().is_some());
964    }
965
966    #[test]
967    fn test_span_add_log() {
968        let mut span = Span::new("trace1", "span1", "op");
969        span.add_log("test log");
970        assert_eq!(span.logs.len(), 1);
971        assert_eq!(span.logs[0].message, "test log");
972    }
973
974    #[test]
975    fn test_tracer_new() {
976        let tracer = SzTracer::new("test-service");
977        assert!(tracer.get_spans().is_empty());
978    }
979
980    #[test]
981    fn test_tracer_start_span() {
982        let tracer = SzTracer::new("test-service");
983        let span = tracer.start_span("test-operation");
984        assert_eq!(span.operation_name, "test-operation");
985    }
986
987    #[test]
988    fn test_tracer_end_span() {
989        let tracer = SzTracer::new("test-service");
990        let span = tracer.start_span("test-operation");
991        tracer.end_span(span);
992
993        let spans = tracer.get_spans();
994        assert_eq!(spans.len(), 1);
995    }
996
997    #[test]
998    fn test_tracer_inject() {
999        let tracer = SzTracer::new("test-service");
1000        let span = tracer.start_span("test");
1001        let headers = tracer.inject(&span);
1002
1003        // v0.2.2 修复 P2-2:默认使用 W3C traceparent header
1004        let tp = headers
1005            .get("traceparent")
1006            .expect("traceparent header must be present");
1007        // 格式:00-<trace_id>-<span_id>-01
1008        let parts: Vec<&str> = tp.split('-').collect();
1009        assert_eq!(parts.len(), 4);
1010        assert_eq!(parts[0], "00"); // 版本号
1011        assert_eq!(parts[1], span.trace_id);
1012        assert_eq!(parts[2], span.span_id);
1013        assert_eq!(parts[3], "01"); // sampled
1014    }
1015
1016    #[test]
1017    fn test_tracer_extract() {
1018        let tracer = SzTracer::new("test-service");
1019        let mut headers = HashMap::new();
1020        // W3C traceparent 格式
1021        let trace_id = "0af7651916cd43dd8448eb211c80319c";
1022        let span_id = "b7ad6b7169203331";
1023        headers.insert(
1024            "traceparent".to_string(),
1025            format!("00-{}-{}-01", trace_id, span_id),
1026        );
1027
1028        let span = tracer.extract(&headers);
1029        assert!(span.is_some());
1030        let span = span.unwrap();
1031        assert_eq!(span.trace_id, trace_id);
1032        assert_eq!(span.span_id, span_id);
1033    }
1034
1035    #[test]
1036    fn test_tracer_extract_legacy_headers() {
1037        // v0.2.2 修复 P2-2:向后兼容 legacy header
1038        let tracer = SzTracer::new("test-service");
1039        let mut headers = HashMap::new();
1040        headers.insert("trace-id".to_string(), "trace123".to_string());
1041        headers.insert("span-id".to_string(), "span456".to_string());
1042
1043        let span = tracer.extract(&headers);
1044        assert!(span.is_some());
1045        let span = span.unwrap();
1046        assert_eq!(span.trace_id, "trace123");
1047        assert_eq!(span.span_id, "span456");
1048    }
1049
1050    #[test]
1051    fn test_tracer_extract_missing_headers() {
1052        let tracer = SzTracer::new("test-service");
1053        let headers = HashMap::new();
1054        let span = tracer.extract(&headers);
1055        assert!(span.is_none());
1056    }
1057
1058    // ===================== v0.2.2 修复 P2-2:W3C TraceContext 测试 =====================
1059
1060    #[test]
1061    fn test_parse_traceparent_valid() {
1062        let valid = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
1063        let span = SzTracer::parse_traceparent(valid).expect("valid traceparent must parse");
1064        assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1065        assert_eq!(span.span_id, "b7ad6b7169203331");
1066    }
1067
1068    #[test]
1069    fn test_parse_traceparent_invalid_version() {
1070        // 版本号不是 2 字符 hex
1071        assert!(SzTracer::parse_traceparent(
1072            "0-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1073        )
1074        .is_none());
1075        // 版本号不是 hex
1076        assert!(SzTracer::parse_traceparent(
1077            "xy-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1078        )
1079        .is_none());
1080    }
1081
1082    #[test]
1083    fn test_parse_traceparent_invalid_trace_id_length() {
1084        // trace_id 不是 32 字符
1085        assert!(SzTracer::parse_traceparent("00-short-b7ad6b7169203331-01").is_none());
1086    }
1087
1088    #[test]
1089    fn test_parse_traceparent_invalid_span_id_length() {
1090        // span_id 不是 16 字符
1091        assert!(
1092            SzTracer::parse_traceparent("00-0af7651916cd43dd8448eb211c80319c-short-01").is_none()
1093        );
1094    }
1095
1096    #[test]
1097    fn test_parse_traceparent_all_zeros_trace_id_rejected() {
1098        // W3C 规范:trace_id 不能全为 0
1099        let all_zero = "00-00000000000000000000000000000000-b7ad6b7169203331-01";
1100        assert!(SzTracer::parse_traceparent(all_zero).is_none());
1101    }
1102
1103    #[test]
1104    fn test_parse_traceparent_all_zeros_span_id_rejected() {
1105        // W3C 规范:span_id 不能全为 0
1106        let all_zero = "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01";
1107        assert!(SzTracer::parse_traceparent(all_zero).is_none());
1108    }
1109
1110    #[test]
1111    fn test_parse_traceparent_invalid_flags() {
1112        // trace_flags 不是 2 字符 hex
1113        assert!(SzTracer::parse_traceparent(
1114            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-xyz"
1115        )
1116        .is_none());
1117    }
1118
1119    #[test]
1120    fn test_parse_traceparent_wrong_part_count() {
1121        // 不是 4 个部分
1122        assert!(SzTracer::parse_traceparent(
1123            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331"
1124        )
1125        .is_none());
1126        assert!(SzTracer::parse_traceparent(
1127            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01-extra"
1128        )
1129        .is_none());
1130    }
1131
1132    #[test]
1133    fn test_inject_legacy_preserves_old_format() {
1134        let tracer = SzTracer::new("svc");
1135        let span = tracer.start_span("op");
1136        let headers = tracer.inject_legacy(&span);
1137
1138        assert_eq!(headers.get("trace-id"), Some(&span.trace_id.to_string()));
1139        assert_eq!(headers.get("span-id"), Some(&span.span_id.to_string()));
1140        assert!(!headers.contains_key("parent-span-id"));
1141    }
1142
1143    #[test]
1144    fn test_extract_legacy_preserves_old_format() {
1145        let tracer = SzTracer::new("svc");
1146        let mut headers = HashMap::new();
1147        headers.insert("trace-id".to_string(), "abc".to_string());
1148        headers.insert("span-id".to_string(), "def".to_string());
1149
1150        let span = tracer.extract_legacy(&headers).expect("legacy extract");
1151        assert_eq!(span.trace_id, "abc");
1152        assert_eq!(span.span_id, "def");
1153    }
1154
1155    #[test]
1156    fn test_w3c_traceparent_roundtrip_preserves_ids() {
1157        // inject → extract 必须保留 trace_id 和 span_id
1158        let tracer = SzTracer::new("svc");
1159        let original = tracer.start_span("roundtrip");
1160        let headers = tracer.inject(&original);
1161
1162        let extracted = tracer.extract(&headers).expect("roundtrip extract");
1163        assert_eq!(extracted.trace_id(), original.trace_id());
1164        assert_eq!(extracted.span_id(), original.span_id());
1165        assert!(extracted.parent_id().is_none());
1166    }
1167
1168    #[test]
1169    fn test_w3c_prefers_traceparent_over_legacy() {
1170        // 同时存在 traceparent 和 legacy header 时,优先 W3C
1171        let tracer = SzTracer::new("svc");
1172        let mut headers = HashMap::new();
1173        headers.insert(
1174            "traceparent".to_string(),
1175            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
1176        );
1177        headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1178        headers.insert("span-id".to_string(), "legacy-span".to_string());
1179
1180        let span = tracer.extract(&headers).expect("extract");
1181        assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1182        assert_eq!(span.span_id, "b7ad6b7169203331");
1183    }
1184
1185    #[test]
1186    fn test_w3c_falls_back_to_legacy_on_invalid_traceparent() {
1187        // traceparent 格式不合法时回退到 legacy
1188        let tracer = SzTracer::new("svc");
1189        let mut headers = HashMap::new();
1190        headers.insert("traceparent".to_string(), "invalid-format".to_string());
1191        headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1192        headers.insert("span-id".to_string(), "legacy-span".to_string());
1193
1194        let span = tracer
1195            .extract(&headers)
1196            .expect("should fall back to legacy");
1197        assert_eq!(span.trace_id, "legacy-trace");
1198        assert_eq!(span.span_id, "legacy-span");
1199    }
1200
1201    #[test]
1202    fn test_otel_tracer() {
1203        let tracer = OtelTracer::new("test-service");
1204        let span = tracer.start_span("test-operation");
1205        assert_eq!(span.operation_name, "test-operation");
1206    }
1207
1208    #[test]
1209    fn test_otel_tracer_is_a_sz_tracer_wrapper_not_a_real_otel_sdk() {
1210        // OtelTracer delegates every Tracer method to SzTracer. Document the
1211        // contract: spans produced via OtelTracer must be observable through
1212        // the underlying SzTracer (i.e. `inner().get_spans()`).
1213        let tracer = OtelTracer::new("svc");
1214        assert!(tracer.inner().get_spans().is_empty());
1215
1216        let span = tracer.start_span("op");
1217        assert_eq!(span.service_name(), "svc");
1218        tracer.end_span(span);
1219
1220        let spans = tracer.inner().get_spans();
1221        assert_eq!(spans.len(), 1);
1222        assert_eq!(spans[0].operation_name(), "op");
1223        assert!(spans[0].end_time.is_some());
1224    }
1225
1226    #[test]
1227    fn test_otel_tracer_inject_extract_roundtrip_preserves_ids() {
1228        // v0.2.2 修复 P2-2:现在使用 W3C traceparent header
1229        let tracer = OtelTracer::new("svc");
1230        let original = tracer.start_span("roundtrip");
1231        let headers = tracer.inject(&original);
1232
1233        // W3C traceparent 必须存在并包含原始 ID
1234        let tp = headers
1235            .get("traceparent")
1236            .expect("traceparent must be present");
1237        assert!(tp.contains(original.trace_id()));
1238        assert!(tp.contains(original.span_id()));
1239        assert!(!headers.contains_key("parent-span-id"));
1240
1241        let extracted = tracer.extract(&headers).expect("extract should round-trip");
1242        assert_eq!(extracted.trace_id(), original.trace_id());
1243        assert_eq!(extracted.span_id(), original.span_id());
1244        assert!(extracted.parent_id().is_none());
1245    }
1246
1247    #[test]
1248    fn test_otel_tracer_preserves_parent_id_through_roundtrip() {
1249        let tracer = OtelTracer::new("svc");
1250        let parent = tracer.start_span("parent");
1251        let child = tracer
1252            .start_span("child")
1253            .with_parent(parent.span_id().to_string());
1254        let headers = tracer.inject(&child);
1255
1256        // parent-span-id 仍然通过独立 header 传递(向后兼容)
1257        assert_eq!(
1258            headers.get("parent-span-id"),
1259            Some(&parent.span_id().to_string())
1260        );
1261
1262        let extracted = tracer.extract(&headers).expect("extract should round-trip");
1263        assert_eq!(extracted.parent_id(), Some(parent.span_id()));
1264    }
1265
1266    #[test]
1267    fn test_otel_tracer_extract_returns_none_without_required_headers() {
1268        let tracer = OtelTracer::new("svc");
1269        let headers: HashMap<String, String> = HashMap::new();
1270        assert!(tracer.extract(&headers).is_none());
1271
1272        // 仅 legacy trace-id 缺失 span-id 时也必须失败
1273        let mut partial = HashMap::new();
1274        partial.insert("trace-id".to_string(), "abc".to_string());
1275        // Missing span-id - extract must fail.
1276        assert!(tracer.extract(&partial).is_none());
1277
1278        // 仅 W3C traceparent 格式错误时(且无 legacy 回退)必须失败
1279        let mut bad_w3c = HashMap::new();
1280        bad_w3c.insert("traceparent".to_string(), "garbage".to_string());
1281        assert!(tracer.extract(&bad_w3c).is_none());
1282    }
1283
1284    #[test]
1285    fn test_otel_tracer_generated_ids_have_correct_length() {
1286        // OpenTelemetry spec: trace_id is 16 bytes (32 hex chars), span_id
1287        // is 8 bytes (16 hex chars). OtelTracer inherits SzTracer's generator
1288        // and must produce the same shape so consumers parsing the IDs do not
1289        // trip over a different length.
1290        let trace_id = SzTracer::generate_trace_id();
1291        let span_id = SzTracer::generate_span_id();
1292        assert_eq!(trace_id.len(), 32, "trace_id must be 32 hex chars");
1293        assert_eq!(span_id.len(), 16, "span_id must be 16 hex chars");
1294
1295        // Hex-only.
1296        assert!(trace_id.chars().all(|c| c.is_ascii_hexdigit()));
1297        assert!(span_id.chars().all(|c| c.is_ascii_hexdigit()));
1298    }
1299
1300    #[test]
1301    fn test_span_accessors() {
1302        let span = Span::new("trace1", "span1", "test-op")
1303            .with_service("svc")
1304            .with_tag("k", "v");
1305
1306        assert_eq!(span.trace_id(), "trace1");
1307        assert_eq!(span.span_id(), "span1");
1308        assert_eq!(span.operation_name(), "test-op");
1309        assert_eq!(span.service_name(), "svc");
1310        assert_eq!(span.tags().get("k"), Some(&"v".to_string()));
1311    }
1312
1313    #[test]
1314    fn test_generate_ids() {
1315        let trace_id = SzTracer::generate_trace_id();
1316        let span_id = SzTracer::generate_span_id();
1317
1318        assert_eq!(trace_id.len(), 32);
1319        assert_eq!(span_id.len(), 16);
1320    }
1321
1322    #[test]
1323    fn test_tracer_clear() {
1324        let tracer = SzTracer::new("test-service");
1325
1326        let span = tracer.start_span("op1");
1327        tracer.end_span(span);
1328        let span = tracer.start_span("op2");
1329        tracer.end_span(span);
1330
1331        assert_eq!(tracer.get_spans().len(), 2);
1332
1333        tracer.clear();
1334        assert!(tracer.get_spans().is_empty());
1335    }
1336
1337    // ===================== LatencyHistogram tests =====================
1338
1339    #[test]
1340    fn test_latency_histogram_new_empty() {
1341        let hist = LatencyHistogram::new(vec![
1342            Duration::from_millis(10),
1343            Duration::from_millis(100),
1344            Duration::from_millis(1000),
1345        ]);
1346        assert_eq!(hist.count(), 0);
1347        assert!(hist.percentile(50.0).is_none());
1348        assert!(hist.mean().is_none());
1349    }
1350
1351    #[test]
1352    fn test_latency_histogram_record_single() {
1353        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1354        hist.record(Duration::from_millis(50));
1355        assert_eq!(hist.count(), 1);
1356        assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(50)));
1357        assert_eq!(hist.mean(), Some(Duration::from_millis(50)));
1358    }
1359
1360    #[test]
1361    fn test_latency_histogram_percentile_p50_sorted() {
1362        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1363        for ms in [10, 20, 30, 40, 50] {
1364            hist.record(Duration::from_millis(ms));
1365        }
1366        // 5 samples sorted: [10, 20, 30, 40, 50], p50 should be 30 (median)
1367        assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1368    }
1369
1370    #[test]
1371    fn test_latency_histogram_percentile_p95_high_value() {
1372        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1373        for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1374            hist.record(Duration::from_millis(ms));
1375        }
1376        // p95 of 10 samples should be the 9th or 10th value (high)
1377        let p95 = hist.percentile(95.0).expect("p95 must exist");
1378        assert!(p95 >= Duration::from_millis(9));
1379    }
1380
1381    #[test]
1382    fn test_latency_histogram_percentile_p99_max_value() {
1383        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1384        for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1385            hist.record(Duration::from_millis(ms));
1386        }
1387        let p99 = hist.percentile(99.0).expect("p99 must exist");
1388        assert_eq!(p99, Duration::from_millis(100));
1389    }
1390
1391    #[test]
1392    fn test_latency_histogram_percentile_p0_min_value() {
1393        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1394        for ms in [10, 20, 30] {
1395            hist.record(Duration::from_millis(ms));
1396        }
1397        assert_eq!(hist.percentile(0.0), Some(Duration::from_millis(10)));
1398    }
1399
1400    #[test]
1401    fn test_latency_histogram_percentile_p100_max_value() {
1402        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1403        for ms in [10, 20, 30] {
1404            hist.record(Duration::from_millis(ms));
1405        }
1406        assert_eq!(hist.percentile(100.0), Some(Duration::from_millis(30)));
1407    }
1408
1409    #[test]
1410    fn test_latency_histogram_percentile_empty_returns_none() {
1411        let hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1412        assert!(hist.percentile(50.0).is_none());
1413    }
1414
1415    #[test]
1416    fn test_latency_histogram_percentile_out_of_range_returns_none() {
1417        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1418        hist.record(Duration::from_millis(50));
1419        assert!(hist.percentile(-1.0).is_none());
1420        assert!(hist.percentile(101.0).is_none());
1421    }
1422
1423    #[test]
1424    fn test_latency_histogram_count() {
1425        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1426        assert_eq!(hist.count(), 0);
1427        hist.record(Duration::from_millis(10));
1428        hist.record(Duration::from_millis(20));
1429        hist.record(Duration::from_millis(30));
1430        assert_eq!(hist.count(), 3);
1431    }
1432
1433    #[test]
1434    fn test_latency_histogram_mean_multiple() {
1435        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1436        for ms in [10, 20, 30, 40, 50] {
1437            hist.record(Duration::from_millis(ms));
1438        }
1439        // mean = (10+20+30+40+50)/5 = 30
1440        assert_eq!(hist.mean(), Some(Duration::from_millis(30)));
1441    }
1442
1443    #[test]
1444    fn test_latency_histogram_record_unsorted_input_stays_sorted() {
1445        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1446        hist.record(Duration::from_millis(50));
1447        hist.record(Duration::from_millis(10));
1448        hist.record(Duration::from_millis(30));
1449        // p50 should be 30 (median of sorted [10,30,50])
1450        assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1451    }
1452
1453    // ===================== ErrorRateCounter tests =====================
1454
1455    #[test]
1456    fn test_error_rate_counter_new_empty() {
1457        let counter = ErrorRateCounter::new(Duration::from_secs(60));
1458        assert_eq!(counter.total(), 0);
1459        assert_eq!(counter.errors(), 0);
1460        assert_eq!(counter.rate(), 0.0);
1461    }
1462
1463    #[test]
1464    fn test_error_rate_counter_all_success_rate_zero() {
1465        let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1466        for _ in 0..10 {
1467            counter.record(true);
1468        }
1469        assert_eq!(counter.total(), 10);
1470        assert_eq!(counter.errors(), 0);
1471        assert_eq!(counter.rate(), 0.0);
1472    }
1473
1474    #[test]
1475    fn test_error_rate_counter_all_failures_rate_one() {
1476        let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1477        for _ in 0..10 {
1478            counter.record(false);
1479        }
1480        assert_eq!(counter.total(), 10);
1481        assert_eq!(counter.errors(), 10);
1482        assert_eq!(counter.rate(), 1.0);
1483    }
1484
1485    #[test]
1486    fn test_error_rate_counter_mixed_rate() {
1487        let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1488        // 7 success, 3 failures -> rate = 0.3
1489        for _ in 0..7 {
1490            counter.record(true);
1491        }
1492        for _ in 0..3 {
1493            counter.record(false);
1494        }
1495        assert_eq!(counter.total(), 10);
1496        assert_eq!(counter.errors(), 3);
1497        let rate = counter.rate();
1498        assert!((rate - 0.3).abs() < 1e-9, "expected 0.3, got {rate}");
1499    }
1500
1501    #[test]
1502    fn test_error_rate_counter_empty_rate_is_zero() {
1503        let counter = ErrorRateCounter::new(Duration::from_secs(60));
1504        // no samples -> rate must be 0.0 (avoid div-by-zero)
1505        assert_eq!(counter.rate(), 0.0);
1506    }
1507
1508    #[test]
1509    fn test_error_rate_counter_window_expires_old_samples() {
1510        // window of 100ms; old samples should be evicted on subsequent record.
1511        let mut counter = ErrorRateCounter::new(Duration::from_millis(100));
1512        counter.record(false);
1513        counter.record(false);
1514        // sleep past the window
1515        std::thread::sleep(Duration::from_millis(120));
1516        counter.record(true);
1517        // Only the recent success should remain -> rate = 0.0, total = 1
1518        assert_eq!(counter.total(), 1);
1519        assert_eq!(counter.errors(), 0);
1520        assert_eq!(counter.rate(), 0.0);
1521    }
1522
1523    // ===================== ErrorBudget tests =====================
1524
1525    #[test]
1526    fn test_error_budget_new_is_full() {
1527        let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1528        assert!((budget.remaining() - 1.0).abs() < 1e-9);
1529        assert!(!budget.is_exhausted());
1530    }
1531
1532    #[test]
1533    fn test_error_budget_consume_reduces_remaining() {
1534        let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1535        // SLO 0.999 -> error_budget = 0.001 per error.
1536        budget.consume(1);
1537        // remaining = 1 - 1 * 0.001 = 0.999
1538        assert!((budget.remaining() - 0.999).abs() < 1e-9);
1539        assert!(!budget.is_exhausted());
1540    }
1541
1542    #[test]
1543    fn test_error_budget_exhausted_at_capacity() {
1544        let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1545        // capacity = 1 / (1 - 0.999) = 1000 errors
1546        budget.consume(1000);
1547        assert_eq!(budget.remaining(), 0.0);
1548        assert!(budget.is_exhausted());
1549    }
1550
1551    #[test]
1552    fn test_error_budget_over_consume_clamps_to_zero() {
1553        let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1554        budget.consume(2000);
1555        assert_eq!(budget.remaining(), 0.0);
1556        assert!(budget.is_exhausted());
1557    }
1558
1559    #[test]
1560    fn test_error_budget_zero_errors_returns_one() {
1561        let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1562        assert_eq!(budget.remaining(), 1.0);
1563        assert!(!budget.is_exhausted());
1564    }
1565
1566    #[test]
1567    fn test_error_budget_window_refills_after_expiry() {
1568        // SLO 0.5 -> error_budget = 0.5 per error -> 2 errors exhaust.
1569        let mut budget = ErrorBudget::new(0.5, Duration::from_millis(100));
1570        budget.consume(2);
1571        assert!(budget.is_exhausted());
1572        // wait for window to expire
1573        std::thread::sleep(Duration::from_millis(120));
1574        assert!((budget.remaining() - 1.0).abs() < 1e-9);
1575        assert!(!budget.is_exhausted());
1576    }
1577
1578    #[test]
1579    fn test_error_budget_slo_one_means_no_errors_allowed() {
1580        // SLO = 1.0 -> error_budget = 0 -> any consume exhausts
1581        let mut budget = ErrorBudget::new(1.0, Duration::from_secs(60));
1582        assert_eq!(budget.remaining(), 1.0);
1583        budget.consume(1);
1584        assert_eq!(budget.remaining(), 0.0);
1585        assert!(budget.is_exhausted());
1586    }
1587
1588    #[test]
1589    fn test_error_budget_multiple_consumes_accumulate() {
1590        let mut budget = ErrorBudget::new(0.99, Duration::from_secs(60));
1591        // SLO 0.99 -> error_budget = 0.01 -> capacity = 100 errors
1592        budget.consume(30);
1593        assert!((budget.remaining() - 0.7).abs() < 1e-9);
1594        budget.consume(30);
1595        assert!((budget.remaining() - 0.4).abs() < 1e-9);
1596        budget.consume(40);
1597        assert_eq!(budget.remaining(), 0.0);
1598        assert!(budget.is_exhausted());
1599    }
1600
1601    // ===================== Alert / AlertLevel tests =====================
1602
1603    #[test]
1604    fn test_alert_level_variants_exist() {
1605        let info = AlertLevel::Info;
1606        let warning = AlertLevel::Warning;
1607        let critical = AlertLevel::Critical;
1608        // Sanity: ensure variants are distinct (debug repr).
1609        assert_ne!(format!("{info:?}"), format!("{warning:?}"));
1610        assert_ne!(format!("{warning:?}"), format!("{critical:?}"));
1611        assert_ne!(format!("{info:?}"), format!("{critical:?}"));
1612    }
1613
1614    #[test]
1615    fn test_alert_construction_with_all_fields() {
1616        let ts = Utc::now();
1617        let alert = Alert {
1618            level: AlertLevel::Critical,
1619            message: "p99 latency exceeded budget".to_string(),
1620            timestamp: ts,
1621            operation: Some("query_user".to_string()),
1622        };
1623        assert_eq!(alert.level, AlertLevel::Critical);
1624        assert_eq!(alert.message, "p99 latency exceeded budget");
1625        assert_eq!(alert.timestamp, ts);
1626        assert_eq!(alert.operation.as_deref(), Some("query_user"));
1627    }
1628
1629    #[test]
1630    fn test_alert_construction_without_operation() {
1631        let alert = Alert {
1632            level: AlertLevel::Info,
1633            message: "system healthy".to_string(),
1634            timestamp: Utc::now(),
1635            operation: None,
1636        };
1637        assert!(alert.operation.is_none());
1638    }
1639
1640    #[test]
1641    fn test_alert_implements_clone_debug() {
1642        let alert = Alert {
1643            level: AlertLevel::Warning,
1644            message: "approaching budget".to_string(),
1645            timestamp: Utc::now(),
1646            operation: Some("op".to_string()),
1647        };
1648        let cloned = alert.clone();
1649        assert_eq!(cloned.level, alert.level);
1650        assert_eq!(cloned.message, alert.message);
1651        // Debug formatting must contain expected fields, not just "not panic".
1652        let debug_str = format!("{alert:?}");
1653        assert!(
1654            debug_str.contains("approaching budget"),
1655            "debug output missing message: {}",
1656            debug_str
1657        );
1658        assert!(
1659            debug_str.contains("Warning"),
1660            "debug output missing level: {}",
1661            debug_str
1662        );
1663    }
1664
1665    // ===================== SaturationGauge tests =====================
1666
1667    #[test]
1668    fn test_saturation_gauge_new_starts_unsaturated() {
1669        let gauge = SaturationGauge::new(0.8);
1670        assert!(!gauge.is_saturated());
1671        assert!(gauge.check_alert().is_none());
1672    }
1673
1674    #[test]
1675    fn test_saturation_gauge_set_below_threshold_not_saturated() {
1676        let mut gauge = SaturationGauge::new(0.8);
1677        gauge.set(0.5);
1678        assert!(!gauge.is_saturated());
1679        assert!(gauge.check_alert().is_none());
1680    }
1681
1682    #[test]
1683    fn test_saturation_gauge_set_at_threshold_is_saturated() {
1684        let mut gauge = SaturationGauge::new(0.8);
1685        gauge.set(0.8);
1686        assert!(gauge.is_saturated());
1687    }
1688
1689    #[test]
1690    fn test_saturation_gauge_set_above_threshold_is_saturated() {
1691        let mut gauge = SaturationGauge::new(0.8);
1692        gauge.set(0.95);
1693        assert!(gauge.is_saturated());
1694    }
1695
1696    #[test]
1697    fn test_saturation_gauge_check_alert_returns_critical_when_saturated() {
1698        let mut gauge = SaturationGauge::new(0.8);
1699        gauge.set(0.95);
1700        let alert = gauge.check_alert().expect("alert must fire when saturated");
1701        assert_eq!(alert.level, AlertLevel::Critical);
1702        assert!(!alert.message.is_empty());
1703        assert!(alert.operation.is_none()); // gauge has no operation context
1704    }
1705
1706    #[test]
1707    fn test_saturation_gauge_check_alert_returns_none_when_not_saturated() {
1708        let mut gauge = SaturationGauge::new(0.8);
1709        gauge.set(0.3);
1710        assert!(gauge.check_alert().is_none());
1711    }
1712
1713    #[test]
1714    fn test_saturation_gauge_set_zero_not_saturated() {
1715        let mut gauge = SaturationGauge::new(0.8);
1716        gauge.set(0.0);
1717        assert!(!gauge.is_saturated());
1718    }
1719
1720    #[test]
1721    fn test_saturation_gauge_set_one_saturated() {
1722        let mut gauge = SaturationGauge::new(0.5);
1723        gauge.set(1.0);
1724        assert!(gauge.is_saturated());
1725    }
1726
1727    #[test]
1728    fn test_saturation_gauge_threshold_zero_always_saturated() {
1729        let mut gauge = SaturationGauge::new(0.0);
1730        gauge.set(0.0);
1731        // threshold = 0 means anything >= 0 is saturated (only negative would not be,
1732        // but saturation values are conventionally in [0, 1]).
1733        assert!(gauge.is_saturated());
1734    }
1735
1736    // ===================== AlertHook / LogAlertHook / InMemoryAlertHook tests =====================
1737
1738    fn sample_alert(level: AlertLevel, op: Option<&str>) -> Alert {
1739        Alert {
1740            level,
1741            message: "test alert".to_string(),
1742            timestamp: Utc::now(),
1743            operation: op.map(str::to_string),
1744        }
1745    }
1746
1747    #[test]
1748    fn test_log_alert_hook_notify_returns_ok() {
1749        let hook = LogAlertHook::new();
1750        let alert = sample_alert(AlertLevel::Warning, Some("op"));
1751        let result = hook.notify(&alert);
1752        assert!(result.is_ok());
1753    }
1754
1755    #[test]
1756    fn test_log_alert_hook_notify_critical_succeeds() {
1757        let hook = LogAlertHook::new();
1758        let alert = sample_alert(AlertLevel::Critical, None);
1759        let result = hook.notify(&alert);
1760        assert!(result.is_ok());
1761    }
1762
1763    #[test]
1764    fn test_log_alert_hook_implements_send_sync() {
1765        fn assert_send_sync<T: Send + Sync>() {}
1766        assert_send_sync::<LogAlertHook>();
1767    }
1768
1769    #[test]
1770    fn test_webhook_alert_hook_new_starts_empty() {
1771        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1772        assert!(hook.sent_alerts().is_empty());
1773    }
1774
1775    #[test]
1776    fn test_webhook_alert_hook_notify_stores_alert() {
1777        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1778        let alert = sample_alert(AlertLevel::Critical, Some("query_user"));
1779        hook.notify(&alert).expect("notify must succeed");
1780        let sent = hook.sent_alerts();
1781        assert_eq!(sent.len(), 1);
1782        assert_eq!(sent[0], alert);
1783    }
1784
1785    #[test]
1786    fn test_webhook_alert_hook_multiple_notifications_accumulate() {
1787        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1788        let a1 = sample_alert(AlertLevel::Info, None);
1789        let a2 = sample_alert(AlertLevel::Warning, Some("op1"));
1790        let a3 = sample_alert(AlertLevel::Critical, Some("op2"));
1791        hook.notify(&a1).unwrap();
1792        hook.notify(&a2).unwrap();
1793        hook.notify(&a3).unwrap();
1794        let sent = hook.sent_alerts();
1795        assert_eq!(sent.len(), 3);
1796        assert_eq!(sent[0], a1);
1797        assert_eq!(sent[1], a2);
1798        assert_eq!(sent[2], a3);
1799    }
1800
1801    #[test]
1802    fn test_webhook_alert_hook_sent_alerts_returns_clone() {
1803        // The returned Vec should be a snapshot; mutating it must not affect the hook.
1804        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1805        let alert = sample_alert(AlertLevel::Info, None);
1806        hook.notify(&alert).unwrap();
1807        let mut sent = hook.sent_alerts();
1808        sent.clear();
1809        // hook itself must remain unaffected.
1810        assert_eq!(hook.sent_alerts().len(), 1);
1811    }
1812
1813    #[test]
1814    fn test_webhook_alert_hook_implements_send_sync() {
1815        fn assert_send_sync<T: Send + Sync>() {}
1816        assert_send_sync::<InMemoryAlertHook>();
1817    }
1818
1819    #[test]
1820    fn test_alert_hook_trait_object_dispatch() {
1821        // Confirm trait-object dispatch works for heterogeneous hooks.
1822        let hooks: Vec<Box<dyn AlertHook>> = vec![
1823            Box::new(LogAlertHook::new()),
1824            Box::new(InMemoryAlertHook::new(
1825                "https://example.com/hook".to_string(),
1826            )),
1827        ];
1828        let alert = sample_alert(AlertLevel::Critical, Some("op"));
1829        for hook in &hooks {
1830            assert!(hook.notify(&alert).is_ok());
1831        }
1832        // The webhook hook stored one alert; verify via downcast-less approach by
1833        // constructing separately.
1834        let webhook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1835        webhook.notify(&alert).unwrap();
1836        assert_eq!(webhook.sent_alerts().len(), 1);
1837    }
1838
1839    // ===================== SlaMonitor / SlaReport tests =====================
1840
1841    #[test]
1842    fn test_sla_monitor_new_empty() {
1843        let monitor = SlaMonitor::new(0.999);
1844        assert!(monitor.operations().is_empty());
1845    }
1846
1847    #[test]
1848    fn test_sla_monitor_observe_creates_operation() {
1849        let monitor = SlaMonitor::new(0.999);
1850        monitor.observe("query", Duration::from_millis(50), true);
1851        let ops = monitor.operations();
1852        assert_eq!(ops, vec!["query".to_string()]);
1853    }
1854
1855    #[test]
1856    fn test_sla_monitor_report_unknown_returns_none() {
1857        let monitor = SlaMonitor::new(0.999);
1858        assert!(monitor.report("unknown").is_none());
1859    }
1860
1861    #[test]
1862    fn test_sla_monitor_report_basic_stats_all_success() {
1863        let monitor = SlaMonitor::new(0.999);
1864        for ms in [10, 20, 30, 40, 50] {
1865            monitor.observe("op", Duration::from_millis(ms), true);
1866        }
1867        let report = monitor.report("op").expect("report must exist");
1868        assert_eq!(report.total_count, 5);
1869        assert_eq!(report.error_rate, 0.0);
1870        assert!((report.slo_target - 0.999).abs() < 1e-9);
1871        // p50 of [10,20,30,40,50] with nearest rank: rank=ceil(0.5*5)=3, samples[2]=30
1872        assert!((report.p50_ms - 30.0).abs() < 1e-9);
1873        // No errors -> full budget remaining, zero saturation.
1874        assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1875        assert!((report.saturation - 0.0).abs() < 1e-9);
1876    }
1877
1878    #[test]
1879    fn test_sla_monitor_report_with_errors_over_budget() {
1880        let monitor = SlaMonitor::new(0.999);
1881        // 8 success, 2 failures -> error_rate = 0.2
1882        for _ in 0..8 {
1883            monitor.observe("op", Duration::from_millis(10), true);
1884        }
1885        for _ in 0..2 {
1886            monitor.observe("op", Duration::from_millis(10), false);
1887        }
1888        let report = monitor.report("op").expect("report must exist");
1889        assert_eq!(report.total_count, 10);
1890        assert!((report.error_rate - 0.2).abs() < 1e-9);
1891        // error_budget = 1 - 0.999 = 0.001
1892        // 0.2 / 0.001 = 200x over budget -> clamped to saturation = 1.0
1893        assert!((report.saturation - 1.0).abs() < 1e-9);
1894        assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
1895    }
1896
1897    #[test]
1898    fn test_sla_monitor_report_partial_budget() {
1899        // SLO 0.9 -> error_budget = 0.1
1900        // 1 error out of 20 -> error_rate = 0.05
1901        // saturation = 0.05 / 0.1 = 0.5
1902        // remaining = 1 - 0.5 = 0.5
1903        let monitor = SlaMonitor::new(0.9);
1904        for i in 0..20 {
1905            monitor.observe("op", Duration::from_millis(i), i != 5);
1906        }
1907        let report = monitor.report("op").expect("report");
1908        assert!((report.error_rate - 0.05).abs() < 1e-9);
1909        assert!((report.saturation - 0.5).abs() < 1e-9);
1910        assert!((report.error_budget_remaining - 0.5).abs() < 1e-9);
1911    }
1912
1913    #[test]
1914    fn test_sla_monitor_operations_isolated() {
1915        let monitor = SlaMonitor::new(0.999);
1916        monitor.observe("op1", Duration::from_millis(10), true);
1917        monitor.observe("op2", Duration::from_millis(20), false);
1918        let mut ops = monitor.operations();
1919        ops.sort();
1920        assert_eq!(ops, vec!["op1".to_string(), "op2".to_string()]);
1921
1922        let r1 = monitor.report("op1").expect("op1 report");
1923        let r2 = monitor.report("op2").expect("op2 report");
1924        assert_eq!(r1.total_count, 1);
1925        assert_eq!(r2.total_count, 1);
1926        assert!((r1.error_rate - 0.0).abs() < 1e-9);
1927        assert!((r2.error_rate - 1.0).abs() < 1e-9);
1928    }
1929
1930    #[test]
1931    fn test_sla_monitor_p95_p99_high_percentile() {
1932        let monitor = SlaMonitor::new(0.999);
1933        for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1934            monitor.observe("op", Duration::from_millis(ms), true);
1935        }
1936        let report = monitor.report("op").expect("report");
1937        // p95/p99 with nearest rank n=10: ceil(0.95*10)=10, ceil(0.99*10)=10 -> samples[9]=100
1938        assert!((report.p95_ms - 100.0).abs() < 1e-9);
1939        assert!((report.p99_ms - 100.0).abs() < 1e-9);
1940    }
1941
1942    #[test]
1943    fn test_sla_monitor_observe_aggregates_multiple_calls() {
1944        let monitor = SlaMonitor::new(0.999);
1945        for _ in 0..100 {
1946            monitor.observe("op", Duration::from_millis(5), true);
1947        }
1948        let report = monitor.report("op").expect("report");
1949        assert_eq!(report.total_count, 100);
1950        assert!((report.p50_ms - 5.0).abs() < 1e-9);
1951    }
1952
1953    #[test]
1954    fn test_sla_monitor_implements_send_sync() {
1955        fn assert_send_sync<T: Send + Sync>() {}
1956        assert_send_sync::<SlaMonitor>();
1957    }
1958
1959    #[test]
1960    fn test_sla_monitor_concurrent_observe_thread_safe() {
1961        use std::sync::Arc;
1962        use std::thread;
1963
1964        let monitor = Arc::new(SlaMonitor::new(0.999));
1965        let mut handles = vec![];
1966
1967        for t in 0..4 {
1968            let m = Arc::clone(&monitor);
1969            handles.push(thread::spawn(move || {
1970                for i in 0..100 {
1971                    // 10% failure rate (i % 10 == 0)
1972                    m.observe("op", Duration::from_millis(i as u64), i % 10 != 0);
1973                }
1974                // Touch another operation per thread to verify isolation.
1975                let op_name = format!("thread-{t}");
1976                m.observe(&op_name, Duration::from_millis(1), true);
1977            }));
1978        }
1979
1980        for h in handles {
1981            h.join().unwrap();
1982        }
1983
1984        let report = monitor.report("op").expect("op report");
1985        assert_eq!(report.total_count, 400);
1986        // 10% error rate
1987        assert!((report.error_rate - 0.1).abs() < 1e-9);
1988
1989        // Each thread registered its own operation.
1990        let mut ops = monitor.operations();
1991        ops.sort();
1992        // "op" + 4 thread-specific operations
1993        assert_eq!(ops.len(), 5);
1994        assert!(ops.contains(&"op".to_string()));
1995    }
1996
1997    #[test]
1998    fn test_sla_monitor_report_slo_one_with_no_errors() {
1999        // SLO = 1.0 with no errors -> full budget, zero saturation.
2000        let monitor = SlaMonitor::new(1.0);
2001        monitor.observe("op", Duration::from_millis(10), true);
2002        let report = monitor.report("op").expect("report");
2003        assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
2004        assert!((report.saturation - 0.0).abs() < 1e-9);
2005    }
2006
2007    #[test]
2008    fn test_sla_monitor_report_slo_one_with_errors() {
2009        // SLO = 1.0 with any error -> budget fully exhausted.
2010        let monitor = SlaMonitor::new(1.0);
2011        monitor.observe("op", Duration::from_millis(10), false);
2012        let report = monitor.report("op").expect("report");
2013        assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
2014        assert!((report.saturation - 1.0).abs() < 1e-9);
2015    }
2016
2017    #[test]
2018    fn test_sla_report_fields_are_public() {
2019        // Verifies all SlaReport fields are accessible per the spec.
2020        let monitor = SlaMonitor::new(0.999);
2021        monitor.observe("op", Duration::from_millis(10), true);
2022        let r = monitor.report("op").unwrap();
2023        let _p50: f64 = r.p50_ms;
2024        let _p95: f64 = r.p95_ms;
2025        let _p99: f64 = r.p99_ms;
2026        let _erate: f64 = r.error_rate;
2027        let _total: usize = r.total_count;
2028        let _slo: f64 = r.slo_target;
2029        let _ebr: f64 = r.error_budget_remaining;
2030        let _sat: f64 = r.saturation;
2031    }
2032}
2033
2034// ============================================================================
2035// OTLP Exporter(feature = "otlp")
2036// ============================================================================
2037
2038/// OTLP exporter configuration
2039///
2040/// Exports SZ-ORM tracing Spans to a Collector via the OpenTelemetry OTLP protocol.
2041///
2042/// # Examples
2043///
2044/// ```no_run
2045/// # #[cfg(feature = "otlp")] {
2046/// use sz_orm_tracing::OtlpConfig;
2047///
2048/// let config = OtlpConfig {
2049///     endpoint: "http://localhost:4317".to_string(),
2050///     service_name: "sz-orm-app".to_string(),
2051///     timeout_ms: 5000,
2052/// };
2053/// # }
2054/// ```
2055#[cfg(feature = "otlp")]
2056#[derive(Debug, Clone)]
2057pub struct OtlpConfig {
2058    /// OTLP gRPC endpoint (e.g., `http://localhost:4317`)
2059    pub endpoint: String,
2060    /// Service name (appears as the service.name tag in traces)
2061    pub service_name: String,
2062    /// Export timeout (milliseconds)
2063    pub timeout_ms: u64,
2064}
2065
2066#[cfg(feature = "otlp")]
2067impl Default for OtlpConfig {
2068    fn default() -> Self {
2069        Self {
2070            endpoint: "http://localhost:4317".to_string(),
2071            service_name: "sz-orm".to_string(),
2072            timeout_ms: 5000,
2073        }
2074    }
2075}
2076
2077/// Initialize the OTLP exporter
2078///
2079/// Integrates SZ-ORM tracing with OpenTelemetry so that Spans are automatically exported to the Collector.
2080///
2081/// # Errors
2082///
2083/// - `TracingError::OtlpInitFailed`: initialization failed
2084///
2085/// # Examples
2086///
2087/// ```no_run
2088/// # #[cfg(feature = "otlp")] {
2089/// # let rt = tokio::runtime::Runtime::new().unwrap();
2090/// # rt.block_on(async {
2091/// use sz_orm_tracing::{init_otlp_exporter, OtlpConfig};
2092///
2093/// let _guard = init_otlp_exporter(OtlpConfig::default()).await.unwrap();
2094/// // 此后通过 `Tracer` 上报的 Span 将自动导出到 OTLP Collector
2095/// # });
2096/// # }
2097/// ```
2098#[cfg(feature = "otlp")]
2099pub async fn init_otlp_exporter(config: OtlpConfig) -> Result<OtlpGuard, TracingError> {
2100    use opentelemetry_otlp::{SpanExporter, WithExportConfig};
2101    use opentelemetry_sdk::resource::Resource;
2102    use opentelemetry_sdk::runtime::Tokio;
2103    use opentelemetry_sdk::trace::TracerProvider;
2104    use std::time::Duration;
2105
2106    let exporter = SpanExporter::builder()
2107        .with_tonic()
2108        .with_endpoint(config.endpoint.clone())
2109        .with_timeout(Duration::from_millis(config.timeout_ms))
2110        .build()
2111        .map_err(|e| TracingError::OtlpInitFailed(format!("exporter build: {e}")))?;
2112
2113    let provider = TracerProvider::builder()
2114        .with_batch_exporter(exporter, Tokio)
2115        .with_resource(Resource::new_with_defaults([opentelemetry::KeyValue::new(
2116            "service.name",
2117            config.service_name.clone(),
2118        )]))
2119        .build();
2120
2121    // 设置为全局 provider
2122    opentelemetry::global::set_tracer_provider(provider.clone());
2123
2124    Ok(OtlpGuard { provider })
2125}
2126
2127/// OTLP exporter guard
2128///
2129/// Gracefully shuts down the exporter on drop, ensuring all Spans have been exported.
2130#[cfg(feature = "otlp")]
2131pub struct OtlpGuard {
2132    provider: opentelemetry_sdk::trace::TracerProvider,
2133}
2134
2135#[cfg(feature = "otlp")]
2136impl Drop for OtlpGuard {
2137    fn drop(&mut self) {
2138        // 优雅关闭:未发送的 span 会被丢弃(不阻塞)
2139        let _ = self.provider.shutdown();
2140    }
2141}