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            .expect("tracing spans read lock poisoned")
162            .clone()
163    }
164
165    pub fn clear(&self) {
166        self.spans
167            .write()
168            .map_err(|e| TracingError::Internal(e.to_string()))
169            .expect("tracing spans write lock poisoned")
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(
769        since = "1.2.0",
770        note = "use `identifier()` instead; this hook does not perform HTTP"
771    )]
772    pub fn url(&self) -> &str {
773        &self.identifier
774    }
775}
776
777impl AlertHook for InMemoryAlertHook {
778    fn notify(&self, alert: &Alert) -> Result<(), String> {
779        match self.sent.write() {
780            Ok(mut guard) => {
781                guard.push(alert.clone());
782                Ok(())
783            }
784            Err(e) => Err(format!("in-memory alert hook lock poisoned: {e}")),
785        }
786    }
787}
788
789/// 单个操作的 SLA 统计聚合(由 [`SlaMonitor`] 的 `RwLock` 保护线程安全)。
790///
791/// 同时维护延迟直方图与累计错误计数,避免 [`ErrorRateCounter`] 的滑动窗口
792/// 在长周期 SLA 报告中丢失历史样本。
793struct OperationStats {
794    latency: LatencyHistogram,
795    total_count: usize,
796    error_count: usize,
797}
798
799impl OperationStats {
800    fn new() -> Self {
801        Self {
802            latency: LatencyHistogram::new(Vec::new()),
803            total_count: 0,
804            error_count: 0,
805        }
806    }
807
808    fn observe(&mut self, duration: Duration, success: bool) {
809        self.latency.record(duration);
810        self.total_count += 1;
811        if !success {
812            self.error_count += 1;
813        }
814    }
815
816    fn error_rate(&self) -> f64 {
817        if self.total_count == 0 {
818            0.0
819        } else {
820            self.error_count as f64 / self.total_count as f64
821        }
822    }
823}
824
825/// SLA 监控报告快照,由 [`SlaMonitor::report`] 生成。
826#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
827pub struct SlaReport {
828    /// P50 延迟(毫秒)。
829    pub p50_ms: f64,
830    /// P95 延迟(毫秒)。
831    pub p95_ms: f64,
832    /// P99 延迟(毫秒)。
833    pub p99_ms: f64,
834    /// 错误率,范围 `[0.0, 1.0]`。
835    pub error_rate: f64,
836    /// 总观测次数。
837    pub total_count: usize,
838    /// SLO 目标成功率(如 `0.999`)。
839    pub slo_target: f64,
840    /// 剩余错误预算,范围 `[0.0, 1.0]`,`1.0` 表示预算完整。
841    pub error_budget_remaining: f64,
842    /// 饱和度,范围 `[0.0, 1.0]`,`1.0` 表示错误预算已耗尽。
843    pub saturation: f64,
844}
845
846/// SLA 监控器,按操作名聚合延迟与错误统计,并生成 [`SlaReport`]。
847///
848/// 内部使用 `RwLock<HashMap>` 实现线程安全,`observe` 通过 `&self` 提供
849/// 内部可变性,因此可通过 `Arc<SlaMonitor>` 在多线程中并发调用。
850pub struct SlaMonitor {
851    slo_target: f64,
852    operations: RwLock<HashMap<String, OperationStats>>,
853}
854
855impl SlaMonitor {
856    pub fn new(slo_target: f64) -> Self {
857        Self {
858            slo_target,
859            operations: RwLock::new(HashMap::new()),
860        }
861    }
862
863    /// 记录一次操作观测:延迟与成功/失败。
864    ///
865    /// 使用 `&self` 而非 `&mut self`,以便通过 `Arc<SlaMonitor>` 在多线程
866    /// 中并发写入(与项目现有 `SzTracer::end_span` 模式一致)。
867    pub fn observe(&self, operation: &str, duration: Duration, success: bool) {
868        if let Ok(mut ops) = self.operations.write() {
869            let stats = ops
870                .entry(operation.to_string())
871                .or_insert_with(OperationStats::new);
872            stats.observe(duration, success);
873        }
874    }
875
876    pub fn report(&self, operation: &str) -> Option<SlaReport> {
877        let ops = self.operations.read().ok()?;
878        let stats = ops.get(operation)?;
879        let error_rate = stats.error_rate();
880        let error_budget = 1.0 - self.slo_target;
881        let (saturation, error_budget_remaining) = if error_budget <= 0.0 {
882            // SLO = 1.0 意味着不允许任何错误:有错误即耗尽预算。
883            if error_rate == 0.0 {
884                (0.0, 1.0)
885            } else {
886                (1.0, 0.0)
887            }
888        } else {
889            let sat = (error_rate / error_budget).clamp(0.0, 1.0);
890            (sat, 1.0 - sat)
891        };
892        let ms = |p: f64| {
893            stats
894                .latency
895                .percentile(p)
896                .map(|d| d.as_nanos() as f64 / 1_000_000.0)
897                .unwrap_or(0.0)
898        };
899        Some(SlaReport {
900            p50_ms: ms(50.0),
901            p95_ms: ms(95.0),
902            p99_ms: ms(99.0),
903            error_rate,
904            total_count: stats.total_count,
905            slo_target: self.slo_target,
906            error_budget_remaining,
907            saturation,
908        })
909    }
910
911    pub fn operations(&self) -> Vec<String> {
912        self.operations
913            .read()
914            .map(|ops| ops.keys().cloned().collect())
915            .unwrap_or_default()
916    }
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922
923    #[test]
924    fn test_span_new() {
925        let span = Span::new("trace1", "span1", "operation1");
926        assert_eq!(span.trace_id, "trace1");
927        assert_eq!(span.span_id, "span1");
928        assert_eq!(span.operation_name, "operation1");
929        assert!(span.end_time.is_none());
930    }
931
932    #[test]
933    fn test_span_with_parent() {
934        let span = Span::new("trace1", "span1", "op").with_parent("parent1");
935        assert_eq!(span.parent_id, Some("parent1".to_string()));
936    }
937
938    #[test]
939    fn test_span_with_service() {
940        let span = Span::new("trace1", "span1", "op").with_service("my-service");
941        assert_eq!(span.service_name, "my-service");
942    }
943
944    #[test]
945    fn test_span_with_tag() {
946        let span = Span::new("trace1", "span1", "op").with_tag("key", "value");
947        assert_eq!(span.tags.get("key"), Some(&"value".to_string()));
948    }
949
950    #[test]
951    fn test_span_finish() {
952        let mut span = Span::new("trace1", "span1", "op");
953        span.finish();
954        assert!(span.end_time.is_some());
955        assert!(span.duration().is_some());
956    }
957
958    #[test]
959    fn test_span_add_log() {
960        let mut span = Span::new("trace1", "span1", "op");
961        span.add_log("test log");
962        assert_eq!(span.logs.len(), 1);
963        assert_eq!(span.logs[0].message, "test log");
964    }
965
966    #[test]
967    fn test_tracer_new() {
968        let tracer = SzTracer::new("test-service");
969        assert!(tracer.get_spans().is_empty());
970    }
971
972    #[test]
973    fn test_tracer_start_span() {
974        let tracer = SzTracer::new("test-service");
975        let span = tracer.start_span("test-operation");
976        assert_eq!(span.operation_name, "test-operation");
977    }
978
979    #[test]
980    fn test_tracer_end_span() {
981        let tracer = SzTracer::new("test-service");
982        let span = tracer.start_span("test-operation");
983        tracer.end_span(span);
984
985        let spans = tracer.get_spans();
986        assert_eq!(spans.len(), 1);
987    }
988
989    #[test]
990    fn test_tracer_inject() {
991        let tracer = SzTracer::new("test-service");
992        let span = tracer.start_span("test");
993        let headers = tracer.inject(&span);
994
995        // v0.2.2 修复 P2-2:默认使用 W3C traceparent header
996        let tp = headers
997            .get("traceparent")
998            .expect("traceparent header must be present");
999        // 格式:00-<trace_id>-<span_id>-01
1000        let parts: Vec<&str> = tp.split('-').collect();
1001        assert_eq!(parts.len(), 4);
1002        assert_eq!(parts[0], "00"); // 版本号
1003        assert_eq!(parts[1], span.trace_id);
1004        assert_eq!(parts[2], span.span_id);
1005        assert_eq!(parts[3], "01"); // sampled
1006    }
1007
1008    #[test]
1009    fn test_tracer_extract() {
1010        let tracer = SzTracer::new("test-service");
1011        let mut headers = HashMap::new();
1012        // W3C traceparent 格式
1013        let trace_id = "0af7651916cd43dd8448eb211c80319c";
1014        let span_id = "b7ad6b7169203331";
1015        headers.insert(
1016            "traceparent".to_string(),
1017            format!("00-{}-{}-01", trace_id, span_id),
1018        );
1019
1020        let span = tracer.extract(&headers);
1021        assert!(span.is_some());
1022        let span = span.unwrap();
1023        assert_eq!(span.trace_id, trace_id);
1024        assert_eq!(span.span_id, span_id);
1025    }
1026
1027    #[test]
1028    fn test_tracer_extract_legacy_headers() {
1029        // v0.2.2 修复 P2-2:向后兼容 legacy header
1030        let tracer = SzTracer::new("test-service");
1031        let mut headers = HashMap::new();
1032        headers.insert("trace-id".to_string(), "trace123".to_string());
1033        headers.insert("span-id".to_string(), "span456".to_string());
1034
1035        let span = tracer.extract(&headers);
1036        assert!(span.is_some());
1037        let span = span.unwrap();
1038        assert_eq!(span.trace_id, "trace123");
1039        assert_eq!(span.span_id, "span456");
1040    }
1041
1042    #[test]
1043    fn test_tracer_extract_missing_headers() {
1044        let tracer = SzTracer::new("test-service");
1045        let headers = HashMap::new();
1046        let span = tracer.extract(&headers);
1047        assert!(span.is_none());
1048    }
1049
1050    // ===================== v0.2.2 修复 P2-2:W3C TraceContext 测试 =====================
1051
1052    #[test]
1053    fn test_parse_traceparent_valid() {
1054        let valid = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
1055        let span = SzTracer::parse_traceparent(valid).expect("valid traceparent must parse");
1056        assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1057        assert_eq!(span.span_id, "b7ad6b7169203331");
1058    }
1059
1060    #[test]
1061    fn test_parse_traceparent_invalid_version() {
1062        // 版本号不是 2 字符 hex
1063        assert!(SzTracer::parse_traceparent(
1064            "0-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1065        )
1066        .is_none());
1067        // 版本号不是 hex
1068        assert!(SzTracer::parse_traceparent(
1069            "xy-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
1070        )
1071        .is_none());
1072    }
1073
1074    #[test]
1075    fn test_parse_traceparent_invalid_trace_id_length() {
1076        // trace_id 不是 32 字符
1077        assert!(SzTracer::parse_traceparent("00-short-b7ad6b7169203331-01").is_none());
1078    }
1079
1080    #[test]
1081    fn test_parse_traceparent_invalid_span_id_length() {
1082        // span_id 不是 16 字符
1083        assert!(
1084            SzTracer::parse_traceparent("00-0af7651916cd43dd8448eb211c80319c-short-01").is_none()
1085        );
1086    }
1087
1088    #[test]
1089    fn test_parse_traceparent_all_zeros_trace_id_rejected() {
1090        // W3C 规范:trace_id 不能全为 0
1091        let all_zero = "00-00000000000000000000000000000000-b7ad6b7169203331-01";
1092        assert!(SzTracer::parse_traceparent(all_zero).is_none());
1093    }
1094
1095    #[test]
1096    fn test_parse_traceparent_all_zeros_span_id_rejected() {
1097        // W3C 规范:span_id 不能全为 0
1098        let all_zero = "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01";
1099        assert!(SzTracer::parse_traceparent(all_zero).is_none());
1100    }
1101
1102    #[test]
1103    fn test_parse_traceparent_invalid_flags() {
1104        // trace_flags 不是 2 字符 hex
1105        assert!(SzTracer::parse_traceparent(
1106            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-xyz"
1107        )
1108        .is_none());
1109    }
1110
1111    #[test]
1112    fn test_parse_traceparent_wrong_part_count() {
1113        // 不是 4 个部分
1114        assert!(SzTracer::parse_traceparent(
1115            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331"
1116        )
1117        .is_none());
1118        assert!(SzTracer::parse_traceparent(
1119            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01-extra"
1120        )
1121        .is_none());
1122    }
1123
1124    #[test]
1125    fn test_inject_legacy_preserves_old_format() {
1126        let tracer = SzTracer::new("svc");
1127        let span = tracer.start_span("op");
1128        let headers = tracer.inject_legacy(&span);
1129
1130        assert_eq!(headers.get("trace-id"), Some(&span.trace_id.to_string()));
1131        assert_eq!(headers.get("span-id"), Some(&span.span_id.to_string()));
1132        assert!(!headers.contains_key("parent-span-id"));
1133    }
1134
1135    #[test]
1136    fn test_extract_legacy_preserves_old_format() {
1137        let tracer = SzTracer::new("svc");
1138        let mut headers = HashMap::new();
1139        headers.insert("trace-id".to_string(), "abc".to_string());
1140        headers.insert("span-id".to_string(), "def".to_string());
1141
1142        let span = tracer.extract_legacy(&headers).expect("legacy extract");
1143        assert_eq!(span.trace_id, "abc");
1144        assert_eq!(span.span_id, "def");
1145    }
1146
1147    #[test]
1148    fn test_w3c_traceparent_roundtrip_preserves_ids() {
1149        // inject → extract 必须保留 trace_id 和 span_id
1150        let tracer = SzTracer::new("svc");
1151        let original = tracer.start_span("roundtrip");
1152        let headers = tracer.inject(&original);
1153
1154        let extracted = tracer.extract(&headers).expect("roundtrip extract");
1155        assert_eq!(extracted.trace_id(), original.trace_id());
1156        assert_eq!(extracted.span_id(), original.span_id());
1157        assert!(extracted.parent_id().is_none());
1158    }
1159
1160    #[test]
1161    fn test_w3c_prefers_traceparent_over_legacy() {
1162        // 同时存在 traceparent 和 legacy header 时,优先 W3C
1163        let tracer = SzTracer::new("svc");
1164        let mut headers = HashMap::new();
1165        headers.insert(
1166            "traceparent".to_string(),
1167            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
1168        );
1169        headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1170        headers.insert("span-id".to_string(), "legacy-span".to_string());
1171
1172        let span = tracer.extract(&headers).expect("extract");
1173        assert_eq!(span.trace_id, "0af7651916cd43dd8448eb211c80319c");
1174        assert_eq!(span.span_id, "b7ad6b7169203331");
1175    }
1176
1177    #[test]
1178    fn test_w3c_falls_back_to_legacy_on_invalid_traceparent() {
1179        // traceparent 格式不合法时回退到 legacy
1180        let tracer = SzTracer::new("svc");
1181        let mut headers = HashMap::new();
1182        headers.insert("traceparent".to_string(), "invalid-format".to_string());
1183        headers.insert("trace-id".to_string(), "legacy-trace".to_string());
1184        headers.insert("span-id".to_string(), "legacy-span".to_string());
1185
1186        let span = tracer
1187            .extract(&headers)
1188            .expect("should fall back to legacy");
1189        assert_eq!(span.trace_id, "legacy-trace");
1190        assert_eq!(span.span_id, "legacy-span");
1191    }
1192
1193    #[test]
1194    fn test_otel_tracer() {
1195        let tracer = OtelTracer::new("test-service");
1196        let span = tracer.start_span("test-operation");
1197        assert_eq!(span.operation_name, "test-operation");
1198    }
1199
1200    #[test]
1201    fn test_otel_tracer_is_a_sz_tracer_wrapper_not_a_real_otel_sdk() {
1202        // OtelTracer delegates every Tracer method to SzTracer. Document the
1203        // contract: spans produced via OtelTracer must be observable through
1204        // the underlying SzTracer (i.e. `inner().get_spans()`).
1205        let tracer = OtelTracer::new("svc");
1206        assert!(tracer.inner().get_spans().is_empty());
1207
1208        let span = tracer.start_span("op");
1209        assert_eq!(span.service_name(), "svc");
1210        tracer.end_span(span);
1211
1212        let spans = tracer.inner().get_spans();
1213        assert_eq!(spans.len(), 1);
1214        assert_eq!(spans[0].operation_name(), "op");
1215        assert!(spans[0].end_time.is_some());
1216    }
1217
1218    #[test]
1219    fn test_otel_tracer_inject_extract_roundtrip_preserves_ids() {
1220        // v0.2.2 修复 P2-2:现在使用 W3C traceparent header
1221        let tracer = OtelTracer::new("svc");
1222        let original = tracer.start_span("roundtrip");
1223        let headers = tracer.inject(&original);
1224
1225        // W3C traceparent 必须存在并包含原始 ID
1226        let tp = headers
1227            .get("traceparent")
1228            .expect("traceparent must be present");
1229        assert!(tp.contains(original.trace_id()));
1230        assert!(tp.contains(original.span_id()));
1231        assert!(!headers.contains_key("parent-span-id"));
1232
1233        let extracted = tracer.extract(&headers).expect("extract should round-trip");
1234        assert_eq!(extracted.trace_id(), original.trace_id());
1235        assert_eq!(extracted.span_id(), original.span_id());
1236        assert!(extracted.parent_id().is_none());
1237    }
1238
1239    #[test]
1240    fn test_otel_tracer_preserves_parent_id_through_roundtrip() {
1241        let tracer = OtelTracer::new("svc");
1242        let parent = tracer.start_span("parent");
1243        let child = tracer
1244            .start_span("child")
1245            .with_parent(parent.span_id().to_string());
1246        let headers = tracer.inject(&child);
1247
1248        // parent-span-id 仍然通过独立 header 传递(向后兼容)
1249        assert_eq!(
1250            headers.get("parent-span-id"),
1251            Some(&parent.span_id().to_string())
1252        );
1253
1254        let extracted = tracer.extract(&headers).expect("extract should round-trip");
1255        assert_eq!(extracted.parent_id(), Some(parent.span_id()));
1256    }
1257
1258    #[test]
1259    fn test_otel_tracer_extract_returns_none_without_required_headers() {
1260        let tracer = OtelTracer::new("svc");
1261        let headers: HashMap<String, String> = HashMap::new();
1262        assert!(tracer.extract(&headers).is_none());
1263
1264        // 仅 legacy trace-id 缺失 span-id 时也必须失败
1265        let mut partial = HashMap::new();
1266        partial.insert("trace-id".to_string(), "abc".to_string());
1267        // Missing span-id - extract must fail.
1268        assert!(tracer.extract(&partial).is_none());
1269
1270        // 仅 W3C traceparent 格式错误时(且无 legacy 回退)必须失败
1271        let mut bad_w3c = HashMap::new();
1272        bad_w3c.insert("traceparent".to_string(), "garbage".to_string());
1273        assert!(tracer.extract(&bad_w3c).is_none());
1274    }
1275
1276    #[test]
1277    fn test_otel_tracer_generated_ids_have_correct_length() {
1278        // OpenTelemetry spec: trace_id is 16 bytes (32 hex chars), span_id
1279        // is 8 bytes (16 hex chars). OtelTracer inherits SzTracer's generator
1280        // and must produce the same shape so consumers parsing the IDs do not
1281        // trip over a different length.
1282        let trace_id = SzTracer::generate_trace_id();
1283        let span_id = SzTracer::generate_span_id();
1284        assert_eq!(trace_id.len(), 32, "trace_id must be 32 hex chars");
1285        assert_eq!(span_id.len(), 16, "span_id must be 16 hex chars");
1286
1287        // Hex-only.
1288        assert!(trace_id.chars().all(|c| c.is_ascii_hexdigit()));
1289        assert!(span_id.chars().all(|c| c.is_ascii_hexdigit()));
1290    }
1291
1292    #[test]
1293    fn test_span_accessors() {
1294        let span = Span::new("trace1", "span1", "test-op")
1295            .with_service("svc")
1296            .with_tag("k", "v");
1297
1298        assert_eq!(span.trace_id(), "trace1");
1299        assert_eq!(span.span_id(), "span1");
1300        assert_eq!(span.operation_name(), "test-op");
1301        assert_eq!(span.service_name(), "svc");
1302        assert_eq!(span.tags().get("k"), Some(&"v".to_string()));
1303    }
1304
1305    #[test]
1306    fn test_generate_ids() {
1307        let trace_id = SzTracer::generate_trace_id();
1308        let span_id = SzTracer::generate_span_id();
1309
1310        assert_eq!(trace_id.len(), 32);
1311        assert_eq!(span_id.len(), 16);
1312    }
1313
1314    #[test]
1315    fn test_tracer_clear() {
1316        let tracer = SzTracer::new("test-service");
1317
1318        let span = tracer.start_span("op1");
1319        tracer.end_span(span);
1320        let span = tracer.start_span("op2");
1321        tracer.end_span(span);
1322
1323        assert_eq!(tracer.get_spans().len(), 2);
1324
1325        tracer.clear();
1326        assert!(tracer.get_spans().is_empty());
1327    }
1328
1329    // ===================== LatencyHistogram tests =====================
1330
1331    #[test]
1332    fn test_latency_histogram_new_empty() {
1333        let hist = LatencyHistogram::new(vec![
1334            Duration::from_millis(10),
1335            Duration::from_millis(100),
1336            Duration::from_millis(1000),
1337        ]);
1338        assert_eq!(hist.count(), 0);
1339        assert!(hist.percentile(50.0).is_none());
1340        assert!(hist.mean().is_none());
1341    }
1342
1343    #[test]
1344    fn test_latency_histogram_record_single() {
1345        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1346        hist.record(Duration::from_millis(50));
1347        assert_eq!(hist.count(), 1);
1348        assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(50)));
1349        assert_eq!(hist.mean(), Some(Duration::from_millis(50)));
1350    }
1351
1352    #[test]
1353    fn test_latency_histogram_percentile_p50_sorted() {
1354        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1355        for ms in [10, 20, 30, 40, 50] {
1356            hist.record(Duration::from_millis(ms));
1357        }
1358        // 5 samples sorted: [10, 20, 30, 40, 50], p50 should be 30 (median)
1359        assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1360    }
1361
1362    #[test]
1363    fn test_latency_histogram_percentile_p95_high_value() {
1364        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1365        for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1366            hist.record(Duration::from_millis(ms));
1367        }
1368        // p95 of 10 samples should be the 9th or 10th value (high)
1369        let p95 = hist.percentile(95.0).expect("p95 must exist");
1370        assert!(p95 >= Duration::from_millis(9));
1371    }
1372
1373    #[test]
1374    fn test_latency_histogram_percentile_p99_max_value() {
1375        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1376        for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1377            hist.record(Duration::from_millis(ms));
1378        }
1379        let p99 = hist.percentile(99.0).expect("p99 must exist");
1380        assert_eq!(p99, Duration::from_millis(100));
1381    }
1382
1383    #[test]
1384    fn test_latency_histogram_percentile_p0_min_value() {
1385        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1386        for ms in [10, 20, 30] {
1387            hist.record(Duration::from_millis(ms));
1388        }
1389        assert_eq!(hist.percentile(0.0), Some(Duration::from_millis(10)));
1390    }
1391
1392    #[test]
1393    fn test_latency_histogram_percentile_p100_max_value() {
1394        let mut hist = LatencyHistogram::new(vec![Duration::from_secs(10)]);
1395        for ms in [10, 20, 30] {
1396            hist.record(Duration::from_millis(ms));
1397        }
1398        assert_eq!(hist.percentile(100.0), Some(Duration::from_millis(30)));
1399    }
1400
1401    #[test]
1402    fn test_latency_histogram_percentile_empty_returns_none() {
1403        let hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1404        assert!(hist.percentile(50.0).is_none());
1405    }
1406
1407    #[test]
1408    fn test_latency_histogram_percentile_out_of_range_returns_none() {
1409        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1410        hist.record(Duration::from_millis(50));
1411        assert!(hist.percentile(-1.0).is_none());
1412        assert!(hist.percentile(101.0).is_none());
1413    }
1414
1415    #[test]
1416    fn test_latency_histogram_count() {
1417        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(100)]);
1418        assert_eq!(hist.count(), 0);
1419        hist.record(Duration::from_millis(10));
1420        hist.record(Duration::from_millis(20));
1421        hist.record(Duration::from_millis(30));
1422        assert_eq!(hist.count(), 3);
1423    }
1424
1425    #[test]
1426    fn test_latency_histogram_mean_multiple() {
1427        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1428        for ms in [10, 20, 30, 40, 50] {
1429            hist.record(Duration::from_millis(ms));
1430        }
1431        // mean = (10+20+30+40+50)/5 = 30
1432        assert_eq!(hist.mean(), Some(Duration::from_millis(30)));
1433    }
1434
1435    #[test]
1436    fn test_latency_histogram_record_unsorted_input_stays_sorted() {
1437        let mut hist = LatencyHistogram::new(vec![Duration::from_millis(1000)]);
1438        hist.record(Duration::from_millis(50));
1439        hist.record(Duration::from_millis(10));
1440        hist.record(Duration::from_millis(30));
1441        // p50 should be 30 (median of sorted [10,30,50])
1442        assert_eq!(hist.percentile(50.0), Some(Duration::from_millis(30)));
1443    }
1444
1445    // ===================== ErrorRateCounter tests =====================
1446
1447    #[test]
1448    fn test_error_rate_counter_new_empty() {
1449        let counter = ErrorRateCounter::new(Duration::from_secs(60));
1450        assert_eq!(counter.total(), 0);
1451        assert_eq!(counter.errors(), 0);
1452        assert_eq!(counter.rate(), 0.0);
1453    }
1454
1455    #[test]
1456    fn test_error_rate_counter_all_success_rate_zero() {
1457        let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1458        for _ in 0..10 {
1459            counter.record(true);
1460        }
1461        assert_eq!(counter.total(), 10);
1462        assert_eq!(counter.errors(), 0);
1463        assert_eq!(counter.rate(), 0.0);
1464    }
1465
1466    #[test]
1467    fn test_error_rate_counter_all_failures_rate_one() {
1468        let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1469        for _ in 0..10 {
1470            counter.record(false);
1471        }
1472        assert_eq!(counter.total(), 10);
1473        assert_eq!(counter.errors(), 10);
1474        assert_eq!(counter.rate(), 1.0);
1475    }
1476
1477    #[test]
1478    fn test_error_rate_counter_mixed_rate() {
1479        let mut counter = ErrorRateCounter::new(Duration::from_secs(60));
1480        // 7 success, 3 failures -> rate = 0.3
1481        for _ in 0..7 {
1482            counter.record(true);
1483        }
1484        for _ in 0..3 {
1485            counter.record(false);
1486        }
1487        assert_eq!(counter.total(), 10);
1488        assert_eq!(counter.errors(), 3);
1489        let rate = counter.rate();
1490        assert!((rate - 0.3).abs() < 1e-9, "expected 0.3, got {rate}");
1491    }
1492
1493    #[test]
1494    fn test_error_rate_counter_empty_rate_is_zero() {
1495        let counter = ErrorRateCounter::new(Duration::from_secs(60));
1496        // no samples -> rate must be 0.0 (avoid div-by-zero)
1497        assert_eq!(counter.rate(), 0.0);
1498    }
1499
1500    #[test]
1501    fn test_error_rate_counter_window_expires_old_samples() {
1502        // window of 100ms; old samples should be evicted on subsequent record.
1503        let mut counter = ErrorRateCounter::new(Duration::from_millis(100));
1504        counter.record(false);
1505        counter.record(false);
1506        // sleep past the window
1507        std::thread::sleep(Duration::from_millis(120));
1508        counter.record(true);
1509        // Only the recent success should remain -> rate = 0.0, total = 1
1510        assert_eq!(counter.total(), 1);
1511        assert_eq!(counter.errors(), 0);
1512        assert_eq!(counter.rate(), 0.0);
1513    }
1514
1515    // ===================== ErrorBudget tests =====================
1516
1517    #[test]
1518    fn test_error_budget_new_is_full() {
1519        let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1520        assert!((budget.remaining() - 1.0).abs() < 1e-9);
1521        assert!(!budget.is_exhausted());
1522    }
1523
1524    #[test]
1525    fn test_error_budget_consume_reduces_remaining() {
1526        let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1527        // SLO 0.999 -> error_budget = 0.001 per error.
1528        budget.consume(1);
1529        // remaining = 1 - 1 * 0.001 = 0.999
1530        assert!((budget.remaining() - 0.999).abs() < 1e-9);
1531        assert!(!budget.is_exhausted());
1532    }
1533
1534    #[test]
1535    fn test_error_budget_exhausted_at_capacity() {
1536        let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1537        // capacity = 1 / (1 - 0.999) = 1000 errors
1538        budget.consume(1000);
1539        assert_eq!(budget.remaining(), 0.0);
1540        assert!(budget.is_exhausted());
1541    }
1542
1543    #[test]
1544    fn test_error_budget_over_consume_clamps_to_zero() {
1545        let mut budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1546        budget.consume(2000);
1547        assert_eq!(budget.remaining(), 0.0);
1548        assert!(budget.is_exhausted());
1549    }
1550
1551    #[test]
1552    fn test_error_budget_zero_errors_returns_one() {
1553        let budget = ErrorBudget::new(0.999, Duration::from_secs(60));
1554        assert_eq!(budget.remaining(), 1.0);
1555        assert!(!budget.is_exhausted());
1556    }
1557
1558    #[test]
1559    fn test_error_budget_window_refills_after_expiry() {
1560        // SLO 0.5 -> error_budget = 0.5 per error -> 2 errors exhaust.
1561        let mut budget = ErrorBudget::new(0.5, Duration::from_millis(100));
1562        budget.consume(2);
1563        assert!(budget.is_exhausted());
1564        // wait for window to expire
1565        std::thread::sleep(Duration::from_millis(120));
1566        assert!((budget.remaining() - 1.0).abs() < 1e-9);
1567        assert!(!budget.is_exhausted());
1568    }
1569
1570    #[test]
1571    fn test_error_budget_slo_one_means_no_errors_allowed() {
1572        // SLO = 1.0 -> error_budget = 0 -> any consume exhausts
1573        let mut budget = ErrorBudget::new(1.0, Duration::from_secs(60));
1574        assert_eq!(budget.remaining(), 1.0);
1575        budget.consume(1);
1576        assert_eq!(budget.remaining(), 0.0);
1577        assert!(budget.is_exhausted());
1578    }
1579
1580    #[test]
1581    fn test_error_budget_multiple_consumes_accumulate() {
1582        let mut budget = ErrorBudget::new(0.99, Duration::from_secs(60));
1583        // SLO 0.99 -> error_budget = 0.01 -> capacity = 100 errors
1584        budget.consume(30);
1585        assert!((budget.remaining() - 0.7).abs() < 1e-9);
1586        budget.consume(30);
1587        assert!((budget.remaining() - 0.4).abs() < 1e-9);
1588        budget.consume(40);
1589        assert_eq!(budget.remaining(), 0.0);
1590        assert!(budget.is_exhausted());
1591    }
1592
1593    // ===================== Alert / AlertLevel tests =====================
1594
1595    #[test]
1596    fn test_alert_level_variants_exist() {
1597        let info = AlertLevel::Info;
1598        let warning = AlertLevel::Warning;
1599        let critical = AlertLevel::Critical;
1600        // Sanity: ensure variants are distinct (debug repr).
1601        assert_ne!(format!("{info:?}"), format!("{warning:?}"));
1602        assert_ne!(format!("{warning:?}"), format!("{critical:?}"));
1603        assert_ne!(format!("{info:?}"), format!("{critical:?}"));
1604    }
1605
1606    #[test]
1607    fn test_alert_construction_with_all_fields() {
1608        let ts = Utc::now();
1609        let alert = Alert {
1610            level: AlertLevel::Critical,
1611            message: "p99 latency exceeded budget".to_string(),
1612            timestamp: ts,
1613            operation: Some("query_user".to_string()),
1614        };
1615        assert_eq!(alert.level, AlertLevel::Critical);
1616        assert_eq!(alert.message, "p99 latency exceeded budget");
1617        assert_eq!(alert.timestamp, ts);
1618        assert_eq!(alert.operation.as_deref(), Some("query_user"));
1619    }
1620
1621    #[test]
1622    fn test_alert_construction_without_operation() {
1623        let alert = Alert {
1624            level: AlertLevel::Info,
1625            message: "system healthy".to_string(),
1626            timestamp: Utc::now(),
1627            operation: None,
1628        };
1629        assert!(alert.operation.is_none());
1630    }
1631
1632    #[test]
1633    fn test_alert_implements_clone_debug() {
1634        let alert = Alert {
1635            level: AlertLevel::Warning,
1636            message: "approaching budget".to_string(),
1637            timestamp: Utc::now(),
1638            operation: Some("op".to_string()),
1639        };
1640        let cloned = alert.clone();
1641        assert_eq!(cloned.level, alert.level);
1642        assert_eq!(cloned.message, alert.message);
1643        // Debug formatting must contain expected fields, not just "not panic".
1644        let debug_str = format!("{alert:?}");
1645        assert!(
1646            debug_str.contains("approaching budget"),
1647            "debug output missing message: {}",
1648            debug_str
1649        );
1650        assert!(
1651            debug_str.contains("Warning"),
1652            "debug output missing level: {}",
1653            debug_str
1654        );
1655    }
1656
1657    // ===================== SaturationGauge tests =====================
1658
1659    #[test]
1660    fn test_saturation_gauge_new_starts_unsaturated() {
1661        let gauge = SaturationGauge::new(0.8);
1662        assert!(!gauge.is_saturated());
1663        assert!(gauge.check_alert().is_none());
1664    }
1665
1666    #[test]
1667    fn test_saturation_gauge_set_below_threshold_not_saturated() {
1668        let mut gauge = SaturationGauge::new(0.8);
1669        gauge.set(0.5);
1670        assert!(!gauge.is_saturated());
1671        assert!(gauge.check_alert().is_none());
1672    }
1673
1674    #[test]
1675    fn test_saturation_gauge_set_at_threshold_is_saturated() {
1676        let mut gauge = SaturationGauge::new(0.8);
1677        gauge.set(0.8);
1678        assert!(gauge.is_saturated());
1679    }
1680
1681    #[test]
1682    fn test_saturation_gauge_set_above_threshold_is_saturated() {
1683        let mut gauge = SaturationGauge::new(0.8);
1684        gauge.set(0.95);
1685        assert!(gauge.is_saturated());
1686    }
1687
1688    #[test]
1689    fn test_saturation_gauge_check_alert_returns_critical_when_saturated() {
1690        let mut gauge = SaturationGauge::new(0.8);
1691        gauge.set(0.95);
1692        let alert = gauge.check_alert().expect("alert must fire when saturated");
1693        assert_eq!(alert.level, AlertLevel::Critical);
1694        assert!(!alert.message.is_empty());
1695        assert!(alert.operation.is_none()); // gauge has no operation context
1696    }
1697
1698    #[test]
1699    fn test_saturation_gauge_check_alert_returns_none_when_not_saturated() {
1700        let mut gauge = SaturationGauge::new(0.8);
1701        gauge.set(0.3);
1702        assert!(gauge.check_alert().is_none());
1703    }
1704
1705    #[test]
1706    fn test_saturation_gauge_set_zero_not_saturated() {
1707        let mut gauge = SaturationGauge::new(0.8);
1708        gauge.set(0.0);
1709        assert!(!gauge.is_saturated());
1710    }
1711
1712    #[test]
1713    fn test_saturation_gauge_set_one_saturated() {
1714        let mut gauge = SaturationGauge::new(0.5);
1715        gauge.set(1.0);
1716        assert!(gauge.is_saturated());
1717    }
1718
1719    #[test]
1720    fn test_saturation_gauge_threshold_zero_always_saturated() {
1721        let mut gauge = SaturationGauge::new(0.0);
1722        gauge.set(0.0);
1723        // threshold = 0 means anything >= 0 is saturated (only negative would not be,
1724        // but saturation values are conventionally in [0, 1]).
1725        assert!(gauge.is_saturated());
1726    }
1727
1728    // ===================== AlertHook / LogAlertHook / InMemoryAlertHook tests =====================
1729
1730    fn sample_alert(level: AlertLevel, op: Option<&str>) -> Alert {
1731        Alert {
1732            level,
1733            message: "test alert".to_string(),
1734            timestamp: Utc::now(),
1735            operation: op.map(str::to_string),
1736        }
1737    }
1738
1739    #[test]
1740    fn test_log_alert_hook_notify_returns_ok() {
1741        let hook = LogAlertHook::new();
1742        let alert = sample_alert(AlertLevel::Warning, Some("op"));
1743        let result = hook.notify(&alert);
1744        assert!(result.is_ok());
1745    }
1746
1747    #[test]
1748    fn test_log_alert_hook_notify_critical_succeeds() {
1749        let hook = LogAlertHook::new();
1750        let alert = sample_alert(AlertLevel::Critical, None);
1751        let result = hook.notify(&alert);
1752        assert!(result.is_ok());
1753    }
1754
1755    #[test]
1756    fn test_log_alert_hook_implements_send_sync() {
1757        fn assert_send_sync<T: Send + Sync>() {}
1758        assert_send_sync::<LogAlertHook>();
1759    }
1760
1761    #[test]
1762    fn test_webhook_alert_hook_new_starts_empty() {
1763        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1764        assert!(hook.sent_alerts().is_empty());
1765    }
1766
1767    #[test]
1768    fn test_webhook_alert_hook_notify_stores_alert() {
1769        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1770        let alert = sample_alert(AlertLevel::Critical, Some("query_user"));
1771        hook.notify(&alert).expect("notify must succeed");
1772        let sent = hook.sent_alerts();
1773        assert_eq!(sent.len(), 1);
1774        assert_eq!(sent[0], alert);
1775    }
1776
1777    #[test]
1778    fn test_webhook_alert_hook_multiple_notifications_accumulate() {
1779        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1780        let a1 = sample_alert(AlertLevel::Info, None);
1781        let a2 = sample_alert(AlertLevel::Warning, Some("op1"));
1782        let a3 = sample_alert(AlertLevel::Critical, Some("op2"));
1783        hook.notify(&a1).unwrap();
1784        hook.notify(&a2).unwrap();
1785        hook.notify(&a3).unwrap();
1786        let sent = hook.sent_alerts();
1787        assert_eq!(sent.len(), 3);
1788        assert_eq!(sent[0], a1);
1789        assert_eq!(sent[1], a2);
1790        assert_eq!(sent[2], a3);
1791    }
1792
1793    #[test]
1794    fn test_webhook_alert_hook_sent_alerts_returns_clone() {
1795        // The returned Vec should be a snapshot; mutating it must not affect the hook.
1796        let hook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1797        let alert = sample_alert(AlertLevel::Info, None);
1798        hook.notify(&alert).unwrap();
1799        let mut sent = hook.sent_alerts();
1800        sent.clear();
1801        // hook itself must remain unaffected.
1802        assert_eq!(hook.sent_alerts().len(), 1);
1803    }
1804
1805    #[test]
1806    fn test_webhook_alert_hook_implements_send_sync() {
1807        fn assert_send_sync<T: Send + Sync>() {}
1808        assert_send_sync::<InMemoryAlertHook>();
1809    }
1810
1811    #[test]
1812    fn test_alert_hook_trait_object_dispatch() {
1813        // Confirm trait-object dispatch works for heterogeneous hooks.
1814        let hooks: Vec<Box<dyn AlertHook>> = vec![
1815            Box::new(LogAlertHook::new()),
1816            Box::new(InMemoryAlertHook::new(
1817                "https://example.com/hook".to_string(),
1818            )),
1819        ];
1820        let alert = sample_alert(AlertLevel::Critical, Some("op"));
1821        for hook in &hooks {
1822            assert!(hook.notify(&alert).is_ok());
1823        }
1824        // The webhook hook stored one alert; verify via downcast-less approach by
1825        // constructing separately.
1826        let webhook = InMemoryAlertHook::new("https://example.com/hook".to_string());
1827        webhook.notify(&alert).unwrap();
1828        assert_eq!(webhook.sent_alerts().len(), 1);
1829    }
1830
1831    // ===================== SlaMonitor / SlaReport tests =====================
1832
1833    #[test]
1834    fn test_sla_monitor_new_empty() {
1835        let monitor = SlaMonitor::new(0.999);
1836        assert!(monitor.operations().is_empty());
1837    }
1838
1839    #[test]
1840    fn test_sla_monitor_observe_creates_operation() {
1841        let monitor = SlaMonitor::new(0.999);
1842        monitor.observe("query", Duration::from_millis(50), true);
1843        let ops = monitor.operations();
1844        assert_eq!(ops, vec!["query".to_string()]);
1845    }
1846
1847    #[test]
1848    fn test_sla_monitor_report_unknown_returns_none() {
1849        let monitor = SlaMonitor::new(0.999);
1850        assert!(monitor.report("unknown").is_none());
1851    }
1852
1853    #[test]
1854    fn test_sla_monitor_report_basic_stats_all_success() {
1855        let monitor = SlaMonitor::new(0.999);
1856        for ms in [10, 20, 30, 40, 50] {
1857            monitor.observe("op", Duration::from_millis(ms), true);
1858        }
1859        let report = monitor.report("op").expect("report must exist");
1860        assert_eq!(report.total_count, 5);
1861        assert_eq!(report.error_rate, 0.0);
1862        assert!((report.slo_target - 0.999).abs() < 1e-9);
1863        // p50 of [10,20,30,40,50] with nearest rank: rank=ceil(0.5*5)=3, samples[2]=30
1864        assert!((report.p50_ms - 30.0).abs() < 1e-9);
1865        // No errors -> full budget remaining, zero saturation.
1866        assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1867        assert!((report.saturation - 0.0).abs() < 1e-9);
1868    }
1869
1870    #[test]
1871    fn test_sla_monitor_report_with_errors_over_budget() {
1872        let monitor = SlaMonitor::new(0.999);
1873        // 8 success, 2 failures -> error_rate = 0.2
1874        for _ in 0..8 {
1875            monitor.observe("op", Duration::from_millis(10), true);
1876        }
1877        for _ in 0..2 {
1878            monitor.observe("op", Duration::from_millis(10), false);
1879        }
1880        let report = monitor.report("op").expect("report must exist");
1881        assert_eq!(report.total_count, 10);
1882        assert!((report.error_rate - 0.2).abs() < 1e-9);
1883        // error_budget = 1 - 0.999 = 0.001
1884        // 0.2 / 0.001 = 200x over budget -> clamped to saturation = 1.0
1885        assert!((report.saturation - 1.0).abs() < 1e-9);
1886        assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
1887    }
1888
1889    #[test]
1890    fn test_sla_monitor_report_partial_budget() {
1891        // SLO 0.9 -> error_budget = 0.1
1892        // 1 error out of 20 -> error_rate = 0.05
1893        // saturation = 0.05 / 0.1 = 0.5
1894        // remaining = 1 - 0.5 = 0.5
1895        let monitor = SlaMonitor::new(0.9);
1896        for i in 0..20 {
1897            monitor.observe("op", Duration::from_millis(i), i != 5);
1898        }
1899        let report = monitor.report("op").expect("report");
1900        assert!((report.error_rate - 0.05).abs() < 1e-9);
1901        assert!((report.saturation - 0.5).abs() < 1e-9);
1902        assert!((report.error_budget_remaining - 0.5).abs() < 1e-9);
1903    }
1904
1905    #[test]
1906    fn test_sla_monitor_operations_isolated() {
1907        let monitor = SlaMonitor::new(0.999);
1908        monitor.observe("op1", Duration::from_millis(10), true);
1909        monitor.observe("op2", Duration::from_millis(20), false);
1910        let mut ops = monitor.operations();
1911        ops.sort();
1912        assert_eq!(ops, vec!["op1".to_string(), "op2".to_string()]);
1913
1914        let r1 = monitor.report("op1").expect("op1 report");
1915        let r2 = monitor.report("op2").expect("op2 report");
1916        assert_eq!(r1.total_count, 1);
1917        assert_eq!(r2.total_count, 1);
1918        assert!((r1.error_rate - 0.0).abs() < 1e-9);
1919        assert!((r2.error_rate - 1.0).abs() < 1e-9);
1920    }
1921
1922    #[test]
1923    fn test_sla_monitor_p95_p99_high_percentile() {
1924        let monitor = SlaMonitor::new(0.999);
1925        for ms in [1, 2, 3, 4, 5, 6, 7, 8, 9, 100] {
1926            monitor.observe("op", Duration::from_millis(ms), true);
1927        }
1928        let report = monitor.report("op").expect("report");
1929        // p95/p99 with nearest rank n=10: ceil(0.95*10)=10, ceil(0.99*10)=10 -> samples[9]=100
1930        assert!((report.p95_ms - 100.0).abs() < 1e-9);
1931        assert!((report.p99_ms - 100.0).abs() < 1e-9);
1932    }
1933
1934    #[test]
1935    fn test_sla_monitor_observe_aggregates_multiple_calls() {
1936        let monitor = SlaMonitor::new(0.999);
1937        for _ in 0..100 {
1938            monitor.observe("op", Duration::from_millis(5), true);
1939        }
1940        let report = monitor.report("op").expect("report");
1941        assert_eq!(report.total_count, 100);
1942        assert!((report.p50_ms - 5.0).abs() < 1e-9);
1943    }
1944
1945    #[test]
1946    fn test_sla_monitor_implements_send_sync() {
1947        fn assert_send_sync<T: Send + Sync>() {}
1948        assert_send_sync::<SlaMonitor>();
1949    }
1950
1951    #[test]
1952    fn test_sla_monitor_concurrent_observe_thread_safe() {
1953        use std::sync::Arc;
1954        use std::thread;
1955
1956        let monitor = Arc::new(SlaMonitor::new(0.999));
1957        let mut handles = vec![];
1958
1959        for t in 0..4 {
1960            let m = Arc::clone(&monitor);
1961            handles.push(thread::spawn(move || {
1962                for i in 0..100 {
1963                    // 10% failure rate (i % 10 == 0)
1964                    m.observe("op", Duration::from_millis(i as u64), i % 10 != 0);
1965                }
1966                // Touch another operation per thread to verify isolation.
1967                let op_name = format!("thread-{t}");
1968                m.observe(&op_name, Duration::from_millis(1), true);
1969            }));
1970        }
1971
1972        for h in handles {
1973            h.join().unwrap();
1974        }
1975
1976        let report = monitor.report("op").expect("op report");
1977        assert_eq!(report.total_count, 400);
1978        // 10% error rate
1979        assert!((report.error_rate - 0.1).abs() < 1e-9);
1980
1981        // Each thread registered its own operation.
1982        let mut ops = monitor.operations();
1983        ops.sort();
1984        // "op" + 4 thread-specific operations
1985        assert_eq!(ops.len(), 5);
1986        assert!(ops.contains(&"op".to_string()));
1987    }
1988
1989    #[test]
1990    fn test_sla_monitor_report_slo_one_with_no_errors() {
1991        // SLO = 1.0 with no errors -> full budget, zero saturation.
1992        let monitor = SlaMonitor::new(1.0);
1993        monitor.observe("op", Duration::from_millis(10), true);
1994        let report = monitor.report("op").expect("report");
1995        assert!((report.error_budget_remaining - 1.0).abs() < 1e-9);
1996        assert!((report.saturation - 0.0).abs() < 1e-9);
1997    }
1998
1999    #[test]
2000    fn test_sla_monitor_report_slo_one_with_errors() {
2001        // SLO = 1.0 with any error -> budget fully exhausted.
2002        let monitor = SlaMonitor::new(1.0);
2003        monitor.observe("op", Duration::from_millis(10), false);
2004        let report = monitor.report("op").expect("report");
2005        assert!((report.error_budget_remaining - 0.0).abs() < 1e-9);
2006        assert!((report.saturation - 1.0).abs() < 1e-9);
2007    }
2008
2009    #[test]
2010    fn test_sla_report_fields_are_public() {
2011        // Verifies all SlaReport fields are accessible per the spec.
2012        let monitor = SlaMonitor::new(0.999);
2013        monitor.observe("op", Duration::from_millis(10), true);
2014        let r = monitor.report("op").unwrap();
2015        let _p50: f64 = r.p50_ms;
2016        let _p95: f64 = r.p95_ms;
2017        let _p99: f64 = r.p99_ms;
2018        let _erate: f64 = r.error_rate;
2019        let _total: usize = r.total_count;
2020        let _slo: f64 = r.slo_target;
2021        let _ebr: f64 = r.error_budget_remaining;
2022        let _sat: f64 = r.saturation;
2023    }
2024}
2025
2026// ============================================================================
2027// OTLP Exporter(feature = "otlp")
2028// ============================================================================
2029
2030/// OTLP exporter 配置
2031///
2032/// 用于将 SZ-ORM tracing 的 Span 通过 OpenTelemetry OTLP 协议导出到 Collector。
2033///
2034/// # 示例
2035///
2036/// ```no_run
2037/// # #[cfg(feature = "otlp")] {
2038/// use sz_orm_tracing::OtlpConfig;
2039///
2040/// let config = OtlpConfig {
2041///     endpoint: "http://localhost:4317".to_string(),
2042///     service_name: "sz-orm-app".to_string(),
2043///     timeout_ms: 5000,
2044/// };
2045/// # }
2046/// ```
2047#[cfg(feature = "otlp")]
2048#[derive(Debug, Clone)]
2049pub struct OtlpConfig {
2050    /// OTLP gRPC endpoint(如 `http://localhost:4317`)
2051    pub endpoint: String,
2052    /// 服务名(出现在 trace 的 service.name 标签)
2053    pub service_name: String,
2054    /// 导出超时(毫秒)
2055    pub timeout_ms: u64,
2056}
2057
2058#[cfg(feature = "otlp")]
2059impl Default for OtlpConfig {
2060    fn default() -> Self {
2061        Self {
2062            endpoint: "http://localhost:4317".to_string(),
2063            service_name: "sz-orm".to_string(),
2064            timeout_ms: 5000,
2065        }
2066    }
2067}
2068
2069/// 初始化 OTLP exporter
2070///
2071/// 将 SZ-ORM 的 tracing 接入 OpenTelemetry,使 Span 自动导出到 Collector。
2072///
2073/// # 错误
2074///
2075/// - `TracingError::OtlpInitFailed`:初始化失败
2076///
2077/// # 示例
2078///
2079/// ```no_run
2080/// # #[cfg(feature = "otlp")] {
2081/// # let rt = tokio::runtime::Runtime::new().unwrap();
2082/// # rt.block_on(async {
2083/// use sz_orm_tracing::{init_otlp_exporter, OtlpConfig};
2084///
2085/// let _guard = init_otlp_exporter(OtlpConfig::default()).await.unwrap();
2086/// // 此后通过 `Tracer` 上报的 Span 将自动导出到 OTLP Collector
2087/// # });
2088/// # }
2089/// ```
2090#[cfg(feature = "otlp")]
2091pub async fn init_otlp_exporter(config: OtlpConfig) -> Result<OtlpGuard, TracingError> {
2092    use opentelemetry_otlp::{SpanExporter, WithExportConfig};
2093    use opentelemetry_sdk::resource::Resource;
2094    use opentelemetry_sdk::runtime::Tokio;
2095    use opentelemetry_sdk::trace::TracerProvider;
2096    use std::time::Duration;
2097
2098    let exporter = SpanExporter::builder()
2099        .with_tonic()
2100        .with_endpoint(config.endpoint.clone())
2101        .with_timeout(Duration::from_millis(config.timeout_ms))
2102        .build()
2103        .map_err(|e| TracingError::OtlpInitFailed(format!("exporter build: {e}")))?;
2104
2105    let provider = TracerProvider::builder()
2106        .with_batch_exporter(exporter, Tokio)
2107        .with_resource(Resource::new_with_defaults([opentelemetry::KeyValue::new(
2108            "service.name",
2109            config.service_name.clone(),
2110        )]))
2111        .build();
2112
2113    // 设置为全局 provider
2114    opentelemetry::global::set_tracer_provider(provider.clone());
2115
2116    Ok(OtlpGuard { provider })
2117}
2118
2119/// OTLP exporter 守卫
2120///
2121/// drop 时优雅关闭 exporter,确保所有 Span 已导出。
2122#[cfg(feature = "otlp")]
2123pub struct OtlpGuard {
2124    provider: opentelemetry_sdk::trace::TracerProvider,
2125}
2126
2127#[cfg(feature = "otlp")]
2128impl Drop for OtlpGuard {
2129    fn drop(&mut self) {
2130        // 优雅关闭:未发送的 span 会被丢弃(不阻塞)
2131        let _ = self.provider.shutdown();
2132    }
2133}