Skip to main content

thunder/server/
observer.rs

1//! Optional per-command metrics observer (SRV-030 extension).
2//!
3//! [`MetricsSnapshot`](crate::server::MetricsSnapshot) gives an exporter
4//! cumulative totals, read whenever it likes. That is enough to graph rates,
5//! and it is not enough for two things a product exporter usually already has:
6//!
7//! - **Per-command dimensions.** `commands_total` is listener-wide, so a
8//!   `{command}` label cannot be recovered from it. Timing inside
9//!   [`Dispatch::dispatch`](crate::server::Dispatch) is not the same
10//!   measurement — that is the dispatch window, while the listener records the
11//!   frame-received-to-frame-sent window, and the two disagree.
12//! - **Distributions.** A histogram of frame sizes cannot be reconstructed from
13//!   a byte total.
14//!
15//! Without a callback the only ingestion path is sampling, which adds a task,
16//! adds staleness up to the sample interval, and cannot see anything that
17//! happened between two ticks.
18//!
19//! So: an optional observer, invoked at exactly the point the built-in metrics
20//! record — **after the successful socket write** — with values the listener
21//! already holds. It is `None` by default and costs nothing when unset; the
22//! command label is not even materialized unless an observer is installed.
23
24use std::time::Duration;
25
26/// Receives one callback per completed command, plus connection lifecycle.
27///
28/// Every method must be cheap and must not block: they run on the connection's
29/// writer task, so time spent here is time the socket is not being written.
30/// Anything expensive belongs behind a channel.
31pub trait MetricsObserver: Send + Sync + 'static {
32    /// One command completed and its response left the socket.
33    ///
34    /// `in_bytes` is the request frame size from the decoder and `out_bytes`
35    /// the encoded response length — neither is ever re-encoded to be measured
36    /// (SRV-007). `duration` is the dispatch time, and `is_error` reflects the
37    /// response carrying `Err`, not a transport failure.
38    fn command_completed(
39        &self,
40        command: &str,
41        in_bytes: usize,
42        out_bytes: usize,
43        duration: Duration,
44        is_error: bool,
45    );
46
47    /// A connection was accepted.
48    fn connection_opened(&self) {}
49
50    /// A connection finished draining and closed.
51    fn connection_closed(&self) {}
52
53    /// An accept was refused at the `max_connections` ceiling.
54    fn connection_refused(&self) {}
55
56    /// A server-initiated frame was written (`id == PUSH_ID`, WIRE-005).
57    fn push_emitted(&self, out_bytes: usize) {
58        let _ = out_bytes;
59    }
60}