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 SRV-030 atomic series. Interior to the listener; consumers read it
11/// through [`MetricsSnapshot`].
12#[derive(Debug, Default)]
13pub(crate) struct Metrics {
14    connections: AtomicU64,
15    connections_refused_total: AtomicU64,
16    commands_total: AtomicU64,
17    commands_error_total: AtomicU64,
18    command_duration_microseconds_total: AtomicU64,
19    frame_bytes_in_total: AtomicU64,
20    frame_bytes_out_total: AtomicU64,
21    slow_commands_total: AtomicU64,
22    non_hello_first_frames_total: AtomicU64,
23}
24
25impl Metrics {
26    /// Gauge up: one connection accepted.
27    pub(crate) fn connection_opened(&self) {
28        self.connections.fetch_add(1, Ordering::Relaxed);
29    }
30
31    /// One accept refused because the listener was at its connection ceiling
32    /// (`ListenerConfig::max_connections`). Counted so a ceiling that is
33    /// engaging is visible instead of looking like client-side failures.
34    pub(crate) fn connection_refused(&self) {
35        self.connections_refused_total
36            .fetch_add(1, Ordering::Relaxed);
37    }
38
39    /// Gauge down: one connection fully drained and closed.
40    pub(crate) fn connection_closed(&self) {
41        self.connections.fetch_sub(1, Ordering::Relaxed);
42    }
43
44    /// Record one completed command — called by the writer task after the
45    /// response left the socket (SRV-030). A zero `slow_threshold`
46    /// disables the slow counter.
47    pub(crate) fn record_command(
48        &self,
49        in_bytes: usize,
50        out_bytes: usize,
51        duration: Duration,
52        is_error: bool,
53        slow_threshold: Duration,
54    ) {
55        self.commands_total.fetch_add(1, Ordering::Relaxed);
56        if is_error {
57            self.commands_error_total.fetch_add(1, Ordering::Relaxed);
58        }
59        self.command_duration_microseconds_total
60            .fetch_add(duration.as_micros() as u64, Ordering::Relaxed);
61        self.frame_bytes_in_total
62            .fetch_add(in_bytes as u64, Ordering::Relaxed);
63        self.frame_bytes_out_total
64            .fetch_add(out_bytes as u64, Ordering::Relaxed);
65        if !slow_threshold.is_zero() && duration >= slow_threshold {
66            self.slow_commands_total.fetch_add(1, Ordering::Relaxed);
67        }
68    }
69
70    /// Record one push frame (SRV-013): only out-bytes — pushes are not
71    /// commands.
72    pub(crate) fn record_push(&self, out_bytes: usize) {
73        self.frame_bytes_out_total
74            .fetch_add(out_bytes as u64, Ordering::Relaxed);
75    }
76
77    /// Record one connection whose first frame was **not** a canonical
78    /// `HELLO` (SPEC-008 handshake section): the adoption signal a product
79    /// watches while migrating its clients to lead with `HELLO`, before it
80    /// cuts a legacy first-frame path. Cumulative; zero under a profile whose
81    /// clients always lead with `HELLO` (`HelloMandatory`).
82    pub(crate) fn record_non_hello_first_frame(&self) {
83        self.non_hello_first_frames_total
84            .fetch_add(1, Ordering::Relaxed);
85    }
86
87    /// Point-in-time copy of every series.
88    pub(crate) fn snapshot(&self) -> MetricsSnapshot {
89        MetricsSnapshot {
90            connections: self.connections.load(Ordering::Relaxed),
91            connections_refused_total: self.connections_refused_total.load(Ordering::Relaxed),
92            commands_total: self.commands_total.load(Ordering::Relaxed),
93            commands_error_total: self.commands_error_total.load(Ordering::Relaxed),
94            command_duration_microseconds_total: self
95                .command_duration_microseconds_total
96                .load(Ordering::Relaxed),
97            frame_bytes_in_total: self.frame_bytes_in_total.load(Ordering::Relaxed),
98            frame_bytes_out_total: self.frame_bytes_out_total.load(Ordering::Relaxed),
99            slow_commands_total: self.slow_commands_total.load(Ordering::Relaxed),
100            non_hello_first_frames_total: self.non_hello_first_frames_total.load(Ordering::Relaxed),
101        }
102    }
103}
104
105/// One consistent-enough read of the listener's counters (SRV-030),
106/// exporter-agnostic by design.
107#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
108pub struct MetricsSnapshot {
109    /// Currently open connections (gauge).
110    pub connections: u64,
111    /// Accepts refused at the `max_connections` ceiling.
112    pub connections_refused_total: u64,
113    /// Responses written, success or error.
114    pub commands_total: u64,
115    /// Error responses written.
116    pub commands_error_total: u64,
117    /// Total dispatch time across all commands, microseconds.
118    pub command_duration_microseconds_total: u64,
119    /// Request bytes as counted by the decoder's length prefix (SRV-007).
120    pub frame_bytes_in_total: u64,
121    /// Response/push bytes as counted from the encoded buffers (SRV-007).
122    pub frame_bytes_out_total: u64,
123    /// Commands slower than the configured threshold (SRV-030).
124    pub slow_commands_total: u64,
125    /// Connections whose first frame was not a canonical `HELLO` — the
126    /// lead-with-`HELLO` adoption signal (SPEC-008 handshake section).
127    pub non_hello_first_frames_total: u64,
128}