Skip to main content

fraiseql_server/
metrics_server.rs

1//! Canonical metrics implementation for `fraiseql-server`.
2//!
3//! Use [`MetricsCollector`] to instrument request handling.
4//! The previous `observability/metrics.rs` ghost layer has been removed.
5//!
6//! Tracks:
7//! - GraphQL query execution time
8//! - Query success/error rates
9//! - Database query performance
10//! - Connection pool statistics
11//! - HTTP request/response metrics
12
13use std::{
14    fmt::Write as _,
15    sync::{
16        Arc,
17        atomic::{AtomicU64, Ordering},
18    },
19    time::Instant,
20};
21
22use dashmap::DashMap;
23
24/// Metrics collector for the server.
25///
26/// All atomic counters are bare [`AtomicU64`]; the collector itself is wrapped
27/// in `Arc<MetricsCollector>` at every use site (see
28/// [`AppState::metrics`](crate::routes::graphql::AppState::metrics)). The
29/// previous per-field `Arc<AtomicU64>` was a redundant second indirection
30/// that bloated the struct to ~30 cache lines and made construction 30 atomic
31/// refcount bumps (F009). Call sites are unchanged — `metrics.foo.fetch_add(...)`
32/// resolves to a single Arc deref + atomic op.
33#[derive(Debug, Default)]
34pub struct MetricsCollector {
35    /// Total GraphQL queries executed
36    pub queries_total: AtomicU64,
37
38    /// Total successful queries
39    pub queries_success: AtomicU64,
40
41    /// Total failed queries
42    pub queries_error: AtomicU64,
43
44    /// Total query execution time (microseconds)
45    pub queries_duration_us: AtomicU64,
46
47    /// Total database queries executed
48    pub db_queries_total: AtomicU64,
49
50    /// Total database query time (microseconds)
51    pub db_queries_duration_us: AtomicU64,
52
53    /// Total validation errors
54    pub validation_errors_total: AtomicU64,
55
56    /// Total parse errors
57    pub parse_errors_total: AtomicU64,
58
59    /// Total execution errors
60    pub execution_errors_total: AtomicU64,
61
62    /// Total HTTP requests
63    pub http_requests_total: AtomicU64,
64
65    /// Total HTTP 2xx responses
66    pub http_responses_2xx: AtomicU64,
67
68    /// Total HTTP 4xx responses
69    pub http_responses_4xx: AtomicU64,
70
71    /// Total HTTP 5xx responses
72    pub http_responses_5xx: AtomicU64,
73
74    /// Cache hits
75    pub cache_hits: AtomicU64,
76
77    /// Cache misses
78    pub cache_misses: AtomicU64,
79
80    // Federation Metrics
81    /// Federation entity resolutions (total)
82    pub federation_entity_resolutions_total: AtomicU64,
83
84    /// Federation entity resolutions (errors)
85    pub federation_entity_resolutions_errors: AtomicU64,
86
87    /// Federation entity resolution duration (microseconds)
88    pub federation_entity_resolution_duration_us: AtomicU64,
89
90    /// Federation subgraph requests (total)
91    pub federation_subgraph_requests_total: AtomicU64,
92
93    /// Federation subgraph requests (errors)
94    pub federation_subgraph_requests_errors: AtomicU64,
95
96    /// Federation subgraph request duration (microseconds)
97    pub federation_subgraph_request_duration_us: AtomicU64,
98
99    /// Federation mutations (total)
100    pub federation_mutations_total: AtomicU64,
101
102    /// Federation mutations (errors)
103    pub federation_mutations_errors: AtomicU64,
104
105    /// Federation mutation duration (microseconds)
106    pub federation_mutation_duration_us: AtomicU64,
107
108    /// Federation entity cache hits
109    pub federation_entity_cache_hits: AtomicU64,
110
111    /// Federation entity cache misses
112    pub federation_entity_cache_misses: AtomicU64,
113
114    /// Federation errors
115    pub federation_errors_total: AtomicU64,
116
117    /// Per-operation metrics (histogram + error counter).
118    ///
119    /// Kept behind `Arc` because the registry is shared with other long-lived
120    /// readers (export endpoints) that need to hold a clone independently.
121    pub operation_metrics: Arc<OperationMetricsRegistry>,
122
123    /// HTTP request duration histogram (shared with export endpoints).
124    pub http_request_duration: Arc<Histogram>,
125
126    /// Database query duration histogram (shared with export endpoints).
127    pub db_query_duration: Arc<Histogram>,
128
129    /// Total successful schema reloads
130    pub schema_reloads_total: AtomicU64,
131
132    /// Total failed schema reload attempts
133    pub schema_reload_errors_total: AtomicU64,
134}
135
136impl MetricsCollector {
137    /// Create new metrics collector.
138    #[must_use]
139    pub fn new() -> Self {
140        Self::default()
141    }
142}
143
144impl MetricsCollector {
145    /// Record entity resolution completion (all strategies).
146    ///
147    /// # Arguments
148    ///
149    /// * `duration_us` - Resolution duration in microseconds
150    /// * `success` - Whether resolution succeeded
151    pub fn record_entity_resolution(&self, duration_us: u64, success: bool) {
152        self.federation_entity_resolutions_total.fetch_add(1, Ordering::Relaxed);
153        self.federation_entity_resolution_duration_us
154            .fetch_add(duration_us, Ordering::Relaxed);
155        if !success {
156            self.federation_entity_resolutions_errors.fetch_add(1, Ordering::Relaxed);
157            self.federation_errors_total.fetch_add(1, Ordering::Relaxed);
158        }
159    }
160
161    /// Record subgraph request completion.
162    ///
163    /// # Arguments
164    ///
165    /// * `duration_us` - Request duration in microseconds
166    /// * `success` - Whether request succeeded (HTTP 2xx)
167    pub fn record_subgraph_request(&self, duration_us: u64, success: bool) {
168        self.federation_subgraph_requests_total.fetch_add(1, Ordering::Relaxed);
169        self.federation_subgraph_request_duration_us
170            .fetch_add(duration_us, Ordering::Relaxed);
171        if !success {
172            self.federation_subgraph_requests_errors.fetch_add(1, Ordering::Relaxed);
173            self.federation_errors_total.fetch_add(1, Ordering::Relaxed);
174        }
175    }
176
177    /// Record federation mutation execution.
178    ///
179    /// # Arguments
180    ///
181    /// * `duration_us` - Mutation duration in microseconds
182    /// * `success` - Whether mutation succeeded
183    pub fn record_mutation(&self, duration_us: u64, success: bool) {
184        self.federation_mutations_total.fetch_add(1, Ordering::Relaxed);
185        self.federation_mutation_duration_us.fetch_add(duration_us, Ordering::Relaxed);
186        if !success {
187            self.federation_mutations_errors.fetch_add(1, Ordering::Relaxed);
188            self.federation_errors_total.fetch_add(1, Ordering::Relaxed);
189        }
190    }
191
192    /// Record entity cache hit.
193    pub fn record_entity_cache_hit(&self) {
194        self.federation_entity_cache_hits.fetch_add(1, Ordering::Relaxed);
195    }
196
197    /// Record entity cache miss.
198    pub fn record_entity_cache_miss(&self) {
199        self.federation_entity_cache_misses.fetch_add(1, Ordering::Relaxed);
200    }
201}
202
203/// Histogram bucket upper bounds in microseconds.
204/// Corresponds to: 1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s
205const HISTOGRAM_BUCKET_BOUNDS_US: [u64; 11] = [
206    1_000, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000, 500_000, 1_000_000, 2_500_000,
207    5_000_000,
208];
209
210/// Prometheus `le` labels matching [`HISTOGRAM_BUCKET_BOUNDS_US`], in seconds.
211const HISTOGRAM_LE_LABELS: [&str; 11] = [
212    "0.001", "0.005", "0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1", "2.5", "5",
213];
214
215/// Per-operation metrics: count, total duration, error count, and histogram buckets.
216#[derive(Debug)]
217pub struct OperationMetrics {
218    count:         AtomicU64,
219    duration_us:   AtomicU64,
220    error_count:   AtomicU64,
221    bucket_counts: [AtomicU64; 11],
222}
223
224impl OperationMetrics {
225    fn new() -> Self {
226        Self {
227            count:         AtomicU64::new(0),
228            duration_us:   AtomicU64::new(0),
229            error_count:   AtomicU64::new(0),
230            bucket_counts: std::array::from_fn(|_| AtomicU64::new(0)),
231        }
232    }
233
234    fn record(&self, duration_us: u64, is_error: bool) {
235        self.count.fetch_add(1, Ordering::Relaxed);
236        self.duration_us.fetch_add(duration_us, Ordering::Relaxed);
237        if is_error {
238            self.error_count.fetch_add(1, Ordering::Relaxed);
239        }
240        // Increment only the first (smallest) bucket whose bound >= duration.
241        // The cumulative sum is computed at render time.
242        for (i, &bound) in HISTOGRAM_BUCKET_BOUNDS_US.iter().enumerate() {
243            if duration_us <= bound {
244                self.bucket_counts[i].fetch_add(1, Ordering::Relaxed);
245                return;
246            }
247        }
248        // Duration exceeds all finite buckets — it only appears in +Inf
249    }
250}
251
252/// Registry of per-operation metrics with cardinality guard.
253///
254/// Operations beyond `max_operations` are folded into an `__overflow__` bucket
255/// to prevent unbounded label cardinality.
256#[derive(Debug)]
257pub struct OperationMetricsRegistry {
258    operations:     DashMap<String, OperationMetrics>,
259    max_operations: usize,
260    overflow:       OperationMetrics,
261}
262
263impl OperationMetricsRegistry {
264    /// Create a new registry with the given cardinality limit.
265    #[must_use]
266    pub fn new(max_operations: usize) -> Self {
267        Self {
268            operations: DashMap::new(),
269            max_operations,
270            overflow: OperationMetrics::new(),
271        }
272    }
273
274    /// Record a query execution for the given operation name.
275    pub fn record(&self, name: &str, duration_us: u64, is_error: bool) {
276        let canonical = if name.is_empty() {
277            "__anonymous__"
278        } else {
279            name
280        };
281
282        // Check if already tracked
283        if let Some(entry) = self.operations.get(canonical) {
284            entry.record(duration_us, is_error);
285            return;
286        }
287
288        // Check cardinality limit before inserting
289        if self.operations.len() >= self.max_operations {
290            self.overflow.record(duration_us, is_error);
291            return;
292        }
293
294        // Insert and record (race-safe: entry() handles concurrent inserts)
295        self.operations
296            .entry(canonical.to_owned())
297            .or_insert_with(OperationMetrics::new)
298            .record(duration_us, is_error);
299    }
300
301    /// Render all per-operation metrics in Prometheus text exposition format.
302    #[must_use]
303    pub fn to_prometheus_format(&self) -> String {
304        let mut out = String::new();
305
306        // Collect entries for deterministic output (sorted by name)
307        let mut entries: Vec<(String, u64, u64, u64, [u64; 11])> = self
308            .operations
309            .iter()
310            .map(|e| {
311                let buckets: [u64; 11] =
312                    std::array::from_fn(|i| e.value().bucket_counts[i].load(Ordering::Relaxed));
313                (
314                    e.key().clone(),
315                    e.value().count.load(Ordering::Relaxed),
316                    e.value().duration_us.load(Ordering::Relaxed),
317                    e.value().error_count.load(Ordering::Relaxed),
318                    buckets,
319                )
320            })
321            .collect();
322
323        // Add overflow if it has data
324        let overflow_count = self.overflow.count.load(Ordering::Relaxed);
325        if overflow_count > 0 {
326            let buckets: [u64; 11] =
327                std::array::from_fn(|i| self.overflow.bucket_counts[i].load(Ordering::Relaxed));
328            entries.push((
329                "__overflow__".to_owned(),
330                overflow_count,
331                self.overflow.duration_us.load(Ordering::Relaxed),
332                self.overflow.error_count.load(Ordering::Relaxed),
333                buckets,
334            ));
335        }
336
337        if entries.is_empty() {
338            return out;
339        }
340
341        entries.sort_by(|a, b| a.0.cmp(&b.0));
342
343        // Duration histogram
344        out.push_str(
345            "\n# HELP fraiseql_query_duration_seconds Per-operation query duration histogram\n\
346             # TYPE fraiseql_query_duration_seconds histogram\n",
347        );
348        for (name, count, duration_us, _, buckets) in &entries {
349            let mut cumulative: u64 = 0;
350            for (i, &bucket_count) in buckets.iter().enumerate() {
351                cumulative += bucket_count;
352                let _ = writeln!(
353                    out,
354                    "fraiseql_query_duration_seconds_bucket{{operation=\"{name}\",le=\"{}\"}} \
355                     {cumulative}",
356                    HISTOGRAM_LE_LABELS[i],
357                );
358            }
359            let _ = writeln!(
360                out,
361                "fraiseql_query_duration_seconds_bucket{{operation=\"{name}\",le=\"+Inf\"}} \
362                 {count}",
363            );
364            #[allow(clippy::cast_precision_loss)]
365            // Reason: precision loss is acceptable for metrics reporting
366            let sum_secs = *duration_us as f64 / 1_000_000.0;
367            let _ = writeln!(
368                out,
369                "fraiseql_query_duration_seconds_sum{{operation=\"{name}\"}} {sum_secs:.6}",
370            );
371            let _ = writeln!(
372                out,
373                "fraiseql_query_duration_seconds_count{{operation=\"{name}\"}} {count}",
374            );
375        }
376
377        // Error counter
378        out.push_str(
379            "\n# HELP fraiseql_query_errors_total Per-operation query error count\n\
380             # TYPE fraiseql_query_errors_total counter\n",
381        );
382        for (name, _, _, error_count, _) in &entries {
383            let _ =
384                writeln!(out, "fraiseql_query_errors_total{{operation=\"{name}\"}} {error_count}",);
385        }
386
387        out
388    }
389}
390
391impl Default for OperationMetricsRegistry {
392    fn default() -> Self {
393        Self::new(500)
394    }
395}
396
397/// General-purpose histogram using the standard 11-bucket scheme.
398#[derive(Debug)]
399pub struct Histogram {
400    count:         AtomicU64,
401    sum_us:        AtomicU64,
402    bucket_counts: [AtomicU64; 11],
403}
404
405impl Histogram {
406    /// Create a new empty histogram.
407    #[must_use]
408    pub fn new() -> Self {
409        Self {
410            count:         AtomicU64::new(0),
411            sum_us:        AtomicU64::new(0),
412            bucket_counts: std::array::from_fn(|_| AtomicU64::new(0)),
413        }
414    }
415
416    /// Observe a duration in microseconds.
417    pub fn observe_us(&self, duration_us: u64) {
418        self.count.fetch_add(1, Ordering::Relaxed);
419        self.sum_us.fetch_add(duration_us, Ordering::Relaxed);
420        for (i, &bound) in HISTOGRAM_BUCKET_BOUNDS_US.iter().enumerate() {
421            if duration_us <= bound {
422                self.bucket_counts[i].fetch_add(1, Ordering::Relaxed);
423                return;
424            }
425        }
426    }
427
428    /// Render as Prometheus text format with the given metric name.
429    #[allow(clippy::cast_precision_loss)] // Reason: microsecond precision loss at >2^53 us (~285 years) is acceptable
430    #[must_use]
431    pub fn to_prometheus_lines(&self, name: &str, help: &str) -> String {
432        let mut out = String::new();
433        let _ = writeln!(out, "\n# HELP {name} {help}");
434        let _ = writeln!(out, "# TYPE {name} histogram");
435        let count = self.count.load(Ordering::Relaxed);
436        let sum_us = self.sum_us.load(Ordering::Relaxed);
437        let mut cumulative = 0u64;
438        for (i, le) in HISTOGRAM_LE_LABELS.iter().enumerate() {
439            cumulative += self.bucket_counts[i].load(Ordering::Relaxed);
440            let _ = writeln!(out, "{name}_bucket{{le=\"{le}\"}} {cumulative}");
441        }
442        let _ = writeln!(out, "{name}_bucket{{le=\"+Inf\"}} {count}");
443        let sum_secs = sum_us as f64 / 1_000_000.0;
444        let _ = writeln!(out, "{name}_sum {sum_secs:.6}");
445        let _ = writeln!(out, "{name}_count {count}");
446        out
447    }
448}
449
450impl Default for Histogram {
451    fn default() -> Self {
452        Self::new()
453    }
454}
455
456impl Histogram {
457    /// Estimate the value at a given quantile (0.0–1.0) from bucket data.
458    ///
459    /// Uses linear interpolation within the bucket that contains the target
460    /// quantile. Returns 0 if the histogram is empty.
461    #[allow(clippy::cast_precision_loss)] // Reason: u64 counters in practice are < 2^53
462    #[allow(clippy::cast_possible_truncation)] // Reason: product of u64 count × quantile (0..1) is always ≤ original count
463    #[allow(clippy::cast_sign_loss)] // Reason: quantile is always in [0, 1], so product is non-negative
464    #[must_use]
465    pub fn estimate_quantile_us(&self, quantile: f64) -> u64 {
466        let total = self.count.load(Ordering::Relaxed);
467        if total == 0 {
468            return 0;
469        }
470
471        let target = (total as f64 * quantile) as u64;
472        let mut cumulative = 0u64;
473
474        for (i, &bound) in HISTOGRAM_BUCKET_BOUNDS_US.iter().enumerate() {
475            let bucket_count = self.bucket_counts[i].load(Ordering::Relaxed);
476            cumulative += bucket_count;
477            if cumulative >= target {
478                // Target falls in this bucket — return the bucket upper bound
479                // as the estimate (conservative: overestimates latency slightly).
480                return bound;
481            }
482        }
483
484        // Beyond all buckets — return last bucket bound
485        HISTOGRAM_BUCKET_BOUNDS_US[HISTOGRAM_BUCKET_BOUNDS_US.len() - 1]
486    }
487}
488
489/// Guard for timing metrics.
490pub struct TimingGuard {
491    start:           Instant,
492    duration_atomic: Arc<AtomicU64>,
493}
494
495impl TimingGuard {
496    /// Create new timing guard.
497    pub fn new(duration_atomic: Arc<AtomicU64>) -> Self {
498        Self {
499            start: Instant::now(),
500            duration_atomic,
501        }
502    }
503
504    /// Record duration in microseconds and consume guard.
505    pub fn record(self) {
506        #[allow(clippy::cast_possible_truncation)]
507        // Reason: microsecond counter cannot exceed u64 in any practical uptime
508        let duration_us = self.start.elapsed().as_micros() as u64;
509        self.duration_atomic.fetch_add(duration_us, Ordering::Relaxed);
510    }
511}
512
513/// Prometheus metrics output format.
514#[derive(Debug)]
515pub struct PrometheusMetrics {
516    /// Total GraphQL queries executed
517    pub queries_total:              u64,
518    /// Successful GraphQL queries
519    pub queries_success:            u64,
520    /// Failed GraphQL queries
521    pub queries_error:              u64,
522    /// Average query duration in milliseconds
523    pub queries_avg_duration_ms:    f64,
524    /// Total database queries executed
525    pub db_queries_total:           u64,
526    /// Average database query duration in milliseconds
527    pub db_queries_avg_duration_ms: f64,
528    /// Total validation errors
529    pub validation_errors_total:    u64,
530    /// Total parse errors
531    pub parse_errors_total:         u64,
532    /// Total execution errors
533    pub execution_errors_total:     u64,
534    /// Total HTTP requests processed
535    pub http_requests_total:        u64,
536    /// HTTP 2xx responses
537    pub http_responses_2xx:         u64,
538    /// HTTP 4xx responses
539    pub http_responses_4xx:         u64,
540    /// HTTP 5xx responses
541    pub http_responses_5xx:         u64,
542    /// Cache hit count
543    pub cache_hits:                 u64,
544    /// Cache miss count
545    pub cache_misses:               u64,
546    /// Cache hit ratio (0.0 to 1.0)
547    pub cache_hit_ratio:            f64,
548}
549
550impl PrometheusMetrics {
551    /// Generate Prometheus text format output.
552    #[must_use]
553    pub fn to_prometheus_format(&self) -> String {
554        format!(
555            r"# HELP fraiseql_graphql_queries_total Total GraphQL queries executed
556# TYPE fraiseql_graphql_queries_total counter
557fraiseql_graphql_queries_total {}
558
559# HELP fraiseql_graphql_queries_success Total successful GraphQL queries
560# TYPE fraiseql_graphql_queries_success counter
561fraiseql_graphql_queries_success {}
562
563# HELP fraiseql_graphql_queries_error Total failed GraphQL queries
564# TYPE fraiseql_graphql_queries_error counter
565fraiseql_graphql_queries_error {}
566
567# HELP fraiseql_graphql_query_duration_ms Average query execution time in milliseconds
568# TYPE fraiseql_graphql_query_duration_ms gauge
569fraiseql_graphql_query_duration_ms {}
570
571# HELP fraiseql_database_queries_total Total database queries executed
572# TYPE fraiseql_database_queries_total counter
573fraiseql_database_queries_total {}
574
575# HELP fraiseql_database_query_duration_ms Average database query time in milliseconds
576# TYPE fraiseql_database_query_duration_ms gauge
577fraiseql_database_query_duration_ms {}
578
579# HELP fraiseql_validation_errors_total Total validation errors
580# TYPE fraiseql_validation_errors_total counter
581fraiseql_validation_errors_total {}
582
583# HELP fraiseql_parse_errors_total Total parse errors
584# TYPE fraiseql_parse_errors_total counter
585fraiseql_parse_errors_total {}
586
587# HELP fraiseql_execution_errors_total Total execution errors
588# TYPE fraiseql_execution_errors_total counter
589fraiseql_execution_errors_total {}
590
591# HELP fraiseql_http_requests_total Total HTTP requests
592# TYPE fraiseql_http_requests_total counter
593fraiseql_http_requests_total {}
594
595# HELP fraiseql_http_responses_2xx Total 2xx HTTP responses
596# TYPE fraiseql_http_responses_2xx counter
597fraiseql_http_responses_2xx {}
598
599# HELP fraiseql_http_responses_4xx Total 4xx HTTP responses
600# TYPE fraiseql_http_responses_4xx counter
601fraiseql_http_responses_4xx {}
602
603# HELP fraiseql_http_responses_5xx Total 5xx HTTP responses
604# TYPE fraiseql_http_responses_5xx counter
605fraiseql_http_responses_5xx {}
606
607# HELP fraiseql_cache_hits Total cache hits
608# TYPE fraiseql_cache_hits counter
609fraiseql_cache_hits {}
610
611# HELP fraiseql_cache_misses Total cache misses
612# TYPE fraiseql_cache_misses counter
613fraiseql_cache_misses {}
614
615# HELP fraiseql_cache_hit_ratio Cache hit ratio (0-1)
616# TYPE fraiseql_cache_hit_ratio gauge
617fraiseql_cache_hit_ratio {:.3}
618",
619            self.queries_total,
620            self.queries_success,
621            self.queries_error,
622            self.queries_avg_duration_ms,
623            self.db_queries_total,
624            self.db_queries_avg_duration_ms,
625            self.validation_errors_total,
626            self.parse_errors_total,
627            self.execution_errors_total,
628            self.http_requests_total,
629            self.http_responses_2xx,
630            self.http_responses_4xx,
631            self.http_responses_5xx,
632            self.cache_hits,
633            self.cache_misses,
634            self.cache_hit_ratio,
635        )
636    }
637}
638
639impl From<&MetricsCollector> for PrometheusMetrics {
640    fn from(collector: &MetricsCollector) -> Self {
641        let queries_total = collector.queries_total.load(Ordering::Relaxed);
642        let queries_success = collector.queries_success.load(Ordering::Relaxed);
643        let queries_error = collector.queries_error.load(Ordering::Relaxed);
644        let queries_duration_us = collector.queries_duration_us.load(Ordering::Relaxed);
645
646        let db_queries_total = collector.db_queries_total.load(Ordering::Relaxed);
647        let db_queries_duration_us = collector.db_queries_duration_us.load(Ordering::Relaxed);
648
649        let cache_hits = collector.cache_hits.load(Ordering::Relaxed);
650        let cache_misses = collector.cache_misses.load(Ordering::Relaxed);
651        let cache_total = cache_hits + cache_misses;
652
653        Self {
654            queries_total,
655            queries_success,
656            queries_error,
657            #[allow(clippy::cast_precision_loss)] // Reason: precision loss is acceptable for metrics/statistics
658            queries_avg_duration_ms: if queries_total > 0 {
659                (queries_duration_us as f64 / queries_total as f64) / 1000.0
660            } else {
661                0.0
662            },
663            db_queries_total,
664            #[allow(clippy::cast_precision_loss)] // Reason: precision loss is acceptable for metrics/statistics
665            db_queries_avg_duration_ms: if db_queries_total > 0 {
666                (db_queries_duration_us as f64 / db_queries_total as f64) / 1000.0
667            } else {
668                0.0
669            },
670            validation_errors_total: collector.validation_errors_total.load(Ordering::Relaxed),
671            parse_errors_total: collector.parse_errors_total.load(Ordering::Relaxed),
672            execution_errors_total: collector.execution_errors_total.load(Ordering::Relaxed),
673            http_requests_total: collector.http_requests_total.load(Ordering::Relaxed),
674            http_responses_2xx: collector.http_responses_2xx.load(Ordering::Relaxed),
675            http_responses_4xx: collector.http_responses_4xx.load(Ordering::Relaxed),
676            http_responses_5xx: collector.http_responses_5xx.load(Ordering::Relaxed),
677            cache_hits,
678            cache_misses,
679            #[allow(clippy::cast_precision_loss)] // Reason: precision loss is acceptable for metrics/statistics
680            cache_hit_ratio: if cache_total > 0 {
681                cache_hits as f64 / cache_total as f64
682            } else {
683                0.0
684            },
685        }
686    }
687}