Skip to main content

sz_orm_tracing/
lib.rs

1//! # SZ-ORM Tracing — 链路追踪
2//!
3//! 提供分布式链路追踪的 Span/Tracer 抽象,支持 OTLP exporter 上报,
4//! 用于跨服务调用链的采集与可视化。
5//!
6//! ## 主要类型
7//!
8//! - [`Span`] — 单个追踪片段
9//! - `Tracer` — 追踪器与导出器
10//!
11//! ## 采样与上下文传播(`sampling` 模块)
12//!
13//! - [`sampling::Sampler`] / [`sampling::TraceIdRatioSampler`] — 采样策略
14//! - [`sampling::Baggage`] / [`sampling::BaggagePropagator`] — W3C Baggage 传播
15//! - [`sampling::BatchSpanExporter`] — 批量 Span 导出
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    /// 注入 W3C TraceContext `traceparent` header
199    ///
200    /// v0.2.2 修复 P2-2:从自定义 `trace-id`/`span-id`/`parent-span-id` header
201    /// 升级为 W3C TraceContext 标准的 `traceparent` header。
202    ///
203    /// 格式:`00-<trace_id>-<span_id>-<trace_flags>`
204    /// - `00`:版本号(W3C 规范固定为 `00`)
205    /// - `trace_id`:32 字符 hex(16 字节)
206    /// - `span_id`:16 字符 hex(8 字节)
207    /// - `trace_flags`:2 字符 hex(`01` 表示 sampled,`00` 表示未采样)
208    ///
209    /// 同时保留 `parent-span-id` header 以向后兼容旧版本的 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    /// 从 headers 中提取 Span
225    ///
226    /// v0.2.2 修复 P2-2:优先解析 W3C TraceContext `traceparent` header,
227    /// 若不存在则回退到 legacy 自定义 header(`trace-id`/`span-id`)。
228    ///
229    /// W3C `traceparent` 格式:`00-<trace_id>-<span_id>-<trace_flags>`
230    /// - 版本号必须为 `00`
231    /// - trace_id 必须为 32 字符 hex
232    /// - span_id 必须为 16 字符 hex
233    /// - trace_flags 必须为 2 字符 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    /// 解析 W3C TraceContext `traceparent` header
267    ///
268    /// 格式:`00-<trace_id>-<span_id>-<trace_flags>`
269    /// - 版本号必须为 `00`
270    /// - trace_id 必须为 32 字符 hex(不能全为 0)
271    /// - span_id 必须为 16 字符 hex(不能全为 0)
272    /// - trace_flags 必须为 2 字符 hex
273    ///
274    /// 返回 `Some(Span)` 表示解析成功,`None` 表示格式不合法。
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    /// 注入 legacy 自定义 header(向后兼容)
320    ///
321    /// v0.2.2 修复 P2-2:保留旧版 header 格式以兼容旧客户端。
322    /// 新代码应使用 [`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    /// 仅从 legacy header 中提取 Span(向后兼容)
336    ///
337    /// v0.2.2 修复 P2-2:保留旧版 header 解析以兼容旧客户端。
338    /// 新代码应使用 [`Tracer::extract`](自动优先 W3C,回退 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 修复 P2-2:**现已实现 W3C TraceContext `traceparent` header 传播**
367///   (`00-<trace_id>-<span_id>-<trace_flags>`),同时保留 `parent-span-id`
368///   header 以传递父 span 关系。`extract` 优先解析 W3C,回退到 legacy。
369/// - It does **not** perform sampling, baggage propagation, or context
370///   extraction across `async` boundaries.
371/// - Span IDs are generated from `std::collections::hash_map::RandomState`
372///   rather than per the OpenTelemetry specification (16 hex chars / 8 bytes
373///   random).
374///
375/// # When to use which
376///
377/// - Use [`SzTracer`] directly for in-process span collection and W3C
378///   TraceContext header propagation.
379/// - Use `OtelTracer` when an existing code base expects a tracer whose name
380///   signals OpenTelemetry-compatibility, but you have already decided to back
381///   it with SzTracer semantics.
382/// - For production distributed tracing across service boundaries, depend on
383///   the real `opentelemetry` SDK and use its `Tracer` implementation instead.
384///
385/// Both [`SzTracer`] and `OtelTracer` satisfy the [`Tracer`] trait, so they
386/// can be swapped at the type level without changing call sites.
387pub struct OtelTracer {
388    tracer: SzTracer,
389}
390
391impl OtelTracer {
392    /// Wraps a freshly-built [`SzTracer`] tagged with `service_name`.
393    pub fn new(service_name: impl Into<String>) -> Self {
394        Self {
395            tracer: SzTracer::new(service_name),
396        }
397    }
398
399    /// Provides read access to the underlying [`SzTracer`] so callers can
400    /// inspect accumulated spans or clear them between tests.
401    pub fn inner(&self) -> &SzTracer {
402        &self.tracer
403    }
404}
405
406impl Tracer for OtelTracer {
407    fn start_span(&self, operation_name: &str) -> Span {
408        self.tracer.start_span(operation_name)
409    }
410
411    fn end_span(&self, span: Span) {
412        self.tracer.end_span(span)
413    }
414
415    fn inject(&self, span: &Span) -> HashMap<String, String> {
416        self.tracer.inject(span)
417    }
418
419    fn extract(&self, headers: &HashMap<String, String>) -> Option<Span> {
420        self.tracer.extract(headers)
421    }
422}
423
424fn current_timestamp() -> i64 {
425    use std::time::{SystemTime, UNIX_EPOCH};
426    SystemTime::now()
427        .duration_since(UNIX_EPOCH)
428        .unwrap_or_default()
429        .as_millis() as i64
430}
431
432fn rand_u64() -> u64 {
433    use std::collections::hash_map::RandomState;
434    use std::hash::{BuildHasher, Hasher};
435    RandomState::new().build_hasher().finish()
436}
437
438#[derive(Debug)]
439pub enum TracingError {
440    SpanNotFound(String),
441    InvalidTraceId(String),
442    Internal(String),
443    /// OTLP exporter 初始化失败(feature = "otlp")
444    #[cfg(feature = "otlp")]
445    OtlpInitFailed(String),
446}
447
448impl std::fmt::Display for TracingError {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        match self {
451            TracingError::SpanNotFound(id) => write!(f, "Span not found: {}", id),
452            TracingError::InvalidTraceId(id) => write!(f, "Invalid trace id: {}", id),
453            TracingError::Internal(msg) => write!(f, "Tracing internal error: {}", msg),
454            #[cfg(feature = "otlp")]
455            TracingError::OtlpInitFailed(msg) => write!(f, "OTLP init failed: {}", msg),
456        }
457    }
458}
459
460impl std::error::Error for TracingError {}
461
462impl serde::Serialize for TracingError {
463    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
464    where
465        S: serde::Serializer,
466    {
467        serializer.serialize_str(&self.to_string())
468    }
469}
470
471// ===================== L4 SLA Monitoring =====================
472
473/// 延迟分位数直方图。
474///
475/// 使用排序数组实现,记录所有观测到的延迟样本,支持任意分位数查询。
476/// 适合金融级 SLA 监控场景下样本量可控的延迟统计;对于超大规模样本
477/// 应考虑 T-Digest 等近似算法以降低内存占用。
478#[derive(Debug, Clone)]
479pub struct LatencyHistogram {
480    samples: Vec<Duration>,
481    sum: Duration,
482}
483
484impl LatencyHistogram {
485    pub fn new(_buckets: Vec<Duration>) -> Self {
486        Self {
487            samples: Vec::new(),
488            sum: Duration::ZERO,
489        }
490    }
491
492    pub fn record(&mut self, duration: Duration) {
493        self.sum += duration;
494        let pos = self.samples.partition_point(|d| *d < duration);
495        self.samples.insert(pos, duration);
496    }
497
498    pub fn percentile(&self, p: f64) -> Option<Duration> {
499        if !(0.0..=100.0).contains(&p) || self.samples.is_empty() {
500            return None;
501        }
502        let n = self.samples.len();
503        // 最近排名法(Nearest Rank):rank = ceil(p/100 * n),至少为 1。
504        // 这是 SLA 监控的标准方法(Google SRE 推荐用法),保证
505        // p=0 返回最小值,p=100 返回最大值,且对高分位数偏保守。
506        let rank = ((p / 100.0) * n as f64).ceil() as usize;
507        let rank = rank.max(1).min(n);
508        Some(self.samples[rank - 1])
509    }
510
511    pub fn count(&self) -> usize {
512        self.samples.len()
513    }
514
515    pub fn mean(&self) -> Option<Duration> {
516        if self.samples.is_empty() {
517            None
518        } else {
519            Some(self.sum / self.samples.len() as u32)
520        }
521    }
522}
523
524/// 错误率计数器,基于滑动时间窗口统计错误率。
525///
526/// 窗口外的样本会在下一次 [`record`](Self::record) 调用时被驱逐,
527/// 保证 `rate()` 始终基于窗口内的最新样本计算。
528#[derive(Debug)]
529pub struct ErrorRateCounter {
530    window: Duration,
531    samples: Vec<(std::time::Instant, bool)>,
532}
533
534impl ErrorRateCounter {
535    pub fn new(window: Duration) -> Self {
536        Self {
537            window,
538            samples: Vec::new(),
539        }
540    }
541
542    pub fn record(&mut self, success: bool) {
543        let now = std::time::Instant::now();
544        let cutoff = now - self.window;
545        // 驱逐窗口外的样本(保留 cutoff 之后到达的样本)。
546        self.samples.retain(|(ts, _)| *ts >= cutoff);
547        self.samples.push((now, success));
548    }
549
550    pub fn rate(&self) -> f64 {
551        if self.samples.is_empty() {
552            return 0.0;
553        }
554        let errors = self.samples.iter().filter(|(_, ok)| !ok).count() as f64;
555        errors / self.samples.len() as f64
556    }
557
558    pub fn total(&self) -> usize {
559        self.samples.len()
560    }
561
562    pub fn errors(&self) -> usize {
563        self.samples.iter().filter(|(_, ok)| !ok).count()
564    }
565}
566
567/// 错误预算(Error Budget),基于 SLO 目标和滑动时间窗口。
568///
569/// 核心模型:每个错误消耗 `(1 - slo_target)` 单位的预算,预算容量为 1.0。
570/// 因此预算耗尽所需的错误数 = `1 / (1 - slo_target)`(例如 SLO=0.999 时为 1000 个错误)。
571/// 该模型假设窗口内基线流量为 `1/(1-SLO)` 个请求;这是金融级 SLA 监控中
572/// 常用的简化模型,适用于独立于流量统计的错误预算追踪。
573///
574/// 窗口外的 `consume` 调用会在下一次记录时被驱逐。
575#[derive(Debug)]
576pub struct ErrorBudget {
577    slo_target: f64,
578    window: Duration,
579    samples: Vec<(std::time::Instant, usize)>,
580}
581
582impl ErrorBudget {
583    pub fn new(slo_target: f64, window: Duration) -> Self {
584        Self {
585            slo_target,
586            window,
587            samples: Vec::new(),
588        }
589    }
590
591    pub fn consume(&mut self, error_count: usize) {
592        let now = std::time::Instant::now();
593        let cutoff = now - self.window;
594        self.samples.retain(|(ts, _)| *ts >= cutoff);
595        if error_count > 0 {
596            self.samples.push((now, error_count));
597        }
598    }
599
600    fn total_errors_in_window(&self) -> usize {
601        let now = std::time::Instant::now();
602        let cutoff = now - self.window;
603        self.samples
604            .iter()
605            .filter(|(ts, _)| *ts >= cutoff)
606            .map(|(_, n)| *n)
607            .sum()
608    }
609
610    pub fn remaining(&self) -> f64 {
611        let total_errors = self.total_errors_in_window();
612        if total_errors == 0 {
613            return 1.0;
614        }
615        let error_budget = 1.0 - self.slo_target;
616        if error_budget <= 0.0 {
617            // SLO = 1.0 意味着不允许任何错误,有错误即耗尽。
618            return 0.0;
619        }
620        let consumed = total_errors as f64 * error_budget;
621        (1.0 - consumed).clamp(0.0, 1.0)
622    }
623
624    pub fn is_exhausted(&self) -> bool {
625        self.remaining() == 0.0
626    }
627}
628
629/// 告警级别。
630#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
631pub enum AlertLevel {
632    Info,
633    Warning,
634    Critical,
635}
636
637/// 告警事件,由 [`SaturationGauge`] / [`SlaMonitor`] 等组件在检测到异常时产生,
638/// 通过 [`AlertHook`] 分发到下游(日志、Webhook 等)。
639#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
640pub struct Alert {
641    pub level: AlertLevel,
642    pub message: String,
643    pub timestamp: DateTime<Utc>,
644    pub operation: Option<String>,
645}
646
647/// 饱和度告警仪表盘。
648///
649/// 跟踪单一饱和度数值(取值约定为 `[0.0, 1.0]`),当数值 `>= threshold`
650/// 时判定为饱和并触发 [`AlertLevel::Critical`] 告警。
651#[derive(Debug)]
652pub struct SaturationGauge {
653    threshold: f64,
654    value: f64,
655}
656
657impl SaturationGauge {
658    pub fn new(threshold: f64) -> Self {
659        Self {
660            threshold,
661            value: 0.0,
662        }
663    }
664
665    pub fn set(&mut self, value: f64) {
666        self.value = value;
667    }
668
669    pub fn is_saturated(&self) -> bool {
670        self.value >= self.threshold
671    }
672
673    pub fn check_alert(&self) -> Option<Alert> {
674        if self.is_saturated() {
675            Some(Alert {
676                level: AlertLevel::Critical,
677                message: format!(
678                    "saturation {:.2} exceeded threshold {:.2}",
679                    self.value, self.threshold
680                ),
681                timestamp: Utc::now(),
682                operation: None,
683            })
684        } else {
685            None
686        }
687    }
688}
689
690/// 告警分发钩子。实现方可以将告警写入日志、发送到 Webhook、推送到 IM 等。
691///
692/// 必须实现 `Send + Sync` 以便在多线程环境中作为 `Arc<dyn AlertHook>` 共享。
693pub trait AlertHook: Send + Sync {
694    /// 处理告警事件。成功返回 `Ok(())`,失败返回错误描述字符串。
695    fn notify(&self, alert: &Alert) -> Result<(), String>;
696}
697
698/// 将告警输出到标准错误流的简单实现。
699///
700/// 不依赖外部 `log` crate,避免在测试或最小化部署场景下引入额外配置。
701pub struct LogAlertHook;
702
703impl LogAlertHook {
704    pub fn new() -> Self {
705        Self
706    }
707}
708
709impl Default for LogAlertHook {
710    fn default() -> Self {
711        Self::new()
712    }
713}
714
715impl AlertHook for LogAlertHook {
716    fn notify(&self, alert: &Alert) -> Result<(), String> {
717        eprintln!(
718            "[SLA ALERT] level={:?} op={:?} ts={} msg={}",
719            alert.level, alert.operation, alert.timestamp, alert.message
720        );
721        Ok(())
722    }
723}
724
725/// 内存告警钩子(**非真实 Webhook**):将告警存入内存,不进行任何网络发送。
726///
727/// 类型名刻意改为 `InMemoryAlertHook`(原 `WebhookAlertHook` 名字暗示 HTTP 调用,
728/// 但实际只 push 到 Vec,会误导用户)。`identifier` 字段仅用于测试断言与调试,
729/// **不会被用于任何 HTTP 请求**。
730///
731/// 如需真实 Webhook 推送,请实现自定义 `AlertHook` 并在 `notify` 中使用
732/// `reqwest` 或 `hyper` 发起 HTTP 请求。
733///
734/// 用于测试和本地开发环境,可通过 [`sent_alerts`](Self::sent_alerts)
735/// 检查被分发过的告警。
736pub struct InMemoryAlertHook {
737    identifier: String,
738    sent: RwLock<Vec<Alert>>,
739}
740
741impl InMemoryAlertHook {
742    /// 创建内存告警钩子。`identifier` 仅作调试标识,不参与网络调用。
743    pub fn new(identifier: String) -> Self {
744        Self {
745            identifier,
746            sent: RwLock::new(Vec::new()),
747        }
748    }
749
750    /// 返回到目前为止已"发送"的告警快照(拷贝)。
751    pub fn sent_alerts(&self) -> Vec<Alert> {
752        self.sent
753            .read()
754            .map(|guard| guard.clone())
755            .unwrap_or_default()
756    }
757
758    /// 暴露配置的标识符,便于调用方调试或断言。
759    pub fn identifier(&self) -> &str {
760        &self.identifier
761    }
762
763    /// 向后兼容:返回 identifier(原 url 字段的别名)。
764    ///
765    /// # 已废弃
766    ///
767    /// 此方法仅为减少破坏性变更而保留,新代码应使用 [`identifier`](Self::identifier)。
768    #[deprecated(since = "1.2.0", note = "use `identifier()` instead; this hook does not perform HTTP")]
769    pub fn url(&self) -> &str {
770        &self.identifier
771    }
772}
773
774impl AlertHook for InMemoryAlertHook {
775    fn notify(&self, alert: &Alert) -> Result<(), String> {
776        match self.sent.write() {
777            Ok(mut guard) => {
778                guard.push(alert.clone());
779                Ok(())
780            }
781            Err(e) => Err(format!("in-memory alert hook lock poisoned: {e}")),
782        }
783    }
784}
785
786/// 单个操作的 SLA 统计聚合(由 [`SlaMonitor`] 的 `RwLock` 保护线程安全)。
787///
788/// 同时维护延迟直方图与累计错误计数,避免 [`ErrorRateCounter`] 的滑动窗口
789/// 在长周期 SLA 报告中丢失历史样本。
790struct OperationStats {
791    latency: LatencyHistogram,
792    total_count: usize,
793    error_count: usize,
794}
795
796impl OperationStats {
797    fn new() -> Self {
798        Self {
799            latency: LatencyHistogram::new(Vec::new()),
800            total_count: 0,
801            error_count: 0,
802        }
803    }
804
805    fn observe(&mut self, duration: Duration, success: bool) {
806        self.latency.record(duration);
807        self.total_count += 1;
808        if !success {
809            self.error_count += 1;
810        }
811    }
812
813    fn error_rate(&self) -> f64 {
814        if self.total_count == 0 {
815            0.0
816        } else {
817            self.error_count as f64 / self.total_count as f64
818        }
819    }
820}
821
822/// SLA 监控报告快照,由 [`SlaMonitor::report`] 生成。
823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
824pub struct SlaReport {
825    /// P50 延迟(毫秒)。
826    pub p50_ms: f64,
827    /// P95 延迟(毫秒)。
828    pub p95_ms: f64,
829    /// P99 延迟(毫秒)。
830    pub p99_ms: f64,
831    /// 错误率,范围 `[0.0, 1.0]`。
832    pub error_rate: f64,
833    /// 总观测次数。
834    pub total_count: usize,
835    /// SLO 目标成功率(如 `0.999`)。
836    pub slo_target: f64,
837    /// 剩余错误预算,范围 `[0.0, 1.0]`,`1.0` 表示预算完整。
838    pub error_budget_remaining: f64,
839    /// 饱和度,范围 `[0.0, 1.0]`,`1.0` 表示错误预算已耗尽。
840    pub saturation: f64,
841}
842
843/// SLA 监控器,按操作名聚合延迟与错误统计,并生成 [`SlaReport`]。
844///
845/// 内部使用 `RwLock<HashMap>` 实现线程安全,`observe` 通过 `&self` 提供
846/// 内部可变性,因此可通过 `Arc<SlaMonitor>` 在多线程中并发调用。
847pub struct SlaMonitor {
848    slo_target: f64,
849    operations: RwLock<HashMap<String, OperationStats>>,
850}
851
852impl SlaMonitor {
853    pub fn new(slo_target: f64) -> Self {
854        Self {
855            slo_target,
856            operations: RwLock::new(HashMap::new()),
857        }
858    }
859
860    /// 记录一次操作观测:延迟与成功/失败。
861    ///
862    /// 使用 `&self` 而非 `&mut self`,以便通过 `Arc<SlaMonitor>` 在多线程
863    /// 中并发写入(与项目现有 `SzTracer::end_span` 模式一致)。
864    pub fn observe(&self, operation: &str, duration: Duration, success: bool) {
865        if let Ok(mut ops) = self.operations.write() {
866            let stats = ops
867                .entry(operation.to_string())
868                .or_insert_with(OperationStats::new);
869            stats.observe(duration, success);
870        }
871    }
872
873    pub fn report(&self, operation: &str) -> Option<SlaReport> {
874        let ops = self.operations.read().ok()?;
875        let stats = ops.get(operation)?;
876        let error_rate = stats.error_rate();
877        let error_budget = 1.0 - self.slo_target;
878        let (saturation, error_budget_remaining) = if error_budget <= 0.0 {
879            // SLO = 1.0 意味着不允许任何错误:有错误即耗尽预算。
880            if error_rate == 0.0 {
881                (0.0, 1.0)
882            } else {
883                (1.0, 0.0)
884            }
885        } else {
886            let sat = (error_rate / error_budget).clamp(0.0, 1.0);
887            (sat, 1.0 - sat)
888        };
889        let ms = |p: f64| {
890            stats
891                .latency
892                .percentile(p)
893                .map(|d| d.as_nanos() as f64 / 1_000_000.0)
894                .unwrap_or(0.0)
895        };
896        Some(SlaReport {
897            p50_ms: ms(50.0),
898            p95_ms: ms(95.0),
899            p99_ms: ms(99.0),
900            error_rate,
901            total_count: stats.total_count,
902            slo_target: self.slo_target,
903            error_budget_remaining,
904            saturation,
905        })
906    }
907
908    pub fn operations(&self) -> Vec<String> {
909        self.operations
910            .read()
911            .map(|ops| ops.keys().cloned().collect())
912            .unwrap_or_default()
913    }
914}
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919
920    #[test]
921    fn test_span_new() {
922        let span = Span::new("trace1", "span1", "operation1");
923        assert_eq!(span.trace_id, "trace1");
924        assert_eq!(span.span_id, "span1");
925        assert_eq!(span.operation_name, "operation1");
926        assert!(span.end_time.is_none());
927    }
928
929    #[test]
930    fn test_span_with_parent() {
931        let span = Span::new("trace1", "span1", "op").with_parent("parent1");
932        assert_eq!(span.parent_id, Some("parent1".to_string()));
933    }
934
935    #[test]
936    fn test_span_with_service() {
937        let span = Span::new("trace1", "span1", "op").with_service("my-service");
938        assert_eq!(span.service_name, "my-service");
939    }
940
941    #[test]
942    fn test_span_with_tag() {
943        let span = Span::new("trace1", "span1", "op").with_tag("key", "value");
944        assert_eq!(span.tags.get("key"), Some(&"value".to_string()));
945    }
946
947    #[test]
948    fn test_span_finish() {
949        let mut span = Span::new("trace1", "span1", "op");
950        span.finish();
951        assert!(span.end_time.is_some());
952        assert!(span.duration().is_some());
953    }
954
955    #[test]
956    fn test_span_add_log() {
957        let mut span = Span::new("trace1", "span1", "op");
958        span.add_log("test log");
959        assert_eq!(span.logs.len(), 1);
960        assert_eq!(span.logs[0].message, "test log");
961    }
962
963    #[test]
964    fn test_tracer_new() {
965        let tracer = SzTracer::new("test-service");
966        assert!(tracer.get_spans().is_empty());
967    }
968
969    #[test]
970    fn test_tracer_start_span() {
971        let tracer = SzTracer::new("test-service");
972        let span = tracer.start_span("test-operation");
973        assert_eq!(span.operation_name, "test-operation");
974    }
975
976    #[test]
977    fn test_tracer_end_span() {
978        let tracer = SzTracer::new("test-service");
979        let span = tracer.start_span("test-operation");
980        tracer.end_span(span);
981
982        let spans = tracer.get_spans();
983        assert_eq!(spans.len(), 1);
984    }
985
986    #[test]
987    fn test_tracer_inject() {
988        let tracer = SzTracer::new("test-service");
989        let span = tracer.start_span("test");
990        let headers = tracer.inject(&span);
991
992        // v0.2.2 修复 P2-2:默认使用 W3C traceparent header
993        let tp = headers
994            .get("traceparent")
995            .expect("traceparent header must be present");
996        // 格式:00-<trace_id>-<span_id>-01
997        let parts: Vec<&str> = tp.split('-').collect();
998        assert_eq!(parts.len(), 4);
999        assert_eq!(parts[0], "00"); // 版本号
1000        assert_eq!(parts[1], span.trace_id);
1001        assert_eq!(parts[2], span.span_id);
1002        assert_eq!(parts[3], "01"); // sampled
1003    }
1004
1005    #[test]
1006    fn test_tracer_extract() {
1007        let tracer = SzTracer::new("test-service");
1008        let mut headers = HashMap::new();
1009        // W3C traceparent 格式
1010        let trace_id = "0af7651916cd43dd8448eb211c80319c";
1011        let span_id = "b7ad6b7169203331";
1012        headers.insert(
1013            "traceparent".to_string(),
1014            format!("00-{}-{}-01", trace_id, span_id),
1015        );
1016
1017        let span = tracer.extract(&headers);
1018        assert!(span.is_some());
1019        let span = span.unwrap();
1020        assert_eq!(span.trace_id, trace_id);
1021        assert_eq!(span.span_id, span_id);
1022    }
1023
1024    #[test]
1025    fn test_tracer_extract_legacy_headers() {
1026        // v0.2.2 修复 P2-2:向后兼容 legacy header
1027        let tracer = SzTracer::new("test-service");
1028        let mut headers = HashMap::new();
1029        headers.insert("trace-id".to_string(), "trace123".to_string());
1030        headers.insert("span-id".to_string(), "span456".to_string());
1031
1032        let span = tracer.extract(&headers);
1033        assert!(span.is_some());
1034        let span = span.unwrap();
1035        assert_eq!(span.trace_id, "trace123");
1036        assert_eq!(span.span_id, "span456");
1037    }
1038
1039    #[test]
1040    fn test_tracer_extract_missing_headers() {
1041        let tracer = SzTracer::new("test-service");
1042        let headers = HashMap::new();
1043        let span = tracer.extract(&headers);
1044        assert!(span.is_none());
1045    }
1046
1047    // ===================== v0.2.2 修复 P2-2:W3C TraceContext 测试 =====================
1048
1049    #[test]
1050    fn test_parse_traceparent_valid() {
1051        let valid = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
1052        let span = SzTracer::parse_traceparent(valid).expect("valid traceparent must parse");
1053        assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1054        assert_eq!(span.span_id, "b7ad6b7169203331");
1055    }
1056
1057    #[test]
1058    fn test_parse_traceparent_invalid_version() {
1059        // 版本号不是 2 字符 hex
1060        assert!(SzTracer::parse_traceparent(
1061            "0-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1062        )
1063        .is_none());
1064        // 版本号不是 hex
1065        assert!(SzTracer::parse_traceparent(
1066            "xy-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1067        )
1068        .is_none());
1069    }
1070
1071    #[test]
1072    fn test_parse_traceparent_invalid_trace_id_length() {
1073        // trace_id 不是 32 字符
1074        assert!(SzTracer::parse_traceparent("00-short-b7ad6b7169203331-01").is_none());
1075    }
1076
1077    #[test]
1078    fn test_parse_traceparent_invalid_span_id_length() {
1079        // span_id 不是 16 字符
1080        assert!(
1081            SzTracer::parse_traceparent("00-0af7651916cd43dd8448eb211c80319c-short-01").is_none()
1082        );
1083    }
1084
1085    #[test]
1086    fn test_parse_traceparent_all_zeros_trace_id_rejected() {
1087        // W3C 规范:trace_id 不能全为 0
1088        let all_zero = "00-00000000000000000000000000000000-b7ad6b7169203331-01";
1089        assert!(SzTracer::parse_traceparent(all_zero).is_none());
1090    }
1091
1092    #[test]
1093    fn test_parse_traceparent_all_zeros_span_id_rejected() {
1094        // W3C 规范:span_id 不能全为 0
1095        let all_zero = "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01";
1096        assert!(SzTracer::parse_traceparent(all_zero).is_none());
1097    }
1098
1099    #[test]
1100    fn test_parse_traceparent_invalid_flags() {
1101        // trace_flags 不是 2 字符 hex
1102        assert!(SzTracer::parse_traceparent(
1103            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-xyz"
1104        )
1105        .is_none());
1106    }
1107
1108    #[test]
1109    fn test_parse_traceparent_wrong_part_count() {
1110        // 不是 4 个部分
1111        assert!(SzTracer::parse_traceparent(
1112            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331"
1113        )
1114        .is_none());
1115        assert!(SzTracer::parse_traceparent(
1116            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01-extra"
1117        )
1118        .is_none());
1119    }
1120
1121    #[test]
1122    fn test_inject_legacy_preserves_old_format() {
1123        let tracer = SzTracer::new("svc");
1124        let span = tracer.start_span("op");
1125        let headers = tracer.inject_legacy(&span);
1126
1127        assert_eq!(headers.get("trace-id"), Some(&span.trace_id.to_string()));
1128        assert_eq!(headers.get("span-id"), Some(&span.span_id.to_string()));
1129        assert!(!headers.contains_key("parent-span-id"));
1130    }
1131
1132    #[test]
1133    fn test_extract_legacy_preserves_old_format() {
1134        let tracer = SzTracer::new("svc");
1135        let mut headers = HashMap::new();
1136        headers.insert("trace-id".to_string(), "abc".to_string());
1137        headers.insert("span-id".to_string(), "def".to_string());
1138
1139        let span = tracer.extract_legacy(&headers).expect("legacy extract");
1140        assert_eq!(span.trace_id, "abc");
1141        assert_eq!(span.span_id, "def");
1142    }
1143
1144    #[test]
1145    fn test_w3c_traceparent_roundtrip_preserves_ids() {
1146        // inject → extract 必须保留 trace_id 和 span_id
1147        let tracer = SzTracer::new("svc");
1148        let original = tracer.start_span("roundtrip");
1149        let headers = tracer.inject(&original);
1150
1151        let extracted = tracer.extract(&headers).expect("roundtrip extract");
1152        assert_eq!(extracted.trace_id(), original.trace_id());
1153        assert_eq!(extracted.span_id(), original.span_id());
1154        assert!(extracted.parent_id().is_none());
1155    }
1156
1157    #[test]
1158    fn test_w3c_prefers_traceparent_over_legacy() {
1159        // 同时存在 traceparent 和 legacy header 时,优先 W3C
1160        let tracer = SzTracer::new("svc");
1161        let mut headers = HashMap::new();
1162        headers.insert(
1163            "traceparent".to_string(),
1164            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
1165        );
1166        headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1167        headers.insert("span-id".to_string(), "legacy-span".to_string());
1168
1169        let span = tracer.extract(&headers).expect("extract");
1170        assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1171        assert_eq!(span.span_id, "b7ad6b7169203331");
1172    }
1173
1174    #[test]
1175    fn test_w3c_falls_back_to_legacy_on_invalid_traceparent() {
1176        // traceparent 格式不合法时回退到 legacy
1177        let tracer = SzTracer::new("svc");
1178        let mut headers = HashMap::new();
1179        headers.insert("traceparent".to_string(), "invalid-format".to_string());
1180        headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1181        headers.insert("span-id".to_string(), "legacy-span".to_string());
1182
1183        let span = tracer
1184            .extract(&headers)
1185            .expect("should fall back to legacy");
1186        assert_eq!(span.trace_id, "legacy-trace");
1187        assert_eq!(span.span_id, "legacy-span");
1188    }
1189
1190    #[test]
1191    fn test_otel_tracer() {
1192        let tracer = OtelTracer::new("test-service");
1193        let span = tracer.start_span("test-operation");
1194        assert_eq!(span.operation_name, "test-operation");
1195    }
1196
1197    #[test]
1198    fn test_otel_tracer_is_a_sz_tracer_wrapper_not_a_real_otel_sdk() {
1199        // OtelTracer delegates every Tracer method to SzTracer. Document the
1200        // contract: spans produced via OtelTracer must be observable through
1201        // the underlying SzTracer (i.e. `inner().get_spans()`).
1202        let tracer = OtelTracer::new("svc");
1203        assert!(tracer.inner().get_spans().is_empty());
1204
1205        let span = tracer.start_span("op");
1206        assert_eq!(span.service_name(), "svc");
1207        tracer.end_span(span);
1208
1209        let spans = tracer.inner().get_spans();
1210        assert_eq!(spans.len(), 1);
1211        assert_eq!(spans[0].operation_name(), "op");
1212        assert!(spans[0].end_time.is_some());
1213    }
1214
1215    #[test]
1216    fn test_otel_tracer_inject_extract_roundtrip_preserves_ids() {
1217        // v0.2.2 修复 P2-2:现在使用 W3C traceparent header
1218        let tracer = OtelTracer::new("svc");
1219        let original = tracer.start_span("roundtrip");
1220        let headers = tracer.inject(&original);
1221
1222        // W3C traceparent 必须存在并包含原始 ID
1223        let tp = headers
1224            .get("traceparent")
1225            .expect("traceparent must be present");
1226        assert!(tp.contains(original.trace_id()));
1227        assert!(tp.contains(original.span_id()));
1228        assert!(!headers.contains_key("parent-span-id"));
1229
1230        let extracted = tracer.extract(&headers).expect("extract should round-trip");
1231        assert_eq!(extracted.trace_id(), original.trace_id());
1232        assert_eq!(extracted.span_id(), original.span_id());
1233        assert!(extracted.parent_id().is_none());
1234    }
1235
1236    #[test]
1237    fn test_otel_tracer_preserves_parent_id_through_roundtrip() {
1238        let tracer = OtelTracer::new("svc");
1239        let parent = tracer.start_span("parent");
1240        let child = tracer
1241            .start_span("child")
1242            .with_parent(parent.span_id().to_string());
1243        let headers = tracer.inject(&child);
1244
1245        // parent-span-id 仍然通过独立 header 传递(向后兼容)
1246        assert_eq!(
1247            headers.get("parent-span-id"),
1248            Some(&parent.span_id().to_string())
1249        );
1250
1251        let extracted = tracer.extract(&headers).expect("extract should round-trip");
1252        assert_eq!(extracted.parent_id(), Some(parent.span_id()));
1253    }
1254
1255    #[test]
1256    fn test_otel_tracer_extract_returns_none_without_required_headers() {
1257        let tracer = OtelTracer::new("svc");
1258        let headers: HashMap<String, String> = HashMap::new();
1259        assert!(tracer.extract(&headers).is_none());
1260
1261        // 仅 legacy trace-id 缺失 span-id 时也必须失败
1262        let mut partial = HashMap::new();
1263        partial.insert("trace-id".to_string(), "abc".to_string());
1264        // Missing span-id - extract must fail.
1265        assert!(tracer.extract(&partial).is_none());
1266
1267        // 仅 W3C traceparent 格式错误时(且无 legacy 回退)必须失败
1268        let mut bad_w3c = HashMap::new();
1269        bad_w3c.insert("traceparent".to_string(), "garbage".to_string());
1270        assert!(tracer.extract(&bad_w3c).is_none());
1271    }
1272
1273    #[test]
1274    fn test_otel_tracer_generated_ids_have_correct_length() {
1275        // OpenTelemetry spec: trace_id is 16 bytes (32 hex chars), span_id
1276        // is 8 bytes (16 hex chars). OtelTracer inherits SzTracer's generator
1277        // and must produce the same shape so consumers parsing the IDs do not
1278        // trip over a different length.
1279        let trace_id = SzTracer::generate_trace_id();
1280        let span_id = SzTracer::generate_span_id();
1281        assert_eq!(trace_id.len(), 32, "trace_id must be 32 hex chars");
1282        assert_eq!(span_id.len(), 16, "span_id must be 16 hex chars");
1283
1284        // Hex-only.
1285        assert!(trace_id.chars().all(|c| c.is_ascii_hexdigit()));
1286        assert!(span_id.chars().all(|c| c.is_ascii_hexdigit()));
1287    }
1288
1289    #[test]
1290    fn test_span_accessors() {
1291        let span = Span::new("trace1", "span1", "test-op")
1292            .with_service("svc")
1293            .with_tag("k", "v");
1294
1295        assert_eq!(span.trace_id(), "trace1");
1296        assert_eq!(span.span_id(), "span1");
1297        assert_eq!(span.operation_name(), "test-op");
1298        assert_eq!(span.service_name(), "svc");
1299        assert_eq!(span.tags().get("k"), Some(&"v".to_string()));
1300    }
1301
1302    #[test]
1303    fn test_generate_ids() {
1304        let trace_id = SzTracer::generate_trace_id();
1305        let span_id = SzTracer::generate_span_id();
1306
1307        assert_eq!(trace_id.len(), 32);
1308        assert_eq!(span_id.len(), 16);
1309    }
1310
1311    #[test]
1312    fn test_tracer_clear() {
1313        let tracer = SzTracer::new("test-service");
1314
1315        let span = tracer.start_span("op1");
1316        tracer.end_span(span);
1317        let span = tracer.start_span("op2");
1318        tracer.end_span(span);
1319
1320        assert_eq!(tracer.get_spans().len(), 2);
1321
1322        tracer.clear();
1323        assert!(tracer.get_spans().is_empty());
1324    }
1325
1326    // ===================== LatencyHistogram tests =====================
1327
1328    #[test]
1329    fn test_latency_histogram_new_empty() {
1330        let hist = LatencyHistogram::new(vec![
1331            Duration::from_millis(10),
1332            Duration::from_millis(100),
1333            Duration::from_millis(1000),
1334        ]);
1335        assert_eq!(hist.count(), 0);
1336        assert!(hist.percentile(50.0).is_none());
1337        assert!(hist.mean().is_none());
1338    }
1339
1340    #[test]
1341    fn test_latency_histogram_record_single() {
1342        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1343        hist.record(Duration::from_millis(50));
1344        assert_eq!(hist.count(), 1);
1345        assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(50)));
1346        assert_eq!(hist.mean(), Some(Duration::from_millis(50)));
1347    }
1348
1349    #[test]
1350    fn test_latency_histogram_percentile_p50_sorted() {
1351        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1352        for ms in [10, 20, 30, 40, 50] {
1353            hist.record(Duration::from_millis(ms));
1354        }
1355        // 5 samples sorted: [10, 20, 30, 40, 50], p50 should be 30 (median)
1356        assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1357    }
1358
1359    #[test]
1360    fn test_latency_histogram_percentile_p95_high_value() {
1361        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1362        for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1363            hist.record(Duration::from_millis(ms));
1364        }
1365        // p95 of 10 samples should be the 9th or 10th value (high)
1366        let p95 = hist.percentile(95.0).expect("p95 must exist");
1367        assert!(p95 >= Duration::from_millis(9));
1368    }
1369
1370    #[test]
1371    fn test_latency_histogram_percentile_p99_max_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        let p99 = hist.percentile(99.0).expect("p99 must exist");
1377        assert_eq!(p99, Duration::from_millis(100));
1378    }
1379
1380    #[test]
1381    fn test_latency_histogram_percentile_p0_min_value() {
1382        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1383        for ms in [10, 20, 30] {
1384            hist.record(Duration::from_millis(ms));
1385        }
1386        assert_eq!(hist.percentile(0.0), Some(Duration::from_millis(10)));
1387    }
1388
1389    #[test]
1390    fn test_latency_histogram_percentile_p100_max_value() {
1391        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1392        for ms in [10, 20, 30] {
1393            hist.record(Duration::from_millis(ms));
1394        }
1395        assert_eq!(hist.percentile(100.0), Some(Duration::from_millis(30)));
1396    }
1397
1398    #[test]
1399    fn test_latency_histogram_percentile_empty_returns_none() {
1400        let hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1401        assert!(hist.percentile(50.0).is_none());
1402    }
1403
1404    #[test]
1405    fn test_latency_histogram_percentile_out_of_range_returns_none() {
1406        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1407        hist.record(Duration::from_millis(50));
1408        assert!(hist.percentile(-1.0).is_none());
1409        assert!(hist.percentile(101.0).is_none());
1410    }
1411
1412    #[test]
1413    fn test_latency_histogram_count() {
1414        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1415        assert_eq!(hist.count(), 0);
1416        hist.record(Duration::from_millis(10));
1417        hist.record(Duration::from_millis(20));
1418        hist.record(Duration::from_millis(30));
1419        assert_eq!(hist.count(), 3);
1420    }
1421
1422    #[test]
1423    fn test_latency_histogram_mean_multiple() {
1424        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1425        for ms in [10, 20, 30, 40, 50] {
1426            hist.record(Duration::from_millis(ms));
1427        }
1428        // mean = (10+20+30+40+50)/5 = 30
1429        assert_eq!(hist.mean(), Some(Duration::from_millis(30)));
1430    }
1431
1432    #[test]
1433    fn test_latency_histogram_record_unsorted_input_stays_sorted() {
1434        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1435        hist.record(Duration::from_millis(50));
1436        hist.record(Duration::from_millis(10));
1437        hist.record(Duration::from_millis(30));
1438        // p50 should be 30 (median of sorted [10,30,50])
1439        assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1440    }
1441
1442    // ===================== ErrorRateCounter tests =====================
1443
1444    #[test]
1445    fn test_error_rate_counter_new_empty() {
1446        let counter = ErrorRateCounter::new(Duration::from_secs(60));
1447        assert_eq!(counter.total(), 0);
1448        assert_eq!(counter.errors(), 0);
1449        assert_eq!(counter.rate(), 0.0);
1450    }
1451
1452    #[test]
1453    fn test_error_rate_counter_all_success_rate_zero() {
1454        let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1455        for _ in 0..10 {
1456            counter.record(true);
1457        }
1458        assert_eq!(counter.total(), 10);
1459        assert_eq!(counter.errors(), 0);
1460        assert_eq!(counter.rate(), 0.0);
1461    }
1462
1463    #[test]
1464    fn test_error_rate_counter_all_failures_rate_one() {
1465        let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1466        for _ in 0..10 {
1467            counter.record(false);
1468        }
1469        assert_eq!(counter.total(), 10);
1470        assert_eq!(counter.errors(), 10);
1471        assert_eq!(counter.rate(), 1.0);
1472    }
1473
1474    #[test]
1475    fn test_error_rate_counter_mixed_rate() {
1476        let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1477        // 7 success, 3 failures -> rate = 0.3
1478        for _ in 0..7 {
1479            counter.record(true);
1480        }
1481        for _ in 0..3 {
1482            counter.record(false);
1483        }
1484        assert_eq!(counter.total(), 10);
1485        assert_eq!(counter.errors(), 3);
1486        let rate = counter.rate();
1487        assert!((rate - 0.3).abs() < 1e-9, "expected 0.3, got {rate}");
1488    }
1489
1490    #[test]
1491    fn test_error_rate_counter_empty_rate_is_zero() {
1492        let counter = ErrorRateCounter::new(Duration::from_secs(60));
1493        // no samples -> rate must be 0.0 (avoid div-by-zero)
1494        assert_eq!(counter.rate(), 0.0);
1495    }
1496
1497    #[test]
1498    fn test_error_rate_counter_window_expires_old_samples() {
1499        // window of 100ms; old samples should be evicted on subsequent record.
1500        let mut counter = ErrorRateCounter::new(Duration::from_millis(100));
1501        counter.record(false);
1502        counter.record(false);
1503        // sleep past the window
1504        std::thread::sleep(Duration::from_millis(120));
1505        counter.record(true);
1506        // Only the recent success should remain -> rate = 0.0, total = 1
1507        assert_eq!(counter.total(), 1);
1508        assert_eq!(counter.errors(), 0);
1509        assert_eq!(counter.rate(), 0.0);
1510    }
1511
1512    // ===================== ErrorBudget tests =====================
1513
1514    #[test]
1515    fn test_error_budget_new_is_full() {
1516        let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1517        assert!((budget.remaining() - 1.0).abs() < 1e-9);
1518        assert!(!budget.is_exhausted());
1519    }
1520
1521    #[test]
1522    fn test_error_budget_consume_reduces_remaining() {
1523        let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1524        // SLO 0.999 -> error_budget = 0.001 per error.
1525        budget.consume(1);
1526        // remaining = 1 - 1 * 0.001 = 0.999
1527        assert!((budget.remaining() - 0.999).abs() < 1e-9);
1528        assert!(!budget.is_exhausted());
1529    }
1530
1531    #[test]
1532    fn test_error_budget_exhausted_at_capacity() {
1533        let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1534        // capacity = 1 / (1 - 0.999) = 1000 errors
1535        budget.consume(1000);
1536        assert_eq!(budget.remaining(), 0.0);
1537        assert!(budget.is_exhausted());
1538    }
1539
1540    #[test]
1541    fn test_error_budget_over_consume_clamps_to_zero() {
1542        let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1543        budget.consume(2000);
1544        assert_eq!(budget.remaining(), 0.0);
1545        assert!(budget.is_exhausted());
1546    }
1547
1548    #[test]
1549    fn test_error_budget_zero_errors_returns_one() {
1550        let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1551        assert_eq!(budget.remaining(), 1.0);
1552        assert!(!budget.is_exhausted());
1553    }
1554
1555    #[test]
1556    fn test_error_budget_window_refills_after_expiry() {
1557        // SLO 0.5 -> error_budget = 0.5 per error -> 2 errors exhaust.
1558        let mut budget = ErrorBudget::new(0.5, Duration::from_millis(100));
1559        budget.consume(2);
1560        assert!(budget.is_exhausted());
1561        // wait for window to expire
1562        std::thread::sleep(Duration::from_millis(120));
1563        assert!((budget.remaining() - 1.0).abs() < 1e-9);
1564        assert!(!budget.is_exhausted());
1565    }
1566
1567    #[test]
1568    fn test_error_budget_slo_one_means_no_errors_allowed() {
1569        // SLO = 1.0 -> error_budget = 0 -> any consume exhausts
1570        let mut budget = ErrorBudget::new(1.0, Duration::from_secs(60));
1571        assert_eq!(budget.remaining(), 1.0);
1572        budget.consume(1);
1573        assert_eq!(budget.remaining(), 0.0);
1574        assert!(budget.is_exhausted());
1575    }
1576
1577    #[test]
1578    fn test_error_budget_multiple_consumes_accumulate() {
1579        let mut budget = ErrorBudget::new(0.99, Duration::from_secs(60));
1580        // SLO 0.99 -> error_budget = 0.01 -> capacity = 100 errors
1581        budget.consume(30);
1582        assert!((budget.remaining() - 0.7).abs() < 1e-9);
1583        budget.consume(30);
1584        assert!((budget.remaining() - 0.4).abs() < 1e-9);
1585        budget.consume(40);
1586        assert_eq!(budget.remaining(), 0.0);
1587        assert!(budget.is_exhausted());
1588    }
1589
1590    // ===================== Alert / AlertLevel tests =====================
1591
1592    #[test]
1593    fn test_alert_level_variants_exist() {
1594        let info = AlertLevel::Info;
1595        let warning = AlertLevel::Warning;
1596        let critical = AlertLevel::Critical;
1597        // Sanity: ensure variants are distinct (debug repr).
1598        assert_ne!(format!("{info:?}"), format!("{warning:?}"));
1599        assert_ne!(format!("{warning:?}"), format!("{critical:?}"));
1600        assert_ne!(format!("{info:?}"), format!("{critical:?}"));
1601    }
1602
1603    #[test]
1604    fn test_alert_construction_with_all_fields() {
1605        let ts = Utc::now();
1606        let alert = Alert {
1607            level: AlertLevel::Critical,
1608            message: "p99 latency exceeded budget".to_string(),
1609            timestamp: ts,
1610            operation: Some("query_user".to_string()),
1611        };
1612        assert_eq!(alert.level, AlertLevel::Critical);
1613        assert_eq!(alert.message, "p99 latency exceeded budget");
1614        assert_eq!(alert.timestamp, ts);
1615        assert_eq!(alert.operation.as_deref(), Some("query_user"));
1616    }
1617
1618    #[test]
1619    fn test_alert_construction_without_operation() {
1620        let alert = Alert {
1621            level: AlertLevel::Info,
1622            message: "system healthy".to_string(),
1623            timestamp: Utc::now(),
1624            operation: None,
1625        };
1626        assert!(alert.operation.is_none());
1627    }
1628
1629    #[test]
1630    fn test_alert_implements_clone_debug() {
1631        let alert = Alert {
1632            level: AlertLevel::Warning,
1633            message: "approaching budget".to_string(),
1634            timestamp: Utc::now(),
1635            operation: Some("op".to_string()),
1636        };
1637        let cloned = alert.clone();
1638        assert_eq!(cloned.level, alert.level);
1639        assert_eq!(cloned.message, alert.message);
1640        // Debug formatting must contain expected fields, not just "not panic".
1641        let debug_str = format!("{alert:?}");
1642        assert!(debug_str.contains("approaching budget"), "debug output missing message: {}", debug_str);
1643        assert!(debug_str.contains("Warning"), "debug output missing level: {}", debug_str);
1644    }
1645
1646    // ===================== SaturationGauge tests =====================
1647
1648    #[test]
1649    fn test_saturation_gauge_new_starts_unsaturated() {
1650        let gauge = SaturationGauge::new(0.8);
1651        assert!(!gauge.is_saturated());
1652        assert!(gauge.check_alert().is_none());
1653    }
1654
1655    #[test]
1656    fn test_saturation_gauge_set_below_threshold_not_saturated() {
1657        let mut gauge = SaturationGauge::new(0.8);
1658        gauge.set(0.5);
1659        assert!(!gauge.is_saturated());
1660        assert!(gauge.check_alert().is_none());
1661    }
1662
1663    #[test]
1664    fn test_saturation_gauge_set_at_threshold_is_saturated() {
1665        let mut gauge = SaturationGauge::new(0.8);
1666        gauge.set(0.8);
1667        assert!(gauge.is_saturated());
1668    }
1669
1670    #[test]
1671    fn test_saturation_gauge_set_above_threshold_is_saturated() {
1672        let mut gauge = SaturationGauge::new(0.8);
1673        gauge.set(0.95);
1674        assert!(gauge.is_saturated());
1675    }
1676
1677    #[test]
1678    fn test_saturation_gauge_check_alert_returns_critical_when_saturated() {
1679        let mut gauge = SaturationGauge::new(0.8);
1680        gauge.set(0.95);
1681        let alert = gauge.check_alert().expect("alert must fire when saturated");
1682        assert_eq!(alert.level, AlertLevel::Critical);
1683        assert!(!alert.message.is_empty());
1684        assert!(alert.operation.is_none()); // gauge has no operation context
1685    }
1686
1687    #[test]
1688    fn test_saturation_gauge_check_alert_returns_none_when_not_saturated() {
1689        let mut gauge = SaturationGauge::new(0.8);
1690        gauge.set(0.3);
1691        assert!(gauge.check_alert().is_none());
1692    }
1693
1694    #[test]
1695    fn test_saturation_gauge_set_zero_not_saturated() {
1696        let mut gauge = SaturationGauge::new(0.8);
1697        gauge.set(0.0);
1698        assert!(!gauge.is_saturated());
1699    }
1700
1701    #[test]
1702    fn test_saturation_gauge_set_one_saturated() {
1703        let mut gauge = SaturationGauge::new(0.5);
1704        gauge.set(1.0);
1705        assert!(gauge.is_saturated());
1706    }
1707
1708    #[test]
1709    fn test_saturation_gauge_threshold_zero_always_saturated() {
1710        let mut gauge = SaturationGauge::new(0.0);
1711        gauge.set(0.0);
1712        // threshold = 0 means anything >= 0 is saturated (only negative would not be,
1713        // but saturation values are conventionally in [0, 1]).
1714        assert!(gauge.is_saturated());
1715    }
1716
1717    // ===================== AlertHook / LogAlertHook / InMemoryAlertHook tests =====================
1718
1719    fn sample_alert(level: AlertLevel, op: Option<&str>) -> Alert {
1720        Alert {
1721            level,
1722            message: "test alert".to_string(),
1723            timestamp: Utc::now(),
1724            operation: op.map(str::to_string),
1725        }
1726    }
1727
1728    #[test]
1729    fn test_log_alert_hook_notify_returns_ok() {
1730        let hook = LogAlertHook::new();
1731        let alert = sample_alert(AlertLevel::Warning, Some("op"));
1732        let result = hook.notify(&alert);
1733        assert!(result.is_ok());
1734    }
1735
1736    #[test]
1737    fn test_log_alert_hook_notify_critical_succeeds() {
1738        let hook = LogAlertHook::new();
1739        let alert = sample_alert(AlertLevel::Critical, None);
1740        let result = hook.notify(&alert);
1741        assert!(result.is_ok());
1742    }
1743
1744    #[test]
1745    fn test_log_alert_hook_implements_send_sync() {
1746        fn assert_send_sync<T: Send + Sync>() {}
1747        assert_send_sync::<LogAlertHook>();
1748    }
1749
1750    #[test]
1751    fn test_webhook_alert_hook_new_starts_empty() {
1752        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1753        assert!(hook.sent_alerts().is_empty());
1754    }
1755
1756    #[test]
1757    fn test_webhook_alert_hook_notify_stores_alert() {
1758        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1759        let alert = sample_alert(AlertLevel::Critical, Some("query_user"));
1760        hook.notify(&alert).expect("notify must succeed");
1761        let sent = hook.sent_alerts();
1762        assert_eq!(sent.len(), 1);
1763        assert_eq!(sent[0], alert);
1764    }
1765
1766    #[test]
1767    fn test_webhook_alert_hook_multiple_notifications_accumulate() {
1768        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1769        let a1 = sample_alert(AlertLevel::Info, None);
1770        let a2 = sample_alert(AlertLevel::Warning, Some("op1"));
1771        let a3 = sample_alert(AlertLevel::Critical, Some("op2"));
1772        hook.notify(&a1).unwrap();
1773        hook.notify(&a2).unwrap();
1774        hook.notify(&a3).unwrap();
1775        let sent = hook.sent_alerts();
1776        assert_eq!(sent.len(), 3);
1777        assert_eq!(sent[0], a1);
1778        assert_eq!(sent[1], a2);
1779        assert_eq!(sent[2], a3);
1780    }
1781
1782    #[test]
1783    fn test_webhook_alert_hook_sent_alerts_returns_clone() {
1784        // The returned Vec should be a snapshot; mutating it must not affect the hook.
1785        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1786        let alert = sample_alert(AlertLevel::Info, None);
1787        hook.notify(&alert).unwrap();
1788        let mut sent = hook.sent_alerts();
1789        sent.clear();
1790        // hook itself must remain unaffected.
1791        assert_eq!(hook.sent_alerts().len(), 1);
1792    }
1793
1794    #[test]
1795    fn test_webhook_alert_hook_implements_send_sync() {
1796        fn assert_send_sync<T: Send + Sync>() {}
1797        assert_send_sync::<InMemoryAlertHook>();
1798    }
1799
1800    #[test]
1801    fn test_alert_hook_trait_object_dispatch() {
1802        // Confirm trait-object dispatch works for heterogeneous hooks.
1803        let hooks: Vec<Box<dyn AlertHook>> = vec![
1804            Box::new(LogAlertHook::new()),
1805            Box::new(InMemoryAlertHook::new(
1806                "https://example.com/hook".to_string(),
1807            )),
1808        ];
1809        let alert = sample_alert(AlertLevel::Critical, Some("op"));
1810        for hook in &hooks {
1811            assert!(hook.notify(&alert).is_ok());
1812        }
1813        // The webhook hook stored one alert; verify via downcast-less approach by
1814        // constructing separately.
1815        let webhook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1816        webhook.notify(&alert).unwrap();
1817        assert_eq!(webhook.sent_alerts().len(), 1);
1818    }
1819
1820    // ===================== SlaMonitor / SlaReport tests =====================
1821
1822    #[test]
1823    fn test_sla_monitor_new_empty() {
1824        let monitor = SlaMonitor::new(0.999);
1825        assert!(monitor.operations().is_empty());
1826    }
1827
1828    #[test]
1829    fn test_sla_monitor_observe_creates_operation() {
1830        let monitor = SlaMonitor::new(0.999);
1831        monitor.observe("query", Duration::from_millis(50), true);
1832        let ops = monitor.operations();
1833        assert_eq!(ops, vec!["query".to_string()]);
1834    }
1835
1836    #[test]
1837    fn test_sla_monitor_report_unknown_returns_none() {
1838        let monitor = SlaMonitor::new(0.999);
1839        assert!(monitor.report("unknown").is_none());
1840    }
1841
1842    #[test]
1843    fn test_sla_monitor_report_basic_stats_all_success() {
1844        let monitor = SlaMonitor::new(0.999);
1845        for ms in [10, 20, 30, 40, 50] {
1846            monitor.observe("op", Duration::from_millis(ms), true);
1847        }
1848        let report = monitor.report("op").expect("report must exist");
1849        assert_eq!(report.total_count, 5);
1850        assert_eq!(report.error_rate, 0.0);
1851        assert!((report.slo_target - 0.999).abs() < 1e-9);
1852        // p50 of [10,20,30,40,50] with nearest rank: rank=ceil(0.5*5)=3, samples[2]=30
1853        assert!((report.p50_ms - 30.0).abs() < 1e-9);
1854        // No errors -> full budget remaining, zero saturation.
1855        assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1856        assert!((report.saturation - 0.0).abs() < 1e-9);
1857    }
1858
1859    #[test]
1860    fn test_sla_monitor_report_with_errors_over_budget() {
1861        let monitor = SlaMonitor::new(0.999);
1862        // 8 success, 2 failures -> error_rate = 0.2
1863        for _ in 0..8 {
1864            monitor.observe("op", Duration::from_millis(10), true);
1865        }
1866        for _ in 0..2 {
1867            monitor.observe("op", Duration::from_millis(10), false);
1868        }
1869        let report = monitor.report("op").expect("report must exist");
1870        assert_eq!(report.total_count, 10);
1871        assert!((report.error_rate - 0.2).abs() < 1e-9);
1872        // error_budget = 1 - 0.999 = 0.001
1873        // 0.2 / 0.001 = 200x over budget -> clamped to saturation = 1.0
1874        assert!((report.saturation - 1.0).abs() < 1e-9);
1875        assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
1876    }
1877
1878    #[test]
1879    fn test_sla_monitor_report_partial_budget() {
1880        // SLO 0.9 -> error_budget = 0.1
1881        // 1 error out of 20 -> error_rate = 0.05
1882        // saturation = 0.05 / 0.1 = 0.5
1883        // remaining = 1 - 0.5 = 0.5
1884        let monitor = SlaMonitor::new(0.9);
1885        for i in 0..20 {
1886            monitor.observe("op", Duration::from_millis(i), i != 5);
1887        }
1888        let report = monitor.report("op").expect("report");
1889        assert!((report.error_rate - 0.05).abs() < 1e-9);
1890        assert!((report.saturation - 0.5).abs() < 1e-9);
1891        assert!((report.error_budget_remaining - 0.5).abs() < 1e-9);
1892    }
1893
1894    #[test]
1895    fn test_sla_monitor_operations_isolated() {
1896        let monitor = SlaMonitor::new(0.999);
1897        monitor.observe("op1", Duration::from_millis(10), true);
1898        monitor.observe("op2", Duration::from_millis(20), false);
1899        let mut ops = monitor.operations();
1900        ops.sort();
1901        assert_eq!(ops, vec!["op1".to_string(), "op2".to_string()]);
1902
1903        let r1 = monitor.report("op1").expect("op1 report");
1904        let r2 = monitor.report("op2").expect("op2 report");
1905        assert_eq!(r1.total_count, 1);
1906        assert_eq!(r2.total_count, 1);
1907        assert!((r1.error_rate - 0.0).abs() < 1e-9);
1908        assert!((r2.error_rate - 1.0).abs() < 1e-9);
1909    }
1910
1911    #[test]
1912    fn test_sla_monitor_p95_p99_high_percentile() {
1913        let monitor = SlaMonitor::new(0.999);
1914        for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1915            monitor.observe("op", Duration::from_millis(ms), true);
1916        }
1917        let report = monitor.report("op").expect("report");
1918        // p95/p99 with nearest rank n=10: ceil(0.95*10)=10, ceil(0.99*10)=10 -> samples[9]=100
1919        assert!((report.p95_ms - 100.0).abs() < 1e-9);
1920        assert!((report.p99_ms - 100.0).abs() < 1e-9);
1921    }
1922
1923    #[test]
1924    fn test_sla_monitor_observe_aggregates_multiple_calls() {
1925        let monitor = SlaMonitor::new(0.999);
1926        for _ in 0..100 {
1927            monitor.observe("op", Duration::from_millis(5), true);
1928        }
1929        let report = monitor.report("op").expect("report");
1930        assert_eq!(report.total_count, 100);
1931        assert!((report.p50_ms - 5.0).abs() < 1e-9);
1932    }
1933
1934    #[test]
1935    fn test_sla_monitor_implements_send_sync() {
1936        fn assert_send_sync<T: Send + Sync>() {}
1937        assert_send_sync::<SlaMonitor>();
1938    }
1939
1940    #[test]
1941    fn test_sla_monitor_concurrent_observe_thread_safe() {
1942        use std::sync::Arc;
1943        use std::thread;
1944
1945        let monitor = Arc::new(SlaMonitor::new(0.999));
1946        let mut handles = vec![];
1947
1948        for t in 0..4 {
1949            let m = Arc::clone(&monitor);
1950            handles.push(thread::spawn(move || {
1951                for i in 0..100 {
1952                    // 10% failure rate (i % 10 == 0)
1953                    m.observe("op", Duration::from_millis(i as u64), i % 10 != 0);
1954                }
1955                // Touch another operation per thread to verify isolation.
1956                let op_name = format!("thread-{t}");
1957                m.observe(&op_name, Duration::from_millis(1), true);
1958            }));
1959        }
1960
1961        for h in handles {
1962            h.join().unwrap();
1963        }
1964
1965        let report = monitor.report("op").expect("op report");
1966        assert_eq!(report.total_count, 400);
1967        // 10% error rate
1968        assert!((report.error_rate - 0.1).abs() < 1e-9);
1969
1970        // Each thread registered its own operation.
1971        let mut ops = monitor.operations();
1972        ops.sort();
1973        // "op" + 4 thread-specific operations
1974        assert_eq!(ops.len(), 5);
1975        assert!(ops.contains(&"op".to_string()));
1976    }
1977
1978    #[test]
1979    fn test_sla_monitor_report_slo_one_with_no_errors() {
1980        // SLO = 1.0 with no errors -> full budget, zero saturation.
1981        let monitor = SlaMonitor::new(1.0);
1982        monitor.observe("op", Duration::from_millis(10), true);
1983        let report = monitor.report("op").expect("report");
1984        assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1985        assert!((report.saturation - 0.0).abs() < 1e-9);
1986    }
1987
1988    #[test]
1989    fn test_sla_monitor_report_slo_one_with_errors() {
1990        // SLO = 1.0 with any error -> budget fully exhausted.
1991        let monitor = SlaMonitor::new(1.0);
1992        monitor.observe("op", Duration::from_millis(10), false);
1993        let report = monitor.report("op").expect("report");
1994        assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
1995        assert!((report.saturation - 1.0).abs() < 1e-9);
1996    }
1997
1998    #[test]
1999    fn test_sla_report_fields_are_public() {
2000        // Verifies all SlaReport fields are accessible per the spec.
2001        let monitor = SlaMonitor::new(0.999);
2002        monitor.observe("op", Duration::from_millis(10), true);
2003        let r = monitor.report("op").unwrap();
2004        let _p50: f64 = r.p50_ms;
2005        let _p95: f64 = r.p95_ms;
2006        let _p99: f64 = r.p99_ms;
2007        let _erate: f64 = r.error_rate;
2008        let _total: usize = r.total_count;
2009        let _slo: f64 = r.slo_target;
2010        let _ebr: f64 = r.error_budget_remaining;
2011        let _sat: f64 = r.saturation;
2012    }
2013}
2014
2015// ============================================================================
2016// OTLP Exporter(feature = "otlp")
2017// ============================================================================
2018
2019/// OTLP exporter 配置
2020///
2021/// 用于将 SZ-ORM tracing 的 Span 通过 OpenTelemetry OTLP 协议导出到 Collector。
2022///
2023/// # 示例
2024///
2025/// ```no_run
2026/// # #[cfg(feature = "otlp")] {
2027/// use sz_orm_tracing::OtlpConfig;
2028///
2029/// let config = OtlpConfig {
2030///     endpoint: "http://localhost:4317".to_string(),
2031///     service_name: "sz-orm-app".to_string(),
2032///     timeout_ms: 5000,
2033/// };
2034/// # }
2035/// ```
2036#[cfg(feature = "otlp")]
2037#[derive(Debug, Clone)]
2038pub struct OtlpConfig {
2039    /// OTLP gRPC endpoint(如 `http://localhost:4317`)
2040    pub endpoint: String,
2041    /// 服务名(出现在 trace 的 service.name 标签)
2042    pub service_name: String,
2043    /// 导出超时(毫秒)
2044    pub timeout_ms: u64,
2045}
2046
2047#[cfg(feature = "otlp")]
2048impl Default for OtlpConfig {
2049    fn default() -> Self {
2050        Self {
2051            endpoint: "http://localhost:4317".to_string(),
2052            service_name: "sz-orm".to_string(),
2053            timeout_ms: 5000,
2054        }
2055    }
2056}
2057
2058/// 初始化 OTLP exporter
2059///
2060/// 将 SZ-ORM 的 tracing 接入 OpenTelemetry,使 Span 自动导出到 Collector。
2061///
2062/// # 错误
2063///
2064/// - `TracingError::OtlpInitFailed`:初始化失败
2065///
2066/// # 示例
2067///
2068/// ```no_run
2069/// # #[cfg(feature = "otlp")] {
2070/// # let rt = tokio::runtime::Runtime::new().unwrap();
2071/// # rt.block_on(async {
2072/// use sz_orm_tracing::{init_otlp_exporter, OtlpConfig};
2073///
2074/// let _guard = init_otlp_exporter(OtlpConfig::default()).await.unwrap();
2075/// // 此后通过 `Tracer` 上报的 Span 将自动导出到 OTLP Collector
2076/// # });
2077/// # }
2078/// ```
2079#[cfg(feature = "otlp")]
2080pub async fn init_otlp_exporter(config: OtlpConfig) -> Result<OtlpGuard, TracingError> {
2081    use opentelemetry_otlp::{SpanExporter, WithExportConfig};
2082    use opentelemetry_sdk::resource::Resource;
2083    use opentelemetry_sdk::runtime::Tokio;
2084    use opentelemetry_sdk::trace::TracerProvider;
2085    use std::time::Duration;
2086
2087    let exporter = SpanExporter::builder()
2088        .with_tonic()
2089        .with_endpoint(config.endpoint.clone())
2090        .with_timeout(Duration::from_millis(config.timeout_ms))
2091        .build()
2092        .map_err(|e| TracingError::OtlpInitFailed(format!("exporter build: {e}")))?;
2093
2094    let provider = TracerProvider::builder()
2095        .with_batch_exporter(exporter, Tokio)
2096        .with_resource(Resource::new_with_defaults([opentelemetry::KeyValue::new(
2097            "service.name",
2098            config.service_name.clone(),
2099        )]))
2100        .build();
2101
2102    // 设置为全局 provider
2103    opentelemetry::global::set_tracer_provider(provider.clone());
2104
2105    Ok(OtlpGuard { provider })
2106}
2107
2108/// OTLP exporter 守卫
2109///
2110/// drop 时优雅关闭 exporter,确保所有 Span 已导出。
2111#[cfg(feature = "otlp")]
2112pub struct OtlpGuard {
2113    provider: opentelemetry_sdk::trace::TracerProvider,
2114}
2115
2116#[cfg(feature = "otlp")]
2117impl Drop for OtlpGuard {
2118    fn drop(&mut self) {
2119        // 优雅关闭:未发送的 span 会被丢弃(不阻塞)
2120        let _ = self.provider.shutdown();
2121    }
2122}