Skip to main content

thunder/server/
metrics.rs

1//! Server metrics as plain atomics (SRV-030) — snapshot-friendly for any
2//! exporter, no metrics-framework dependency. Every series records **after**
3//! a successful socket write, per the writer contract; byte counts
4//! come from the decoder's frame size (in) and the single encoded response
5//! buffer (out) — nothing is ever re-encoded to be measured (SRV-007).
6
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::time::Duration;
9
10/// The eight atomic series of SRV-030. Interior to the listener; consumers
11/// read it through [`MetricsSnapshot`].
12#[derive(Debug, Default)]
13pub(crate) struct Metrics {
14    connections: AtomicU64,
15    commands_total: AtomicU64,
16    commands_error_total: AtomicU64,
17    command_duration_microseconds_total: AtomicU64,
18    frame_bytes_in_total: AtomicU64,
19    frame_bytes_out_total: AtomicU64,
20    slow_commands_total: AtomicU64,
21    non_hello_first_frames_total: AtomicU64,
22}
23
24impl Metrics {
25    /// Gauge up: one connection accepted.
26    pub(crate) fn connection_opened(&self) {
27        self.connections.fetch_add(1, Ordering::Relaxed);
28    }
29
30    /// Gauge down: one connection fully drained and closed.
31    pub(crate) fn connection_closed(&self) {
32        self.connections.fetch_sub(1, Ordering::Relaxed);
33    }
34
35    /// Record one completed command — called by the writer task after the
36    /// response left the socket (SRV-030). A zero `slow_threshold`
37    /// disables the slow counter.
38    pub(crate) fn record_command(
39        &self,
40        in_bytes: usize,
41        out_bytes: usize,
42        duration: Duration,
43        is_error: bool,
44        slow_threshold: Duration,
45    ) {
46        self.commands_total.fetch_add(1, Ordering::Relaxed);
47        if is_error {
48            self.commands_error_total.fetch_add(1, Ordering::Relaxed);
49        }
50        self.command_duration_microseconds_total
51            .fetch_add(duration.as_micros() as u64, Ordering::Relaxed);
52        self.frame_bytes_in_total
53            .fetch_add(in_bytes as u64, Ordering::Relaxed);
54        self.frame_bytes_out_total
55            .fetch_add(out_bytes as u64, Ordering::Relaxed);
56        if !slow_threshold.is_zero() && duration >= slow_threshold {
57            self.slow_commands_total.fetch_add(1, Ordering::Relaxed);
58        }
59    }
60
61    /// Record one push frame (SRV-013): only out-bytes — pushes are not
62    /// commands.
63    pub(crate) fn record_push(&self, out_bytes: usize) {
64        self.frame_bytes_out_total
65            .fetch_add(out_bytes as u64, Ordering::Relaxed);
66    }
67
68    /// Record one connection whose first frame was **not** a canonical
69    /// `HELLO` (SPEC-008 handshake section): the adoption signal a product
70    /// watches while migrating its clients to lead with `HELLO`, before it
71    /// cuts a legacy first-frame path. Cumulative; zero under a profile whose
72    /// clients always lead with `HELLO` (`HelloMandatory`).
73    pub(crate) fn record_non_hello_first_frame(&self) {
74        self.non_hello_first_frames_total
75            .fetch_add(1, Ordering::Relaxed);
76    }
77
78    /// Point-in-time copy of every series.
79    pub(crate) fn snapshot(&self) -> MetricsSnapshot {
80        MetricsSnapshot {
81            connections: self.connections.load(Ordering::Relaxed),
82            commands_total: self.commands_total.load(Ordering::Relaxed),
83            commands_error_total: self.commands_error_total.load(Ordering::Relaxed),
84            command_duration_microseconds_total: self
85                .command_duration_microseconds_total
86                .load(Ordering::Relaxed),
87            frame_bytes_in_total: self.frame_bytes_in_total.load(Ordering::Relaxed),
88            frame_bytes_out_total: self.frame_bytes_out_total.load(Ordering::Relaxed),
89            slow_commands_total: self.slow_commands_total.load(Ordering::Relaxed),
90            non_hello_first_frames_total: self.non_hello_first_frames_total.load(Ordering::Relaxed),
91        }
92    }
93}
94
95/// One consistent-enough read of the listener's counters (SRV-030),
96/// exporter-agnostic by design.
97#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
98pub struct MetricsSnapshot {
99    /// Currently open connections (gauge).
100    pub connections: u64,
101    /// Responses written, success or error.
102    pub commands_total: u64,
103    /// Error responses written.
104    pub commands_error_total: u64,
105    /// Total dispatch time across all commands, microseconds.
106    pub command_duration_microseconds_total: u64,
107    /// Request bytes as counted by the decoder's length prefix (SRV-007).
108    pub frame_bytes_in_total: u64,
109    /// Response/push bytes as counted from the encoded buffers (SRV-007).
110    pub frame_bytes_out_total: u64,
111    /// Commands slower than the configured threshold (SRV-030).
112    pub slow_commands_total: u64,
113    /// Connections whose first frame was not a canonical `HELLO` — the
114    /// lead-with-`HELLO` adoption signal (SPEC-008 handshake section).
115    pub non_hello_first_frames_total: u64,
116}