1use std::sync::atomic::{AtomicU64, Ordering};
29use std::sync::Arc;
30use std::time::{Duration, Instant};
31use tracing::Instrument;
32
33#[derive(Debug, Clone)]
35pub struct TelemetryConfig {
36 pub service_name: String,
38 pub enable_query_span: bool,
40 pub enable_pool_span: bool,
42 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 pub fn new(service_name: impl Into<String>) -> Self {
60 Self {
61 service_name: service_name.into(),
62 ..Default::default()
63 }
64 }
65
66 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 pub fn with_query_span(mut self, enabled: bool) -> Self {
74 self.enable_query_span = enabled;
75 self
76 }
77
78 pub fn with_pool_span(mut self, enabled: bool) -> Self {
80 self.enable_pool_span = enabled;
81 self
82 }
83}
84
85#[derive(Debug, Default)]
87pub struct TelemetryMetrics {
88 query_count: AtomicU64,
90 query_total_duration_ns: AtomicU64,
92 query_total_rows: AtomicU64,
94 pool_acquire_count: AtomicU64,
96 pool_acquire_total_duration_ns: AtomicU64,
98 query_error_count: AtomicU64,
100 prewarm_count: AtomicU64,
102 prewarm_failed_count: AtomicU64,
104 prewarm_duration_ns: AtomicU64,
106}
107
108impl TelemetryMetrics {
109 pub fn new() -> Self {
111 Self::default()
112 }
113
114 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 pub fn record_query_error(&self) {
124 self.query_error_count.fetch_add(1, Ordering::Relaxed);
125 }
126
127 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 pub fn record_prewarm_success(&self) {
136 self.prewarm_count.fetch_add(1, Ordering::Relaxed);
137 }
138
139 pub fn record_prewarm_failure(&self) {
141 self.prewarm_failed_count.fetch_add(1, Ordering::Relaxed);
142 }
143
144 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 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#[derive(Debug, Clone)]
174pub struct TelemetryMetricsSnapshot {
175 pub query_count: u64,
177 pub query_total_duration: Duration,
179 pub query_total_rows: u64,
181 pub pool_acquire_count: u64,
183 pub pool_acquire_total_duration: Duration,
185 pub query_error_count: u64,
187 pub prewarm_count: u64,
189 pub prewarm_failed_count: u64,
191 pub prewarm_duration: Duration,
193}
194
195impl TelemetryMetricsSnapshot {
196 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 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 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 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#[derive(Clone)]
235pub struct Telemetry {
236 config: Arc<TelemetryConfig>,
237 metrics: Arc<TelemetryMetrics>,
238}
239
240impl Telemetry {
241 pub fn new(config: TelemetryConfig) -> Self {
243 Self {
244 config: Arc::new(config),
245 metrics: Arc::new(TelemetryMetrics::new()),
246 }
247 }
248
249 pub fn config(&self) -> &TelemetryConfig {
251 &self.config
252 }
253
254 pub fn metrics(&self) -> TelemetryMetricsSnapshot {
256 self.metrics.snapshot()
257 }
258
259 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 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 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
328pub 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 pub fn set_rows(&mut self, rows: u64) {
360 self.rows = rows;
361 }
362
363 pub fn set_error(&mut self) {
365 self.error = true;
366 }
367
368 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
389pub 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 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}