Skip to main content

sz_orm_core/
telemetry.rs

1//! # 可观测性遥测(Telemetry)
2//!
3//! 提供结构化链路追踪 span + 指标记录,基于 `tracing` crate。
4//!
5//! ## OpenTelemetry 桥接
6//!
7//! 本模块仅创建 `tracing` span,不直接依赖 `opentelemetry` SDK。
8//! 用户可通过 `tracing-opentelemetry` 桥接器将 span 导出为 OpenTelemetry 格式:
9//!
10//! ```ignore
11//! use tracing_subscriber::layer::SubscriberExt;
12//! use tracing_opentelemetry::OpenTelemetryLayer;
13//!
14//! let tracer = opentelemetry::global::tracer("sz-orm");
15//! let telemetry_layer = OpenTelemetryLayer::new(tracer);
16//! let subscriber = tracing_subscriber::fmt::layer()
17//!     .with_subscriber(tracing_subscriber::registry().with(telemetry_layer));
18//! tracing::subscriber::set_global_default(subscriber).unwrap();
19//! ```
20//!
21//! ## 指标
22//!
23//! - `sz_orm_query_duration` — 查询耗时(毫秒)
24//! - `sz_orm_query_rows` — 查询返回行数
25//! - `sz_orm_pool_acquire_duration` — 连接获取耗时(毫秒)
26//! - `sz_orm_pool_size` — 当前连接池大小
27
28use std::sync::atomic::{AtomicU64, Ordering};
29use std::sync::Arc;
30use std::time::{Duration, Instant};
31use tracing::Instrument;
32
33/// 遥测配置
34#[derive(Debug, Clone)]
35pub struct TelemetryConfig {
36    /// 服务名(用于 span 属性 `service.name`)
37    pub service_name: String,
38    /// 是否启用查询 span(默认 true)
39    pub enable_query_span: bool,
40    /// 是否连接池 span(默认 true)
41    pub enable_pool_span: bool,
42    /// 采样率(0.0 ~ 1.0,1.0 = 全采样,默认 1.0)
43    pub sample_rate: f64,
44}
45
46impl Default for TelemetryConfig {
47    fn default() -> Self {
48        Self {
49            service_name: "sz-orm".to_string(),
50            enable_query_span: true,
51            enable_pool_span: true,
52            sample_rate: 1.0,
53        }
54    }
55}
56
57impl TelemetryConfig {
58    /// 创建默认配置(指定服务名)
59    pub fn new(service_name: impl Into<String>) -> Self {
60        Self {
61            service_name: service_name.into(),
62            ..Default::default()
63        }
64    }
65
66    /// 设置采样率(自动限制在 0.0 ~ 1.0)
67    pub fn with_sample_rate(mut self, rate: f64) -> Self {
68        self.sample_rate = rate.clamp(0.0, 1.0);
69        self
70    }
71
72    /// 设置是否启用查询 span
73    pub fn with_query_span(mut self, enabled: bool) -> Self {
74        self.enable_query_span = enabled;
75        self
76    }
77
78    /// 设置是否启用连接池 span
79    pub fn with_pool_span(mut self, enabled: bool) -> Self {
80        self.enable_pool_span = enabled;
81        self
82    }
83}
84
85/// 遥测指标计数器(无锁原子操作)
86#[derive(Debug, Default)]
87pub struct TelemetryMetrics {
88    /// 累计查询次数
89    query_count: AtomicU64,
90    /// 累计查询耗时(纳秒)
91    query_total_duration_ns: AtomicU64,
92    /// 累计查询行数
93    query_total_rows: AtomicU64,
94    /// 累计连接获取次数
95    pool_acquire_count: AtomicU64,
96    /// 累计连接获取耗时(纳秒)
97    pool_acquire_total_duration_ns: AtomicU64,
98    /// 累计查询错误次数
99    query_error_count: AtomicU64,
100    /// v3.2.0:累计预热成功次数
101    prewarm_count: AtomicU64,
102    /// v3.2.0:累计预热失败次数
103    prewarm_failed_count: AtomicU64,
104    /// v3.2.0:累计预热耗时(纳秒)
105    prewarm_duration_ns: AtomicU64,
106}
107
108impl TelemetryMetrics {
109    /// 创建零值指标计数器
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// 记录一次查询
115    pub fn record_query(&self, duration: Duration, rows: u64) {
116        self.query_count.fetch_add(1, Ordering::Relaxed);
117        self.query_total_duration_ns
118            .fetch_add(duration.as_nanos() as u64, Ordering::Relaxed);
119        self.query_total_rows.fetch_add(rows, Ordering::Relaxed);
120    }
121
122    /// 记录一次查询错误
123    pub fn record_query_error(&self) {
124        self.query_error_count.fetch_add(1, Ordering::Relaxed);
125    }
126
127    /// 记录一次连接获取
128    pub fn record_pool_acquire(&self, duration: Duration) {
129        self.pool_acquire_count.fetch_add(1, Ordering::Relaxed);
130        self.pool_acquire_total_duration_ns
131            .fetch_add(duration.as_nanos() as u64, Ordering::Relaxed);
132    }
133
134    /// v3.2.0:记录预热成功
135    pub fn record_prewarm_success(&self) {
136        self.prewarm_count.fetch_add(1, Ordering::Relaxed);
137    }
138
139    /// v3.2.0:记录预热失败
140    pub fn record_prewarm_failure(&self) {
141        self.prewarm_failed_count.fetch_add(1, Ordering::Relaxed);
142    }
143
144    /// v3.2.0:记录预热耗时
145    pub fn record_prewarm_duration(&self, duration: Duration) {
146        self.prewarm_duration_ns
147            .fetch_add(duration.as_nanos() as u64, Ordering::Relaxed);
148    }
149
150    /// 获取指标快照
151    pub fn snapshot(&self) -> TelemetryMetricsSnapshot {
152        TelemetryMetricsSnapshot {
153            query_count: self.query_count.load(Ordering::Relaxed),
154            query_total_duration: Duration::from_nanos(
155                self.query_total_duration_ns.load(Ordering::Relaxed),
156            ),
157            query_total_rows: self.query_total_rows.load(Ordering::Relaxed),
158            pool_acquire_count: self.pool_acquire_count.load(Ordering::Relaxed),
159            pool_acquire_total_duration: Duration::from_nanos(
160                self.pool_acquire_total_duration_ns.load(Ordering::Relaxed),
161            ),
162            query_error_count: self.query_error_count.load(Ordering::Relaxed),
163            prewarm_count: self.prewarm_count.load(Ordering::Relaxed),
164            prewarm_failed_count: self.prewarm_failed_count.load(Ordering::Relaxed),
165            prewarm_duration: Duration::from_nanos(
166                self.prewarm_duration_ns.load(Ordering::Relaxed),
167            ),
168        }
169    }
170}
171
172/// 指标快照
173#[derive(Debug, Clone)]
174pub struct TelemetryMetricsSnapshot {
175    /// 累计查询次数
176    pub query_count: u64,
177    /// 累计查询耗时
178    pub query_total_duration: Duration,
179    /// 累计查询行数
180    pub query_total_rows: u64,
181    /// 累计连接获取次数
182    pub pool_acquire_count: u64,
183    /// 累计连接获取耗时
184    pub pool_acquire_total_duration: Duration,
185    /// 累计查询错误次数
186    pub query_error_count: u64,
187    /// v3.2.0:预热成功次数
188    pub prewarm_count: u64,
189    /// v3.2.0:预热失败次数
190    pub prewarm_failed_count: u64,
191    /// v3.2.0:预热总耗时
192    pub prewarm_duration: Duration,
193}
194
195impl TelemetryMetricsSnapshot {
196    /// 平均查询耗时
197    pub fn avg_query_duration(&self) -> Duration {
198        if self.query_count == 0 {
199            Duration::ZERO
200        } else {
201            self.query_total_duration / self.query_count as u32
202        }
203    }
204
205    /// 平均查询行数
206    pub fn avg_query_rows(&self) -> f64 {
207        if self.query_count == 0 {
208            0.0
209        } else {
210            self.query_total_rows as f64 / self.query_count as f64
211        }
212    }
213
214    /// 查询错误率
215    pub fn query_error_rate(&self) -> f64 {
216        if self.query_count == 0 {
217            0.0
218        } else {
219            self.query_error_count as f64 / self.query_count as f64
220        }
221    }
222
223    /// 平均连接获取耗时
224    pub fn avg_pool_acquire_duration(&self) -> Duration {
225        if self.pool_acquire_count == 0 {
226            Duration::ZERO
227        } else {
228            self.pool_acquire_total_duration / self.pool_acquire_count as u32
229        }
230    }
231}
232
233/// 遥测上下文 — 持有配置和指标
234#[derive(Clone)]
235pub struct Telemetry {
236    config: Arc<TelemetryConfig>,
237    metrics: Arc<TelemetryMetrics>,
238}
239
240impl Telemetry {
241    /// 创建遥测实例
242    pub fn new(config: TelemetryConfig) -> Self {
243        Self {
244            config: Arc::new(config),
245            metrics: Arc::new(TelemetryMetrics::new()),
246        }
247    }
248
249    /// 获取配置引用
250    pub fn config(&self) -> &TelemetryConfig {
251        &self.config
252    }
253
254    /// 获取指标快照
255    pub fn metrics(&self) -> TelemetryMetricsSnapshot {
256        self.metrics.snapshot()
257    }
258
259    /// 创建查询 span
260    ///
261    /// 返回一个 `QuerySpanGuard`,drop 时自动记录耗时和行数。
262    pub fn query_span(&self, sql: &str) -> QuerySpanGuard {
263        if !self.config.enable_query_span {
264            return QuerySpanGuard::disabled(self.metrics.clone());
265        }
266        let span = tracing::info_span!(
267            "sz_orm_query",
268            "otel.name" = "sz_orm.query",
269            "otel.kind" = "client",
270            "service.name" = %self.config.service_name,
271            sql = %sql,
272        );
273        QuerySpanGuard::enabled(self.metrics.clone(), span, Instant::now())
274    }
275
276    /// 创建连接获取 span
277    pub fn pool_acquire_span(&self) -> PoolAcquireSpanGuard {
278        if !self.config.enable_pool_span {
279            return PoolAcquireSpanGuard::disabled(self.metrics.clone());
280        }
281        let span = tracing::info_span!(
282            "sz_orm_pool_acquire",
283            "otel.name" = "sz_orm.pool.acquire",
284            "otel.kind" = "internal",
285            "service.name" = %self.config.service_name,
286        );
287        PoolAcquireSpanGuard::enabled(self.metrics.clone(), span, Instant::now())
288    }
289
290    /// 用查询 span 包装异步查询闭包
291    ///
292    /// ```ignore
293    /// let rows = telemetry.with_query_span("SELECT * FROM users", || async {
294    ///     conn.query(sql).await
295    /// }).await;
296    /// ```
297    pub async fn with_query_span<F, Fut, T>(&self, sql: &str, f: F) -> T
298    where
299        F: FnOnce() -> Fut,
300        Fut: std::future::Future<Output = T>,
301    {
302        if !self.config.enable_query_span {
303            let start = Instant::now();
304            let result = f().await;
305            self.metrics.record_query(start.elapsed(), 0);
306            return result;
307        }
308        let span = tracing::info_span!(
309            "sz_orm_query",
310            "otel.name" = "sz_orm.query",
311            "otel.kind" = "client",
312            "service.name" = %self.config.service_name,
313            sql = %sql,
314        );
315        let start = Instant::now();
316        let result = f().instrument(span).await;
317        self.metrics.record_query(start.elapsed(), 0);
318        result
319    }
320}
321
322impl Default for Telemetry {
323    fn default() -> Self {
324        Self::new(TelemetryConfig::default())
325    }
326}
327
328/// 查询 span 守卫 — drop 时自动记录指标
329pub struct QuerySpanGuard {
330    metrics: Arc<TelemetryMetrics>,
331    span: Option<tracing::span::Span>,
332    start: Instant,
333    rows: u64,
334    error: bool,
335}
336
337impl QuerySpanGuard {
338    fn enabled(metrics: Arc<TelemetryMetrics>, span: tracing::span::Span, start: Instant) -> Self {
339        Self {
340            metrics,
341            span: Some(span),
342            start,
343            rows: 0,
344            error: false,
345        }
346    }
347
348    fn disabled(metrics: Arc<TelemetryMetrics>) -> Self {
349        Self {
350            metrics,
351            span: None,
352            start: Instant::now(),
353            rows: 0,
354            error: false,
355        }
356    }
357
358    /// 记录返回行数
359    pub fn set_rows(&mut self, rows: u64) {
360        self.rows = rows;
361    }
362
363    /// 标记查询出错
364    pub fn set_error(&mut self) {
365        self.error = true;
366    }
367
368    /// 进入 span 上下文
369    pub fn enter(&self) -> Option<tracing::span::Entered<'_>> {
370        self.span.as_ref().map(|s| s.enter())
371    }
372}
373
374impl Drop for QuerySpanGuard {
375    fn drop(&mut self) {
376        let duration = self.start.elapsed();
377        self.metrics.record_query(duration, self.rows);
378        if self.error {
379            self.metrics.record_query_error();
380        }
381        if let Some(ref span) = self.span {
382            span.record("query.duration_ms", duration.as_millis() as u64);
383            span.record("query.rows", self.rows);
384            span.record("query.error", self.error);
385        }
386    }
387}
388
389/// 连接获取 span 守卫
390pub struct PoolAcquireSpanGuard {
391    metrics: Arc<TelemetryMetrics>,
392    span: Option<tracing::span::Span>,
393    start: Instant,
394}
395
396impl PoolAcquireSpanGuard {
397    fn enabled(metrics: Arc<TelemetryMetrics>, span: tracing::span::Span, start: Instant) -> Self {
398        Self {
399            metrics,
400            span: Some(span),
401            start,
402        }
403    }
404
405    fn disabled(metrics: Arc<TelemetryMetrics>) -> Self {
406        Self {
407            metrics,
408            span: None,
409            start: Instant::now(),
410        }
411    }
412
413    /// 进入 span(若存在)
414    pub fn enter(&self) -> Option<tracing::span::Entered<'_>> {
415        self.span.as_ref().map(|s| s.enter())
416    }
417}
418
419impl Drop for PoolAcquireSpanGuard {
420    fn drop(&mut self) {
421        let duration = self.start.elapsed();
422        self.metrics.record_pool_acquire(duration);
423        if let Some(ref span) = self.span {
424            span.record("pool.acquire_duration_ms", duration.as_millis() as u64);
425        }
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn test_telemetry_config_defaults() {
435        let config = TelemetryConfig::default();
436        assert_eq!(config.service_name, "sz-orm");
437        assert!(config.enable_query_span);
438        assert!(config.enable_pool_span);
439        assert_eq!(config.sample_rate, 1.0);
440    }
441
442    #[test]
443    fn test_telemetry_config_builders() {
444        let config = TelemetryConfig::new("my-service")
445            .with_sample_rate(0.5)
446            .with_query_span(false)
447            .with_pool_span(false);
448        assert_eq!(config.service_name, "my-service");
449        assert_eq!(config.sample_rate, 0.5);
450        assert!(!config.enable_query_span);
451        assert!(!config.enable_pool_span);
452    }
453
454    #[test]
455    fn test_sample_rate_clamped() {
456        let config = TelemetryConfig::default().with_sample_rate(2.0);
457        assert_eq!(config.sample_rate, 1.0);
458
459        let config = TelemetryConfig::default().with_sample_rate(-1.0);
460        assert_eq!(config.sample_rate, 0.0);
461    }
462
463    #[test]
464    fn test_metrics_record_query() {
465        let metrics = TelemetryMetrics::new();
466        metrics.record_query(Duration::from_millis(100), 10);
467        metrics.record_query(Duration::from_millis(200), 20);
468
469        let snap = metrics.snapshot();
470        assert_eq!(snap.query_count, 2);
471        assert_eq!(snap.query_total_duration, Duration::from_millis(300));
472        assert_eq!(snap.query_total_rows, 30);
473        assert_eq!(snap.avg_query_duration(), Duration::from_millis(150));
474        assert_eq!(snap.avg_query_rows(), 15.0);
475    }
476
477    #[test]
478    fn test_metrics_record_error() {
479        let metrics = TelemetryMetrics::new();
480        metrics.record_query(Duration::from_millis(50), 0);
481        metrics.record_query_error();
482        metrics.record_query(Duration::from_millis(100), 5);
483
484        let snap = metrics.snapshot();
485        assert_eq!(snap.query_count, 2);
486        assert_eq!(snap.query_error_count, 1);
487        assert_eq!(snap.query_error_rate(), 0.5);
488    }
489
490    #[test]
491    fn test_metrics_record_pool_acquire() {
492        let metrics = TelemetryMetrics::new();
493        metrics.record_pool_acquire(Duration::from_millis(10));
494        metrics.record_pool_acquire(Duration::from_millis(30));
495
496        let snap = metrics.snapshot();
497        assert_eq!(snap.pool_acquire_count, 2);
498        assert_eq!(snap.pool_acquire_total_duration, Duration::from_millis(40));
499        assert_eq!(snap.avg_pool_acquire_duration(), Duration::from_millis(20));
500    }
501
502    #[test]
503    fn test_metrics_empty_snapshot() {
504        let metrics = TelemetryMetrics::new();
505        let snap = metrics.snapshot();
506        assert_eq!(snap.query_count, 0);
507        assert_eq!(snap.avg_query_duration(), Duration::ZERO);
508        assert_eq!(snap.avg_query_rows(), 0.0);
509        assert_eq!(snap.query_error_rate(), 0.0);
510        assert_eq!(snap.avg_pool_acquire_duration(), Duration::ZERO);
511    }
512
513    #[test]
514    fn test_query_span_guard_records_on_drop() {
515        let telemetry = Telemetry::default();
516        {
517            let mut guard = telemetry.query_span("SELECT 1");
518            guard.set_rows(42);
519        }
520        let snap = telemetry.metrics();
521        assert_eq!(snap.query_count, 1);
522        assert_eq!(snap.query_total_rows, 42);
523    }
524
525    #[test]
526    fn test_query_span_guard_error() {
527        let telemetry = Telemetry::default();
528        {
529            let mut guard = telemetry.query_span("SELECT bad");
530            guard.set_error();
531        }
532        let snap = telemetry.metrics();
533        assert_eq!(snap.query_count, 1);
534        assert_eq!(snap.query_error_count, 1);
535        assert_eq!(snap.query_error_rate(), 1.0);
536    }
537
538    #[test]
539    fn test_pool_acquire_span_guard_records_on_drop() {
540        let telemetry = Telemetry::default();
541        {
542            let _guard = telemetry.pool_acquire_span();
543        }
544        let snap = telemetry.metrics();
545        assert_eq!(snap.pool_acquire_count, 1);
546    }
547
548    #[tokio::test]
549    async fn test_with_query_span_async() {
550        let telemetry = Telemetry::default();
551        let result = telemetry.with_query_span("SELECT 1", || async { 42 }).await;
552        assert_eq!(result, 42);
553        let snap = telemetry.metrics();
554        assert_eq!(snap.query_count, 1);
555    }
556
557    #[test]
558    fn test_telemetry_disabled_spans() {
559        let config = TelemetryConfig::default()
560            .with_query_span(false)
561            .with_pool_span(false);
562        let telemetry = Telemetry::new(config);
563        {
564            let _guard = telemetry.query_span("SELECT 1");
565        }
566        {
567            let _guard = telemetry.pool_acquire_span();
568        }
569        let snap = telemetry.metrics();
570        assert_eq!(snap.query_count, 1);
571        assert_eq!(snap.pool_acquire_count, 1);
572    }
573
574    #[test]
575    fn test_telemetry_clone_shares_metrics() {
576        let telemetry = Telemetry::default();
577        let telemetry2 = telemetry.clone();
578        {
579            let _guard = telemetry.query_span("SELECT 1");
580        }
581        {
582            let _guard = telemetry2.query_span("SELECT 2");
583        }
584        let snap = telemetry.metrics();
585        assert_eq!(snap.query_count, 2);
586    }
587}