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}