Skip to main content

sz_orm_observability/
lib.rs

1//! SZ-ORM 可观测性模块
2//!
3//! 提供 Prometheus exporter、OTLP exporter、SLO 燃烧率监控等能力,
4//! 与 `sz-orm-tracing` 配合形成完整的可观测性闭环。
5//!
6//! # 核心能力
7//!
8//! ## 1. MetricsRegistry(默认启用)
9//!
10//! 统一的指标注册中心,支持 Counter / Gauge / Histogram 三种类型,
11//! 内置线程安全(`RwLock`),可通过 `render()` 输出 Prometheus 文本格式。
12//!
13//! ## 2. Prometheus exporter(feature = "prometheus")
14//!
15//! 在指定端口暴露 `/metrics` HTTP 端点,供 Prometheus 拉取。
16//!
17//! ## 3. OTLP exporter(feature = "otlp")
18//!
19//! 通过 OpenTelemetry OTLP 协议将 traces 导出到 Collector。
20//!
21//! ## 4. SLO 燃烧率
22//!
23//! 基于 5m / 1h 两个窗口计算 SLO 燃烧率,支持多窗口告警。
24//!
25//! # 快速入门
26//!
27//! ```no_run
28//! use sz_orm_observability::{MetricsRegistry, MetricKind};
29//! use std::time::Duration;
30//!
31//! // 创建指标注册中心
32//! let registry = MetricsRegistry::new();
33//!
34//! // 注册指标
35//! let counter = registry.register_counter("sz_orm_pool_acquires_total", "Total pool acquire calls");
36//! let gauge = registry.register_gauge("sz_orm_pool_active_connections", "Current active connections");
37//! let histogram = registry.register_histogram(
38//!     "sz_orm_query_duration_seconds",
39//!     "Query duration in seconds",
40//!     vec![0.001, 0.01, 0.1, 1.0, 10.0],
41//! );
42//!
43//! // 更新指标
44//! counter.inc();
45//! gauge.set(5.0);
46//! histogram.observe(0.025);
47//!
48//! // 输出 Prometheus 文本格式
49//! let output = registry.render();
50//! println!("{}", output);
51//! ```
52
53#![warn(missing_docs)]
54
55use parking_lot::RwLock;
56use std::collections::HashMap;
57use std::sync::Arc;
58
59pub mod slo;
60
61pub use slo::{SloBurnRate, SloConfig, SloMonitor};
62
63/// 指标类型
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum MetricKind {
66    /// 单调递增计数器(如总请求数)
67    Counter,
68    /// 可增可减的瞬时值(如当前连接数)
69    Gauge,
70    /// 直方图(如请求延迟分布)
71    Histogram,
72}
73
74/// 指标元数据
75#[derive(Debug, Clone)]
76pub struct MetricMeta {
77    /// 指标名(如 `sz_orm_pool_acquires_total`)
78    pub name: String,
79    /// 帮助文本
80    pub help: String,
81    /// 指标类型
82    pub kind: MetricKind,
83}
84
85/// 计数器(单调递增)
86pub struct Counter {
87    name: String,
88    value: Arc<RwLock<f64>>,
89    labels: HashMap<String, String>,
90}
91
92impl Counter {
93    /// 递增 1
94    pub fn inc(&self) {
95        self.inc_by(1.0);
96    }
97
98    /// 递增指定值
99    pub fn inc_by(&self, delta: f64) {
100        let mut v = self.value.write();
101        *v += delta;
102    }
103
104    /// 当前值
105    pub fn value(&self) -> f64 {
106        *self.value.read()
107    }
108
109    /// 指标名
110    pub fn name(&self) -> &str {
111        &self.name
112    }
113
114    /// 渲染为 Prometheus 文本格式
115    pub fn render(&self) -> String {
116        let v = self.value.read();
117        if self.labels.is_empty() {
118            format!("{} {}\n", self.name, v)
119        } else {
120            let labels: Vec<String> = self
121                .labels
122                .iter()
123                .map(|(k, val)| format!("{}=\"{}\"", k, val.replace('"', "\\\"")))
124                .collect();
125            format!("{}{{{}}} {}\n", self.name, labels.join(","), v)
126        }
127    }
128}
129
130/// Gauge(可增可减)
131pub struct Gauge {
132    name: String,
133    value: Arc<RwLock<f64>>,
134    labels: HashMap<String, String>,
135}
136
137impl Gauge {
138    /// 设置值
139    pub fn set(&self, value: f64) {
140        *self.value.write() = value;
141    }
142
143    /// 递增
144    pub fn inc(&self) {
145        self.inc_by(1.0);
146    }
147
148    /// 递增指定值
149    pub fn inc_by(&self, delta: f64) {
150        let mut v = self.value.write();
151        *v += delta;
152    }
153
154    /// 递减指定值
155    pub fn dec_by(&self, delta: f64) {
156        let mut v = self.value.write();
157        *v -= delta;
158    }
159
160    /// 当前值
161    pub fn value(&self) -> f64 {
162        *self.value.read()
163    }
164
165    /// 指标名
166    pub fn name(&self) -> &str {
167        &self.name
168    }
169
170    /// 渲染为 Prometheus 文本格式
171    pub fn render(&self) -> String {
172        let v = self.value.read();
173        if self.labels.is_empty() {
174            format!("{} {}\n", self.name, v)
175        } else {
176            let labels: Vec<String> = self
177                .labels
178                .iter()
179                .map(|(k, val)| format!("{}=\"{}\"", k, val.replace('"', "\\\"")))
180                .collect();
181            format!("{}{{{}}} {}\n", self.name, labels.join(","), v)
182        }
183    }
184}
185
186/// 直方图(延迟分布等)
187pub struct Histogram {
188    name: String,
189    buckets: Vec<f64>,
190    counts: Arc<RwLock<Vec<u64>>>,
191    sum: Arc<RwLock<f64>>,
192    count: Arc<RwLock<u64>>,
193}
194
195impl Histogram {
196    /// 观察一个值
197    pub fn observe(&self, value: f64) {
198        let mut counts = self.counts.write();
199        for (i, bucket) in self.buckets.iter().enumerate() {
200            if value <= *bucket {
201                counts[i] += 1;
202            }
203        }
204        // 最后一个 bucket 是 +Inf,必须递增
205        let last = counts.len() - 1;
206        counts[last] += 1;
207
208        let mut sum = self.sum.write();
209        *sum += value;
210        let mut count = self.count.write();
211        *count += 1;
212    }
213
214    /// 总观察次数
215    pub fn count(&self) -> u64 {
216        *self.count.read()
217    }
218
219    /// 所有观察值之和
220    pub fn sum(&self) -> f64 {
221        *self.sum.read()
222    }
223
224    /// 指标名
225    pub fn name(&self) -> &str {
226        &self.name
227    }
228
229    /// 渲染为 Prometheus 文本格式
230    pub fn render(&self) -> String {
231        let counts = self.counts.read();
232        let sum = self.sum.read();
233        let count = self.count.read();
234
235        let mut output = String::new();
236        for (i, bucket) in self.buckets.iter().enumerate() {
237            output.push_str(&format!(
238                "{}_bucket{{le=\"{}\"}} {}\n",
239                self.name, bucket, counts[i]
240            ));
241        }
242        output.push_str(&format!("{}_sum {}\n", self.name, sum));
243        output.push_str(&format!("{}_count {}\n", self.name, count));
244        output
245    }
246}
247
248/// 指标注册中心
249pub struct MetricsRegistry {
250    counters: RwLock<HashMap<String, Arc<Counter>>>,
251    gauges: RwLock<HashMap<String, Arc<Gauge>>>,
252    histograms: RwLock<HashMap<String, Arc<Histogram>>>,
253    metas: RwLock<Vec<MetricMeta>>,
254}
255
256impl Default for MetricsRegistry {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262impl MetricsRegistry {
263    /// 创建空注册中心
264    pub fn new() -> Self {
265        Self {
266            counters: RwLock::new(HashMap::new()),
267            gauges: RwLock::new(HashMap::new()),
268            histograms: RwLock::new(HashMap::new()),
269            metas: RwLock::new(Vec::new()),
270        }
271    }
272
273    /// 注册 Counter
274    pub fn register_counter(&self, name: &str, help: &str) -> Arc<Counter> {
275        self.register_counter_with_labels(name, help, HashMap::new())
276    }
277
278    /// 注册带标签的 Counter
279    pub fn register_counter_with_labels(
280        &self,
281        name: &str,
282        help: &str,
283        labels: HashMap<String, String>,
284    ) -> Arc<Counter> {
285        let mut counters = self.counters.write();
286        let key = format!("{}_{:?}", name, labels);
287        if let Some(c) = counters.get(&key) {
288            return c.clone();
289        }
290        let counter = Arc::new(Counter {
291            name: name.to_string(),
292            value: Arc::new(RwLock::new(0.0)),
293            labels,
294        });
295        counters.insert(key, counter.clone());
296
297        let mut metas = self.metas.write();
298        metas.push(MetricMeta {
299            name: name.to_string(),
300            help: help.to_string(),
301            kind: MetricKind::Counter,
302        });
303        counter
304    }
305
306    /// 注册 Gauge
307    pub fn register_gauge(&self, name: &str, help: &str) -> Arc<Gauge> {
308        self.register_gauge_with_labels(name, help, HashMap::new())
309    }
310
311    /// 注册带标签的 Gauge
312    pub fn register_gauge_with_labels(
313        &self,
314        name: &str,
315        help: &str,
316        labels: HashMap<String, String>,
317    ) -> Arc<Gauge> {
318        let mut gauges = self.gauges.write();
319        let key = format!("{}_{:?}", name, labels);
320        if let Some(g) = gauges.get(&key) {
321            return g.clone();
322        }
323        let gauge = Arc::new(Gauge {
324            name: name.to_string(),
325            value: Arc::new(RwLock::new(0.0)),
326            labels,
327        });
328        gauges.insert(key, gauge.clone());
329
330        let mut metas = self.metas.write();
331        metas.push(MetricMeta {
332            name: name.to_string(),
333            help: help.to_string(),
334            kind: MetricKind::Gauge,
335        });
336        gauge
337    }
338
339    /// 注册 Histogram
340    pub fn register_histogram(&self, name: &str, help: &str, buckets: Vec<f64>) -> Arc<Histogram> {
341        let mut histograms = self.histograms.write();
342        if let Some(h) = histograms.get(name) {
343            return h.clone();
344        }
345        // 最后一个 bucket 必须是 +Inf
346        let mut all_buckets = buckets;
347        all_buckets.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
348        if !all_buckets.contains(&f64::INFINITY) {
349            all_buckets.push(f64::INFINITY);
350        }
351        let count = all_buckets.len();
352        let histogram = Arc::new(Histogram {
353            name: name.to_string(),
354            buckets: all_buckets,
355            counts: Arc::new(RwLock::new(vec![0; count])),
356            sum: Arc::new(RwLock::new(0.0)),
357            count: Arc::new(RwLock::new(0)),
358        });
359        histograms.insert(name.to_string(), histogram.clone());
360
361        let mut metas = self.metas.write();
362        metas.push(MetricMeta {
363            name: name.to_string(),
364            help: help.to_string(),
365            kind: MetricKind::Histogram,
366        });
367        histogram
368    }
369
370    /// 渲染所有指标为 Prometheus 文本格式
371    pub fn render(&self) -> String {
372        let mut output = String::new();
373
374        // 输出 HELP/TYPE 头
375        let metas = self.metas.read();
376        let mut seen = std::collections::HashSet::new();
377        for meta in metas.iter() {
378            if seen.contains(&meta.name) {
379                continue;
380            }
381            seen.insert(meta.name.clone());
382            output.push_str(&format!("# HELP {} {}\n", meta.name, meta.help));
383            let type_str = match meta.kind {
384                MetricKind::Counter => "counter",
385                MetricKind::Gauge => "gauge",
386                MetricKind::Histogram => "histogram",
387            };
388            output.push_str(&format!("# TYPE {} {}\n", meta.name, type_str));
389        }
390
391        // 输出 Counter 值
392        let counters = self.counters.read();
393        for c in counters.values() {
394            output.push_str(&c.render());
395        }
396
397        // 输出 Gauge 值
398        let gauges = self.gauges.read();
399        for g in gauges.values() {
400            output.push_str(&g.render());
401        }
402
403        // 输出 Histogram 值
404        let histograms = self.histograms.read();
405        for h in histograms.values() {
406            output.push_str(&h.render());
407        }
408
409        output
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn test_counter_basic() {
419        let registry = MetricsRegistry::new();
420        let counter = registry.register_counter("test_counter", "Test counter");
421        counter.inc();
422        counter.inc_by(2.5);
423        assert_eq!(counter.value(), 3.5);
424    }
425
426    #[test]
427    fn test_gauge_basic() {
428        let registry = MetricsRegistry::new();
429        let gauge = registry.register_gauge("test_gauge", "Test gauge");
430        gauge.set(10.0);
431        gauge.inc();
432        gauge.dec_by(3.0);
433        assert_eq!(gauge.value(), 8.0);
434    }
435
436    #[test]
437    fn test_histogram_basic() {
438        let registry = MetricsRegistry::new();
439        let histogram =
440            registry.register_histogram("test_histogram", "Test histogram", vec![0.1, 0.5, 1.0]);
441        histogram.observe(0.05);
442        histogram.observe(0.2);
443        histogram.observe(0.6);
444        histogram.observe(1.5);
445
446        assert_eq!(histogram.count(), 4);
447        assert!((histogram.sum() - 2.35).abs() < 1e-9);
448    }
449
450    #[test]
451    fn test_render_prometheus_format() {
452        let registry = MetricsRegistry::new();
453        let counter = registry.register_counter("ops_total", "Total operations");
454        let gauge = registry.register_gauge("conn_active", "Active connections");
455        let histogram =
456            registry.register_histogram("latency_seconds", "Latency in seconds", vec![0.01, 0.1]);
457
458        counter.inc_by(10.0);
459        gauge.set(5.0);
460        histogram.observe(0.005);
461        histogram.observe(0.05);
462        histogram.observe(0.5);
463
464        let output = registry.render();
465        assert!(output.contains("# HELP ops_total Total operations"));
466        assert!(output.contains("# TYPE ops_total counter"));
467        assert!(output.contains("ops_total 10"));
468        assert!(output.contains("conn_active 5"));
469        assert!(output.contains("latency_seconds_bucket{le=\"0.01\"} 1"));
470        assert!(output.contains("latency_seconds_bucket{le=\"0.1\"} 2"));
471        assert!(output.contains("latency_seconds_sum 0.555"));
472        assert!(output.contains("latency_seconds_count 3"));
473    }
474
475    #[test]
476    fn test_counter_with_labels() {
477        let registry = MetricsRegistry::new();
478        let mut labels = HashMap::new();
479        labels.insert("method".to_string(), "GET".to_string());
480        labels.insert("status".to_string(), "200".to_string());
481
482        let counter =
483            registry.register_counter_with_labels("http_requests_total", "HTTP requests", labels);
484        counter.inc();
485        let output = registry.render();
486        // HashMap 顺序未定义,分别验证各标签
487        assert!(output.contains("http_requests_total{"));
488        assert!(output.contains("method=\"GET\""));
489        assert!(output.contains("status=\"200\""));
490        assert!(output.contains("} 1"));
491    }
492}