Skip to main content

phantom_protocol/observability/
snapshot.rs

1//! Cold-path snapshot of the observability state.
2//!
3//! Reads every hot-path atomic with `Ordering::Relaxed` and exposes the
4//! values in a `Clone`-able plain struct suitable for FFI, logging, and
5//! debugging. Per-leg breakdown is preserved so consumers can compute their
6//! own slices.
7//!
8//! In step 3 of the rollout this struct does NOT carry the security signal
9//! counters or histogram (those are added with the OTel instruments in
10//! later steps). It provides at least the same totals as the legacy
11//! `transport::metrics::MetricsSnapshot` so call sites can migrate.
12
13use crate::observability::atomics::{HotPathAtomics, DIR_RECV, DIR_SEND};
14use crate::transport::types::LegType;
15
16/// Immutable cold-path snapshot of the hot-path atomics.
17#[derive(Debug, Clone)]
18pub struct MetricsSnapshot {
19    pub packets_sent: u64,
20    pub packets_recv: u64,
21    pub bytes_sent: u64,
22    pub bytes_recv: u64,
23
24    /// Per-leg packet counts: `(LegType, packets_sent, packets_recv)`.
25    pub per_leg_packets: [(LegType, u64, u64); 3],
26    /// Per-leg byte counts: `(LegType, bytes_sent, bytes_recv)`.
27    pub per_leg_bytes: [(LegType, u64, u64); 3],
28
29    pub avg_encrypt_ns: u64,
30    pub avg_decrypt_ns: u64,
31    pub encrypt_count: u64,
32    pub decrypt_count: u64,
33
34    pub rtt_us_path_0: u64,
35
36    pub active_sessions: i64,
37    pub active_streams: i64,
38
39    pub handshakes_success: u64,
40    pub handshakes_failure: u64,
41    pub handshake_latency_ns_sum: u64,
42    pub handshake_latency_count: u64,
43
44    pub uptime_secs: u64,
45}
46
47impl Default for MetricsSnapshot {
48    fn default() -> Self {
49        Self {
50            packets_sent: 0,
51            packets_recv: 0,
52            bytes_sent: 0,
53            bytes_recv: 0,
54            per_leg_packets: [
55                (LegType::Kcp, 0, 0),
56                (LegType::Tcp, 0, 0),
57                (LegType::FakeTls, 0, 0),
58            ],
59            per_leg_bytes: [
60                (LegType::Kcp, 0, 0),
61                (LegType::Tcp, 0, 0),
62                (LegType::FakeTls, 0, 0),
63            ],
64            avg_encrypt_ns: 0,
65            avg_decrypt_ns: 0,
66            encrypt_count: 0,
67            decrypt_count: 0,
68            rtt_us_path_0: 0,
69            active_sessions: 0,
70            active_streams: 0,
71            handshakes_success: 0,
72            handshakes_failure: 0,
73            handshake_latency_ns_sum: 0,
74            handshake_latency_count: 0,
75            uptime_secs: 0,
76        }
77    }
78}
79
80impl MetricsSnapshot {
81    pub(crate) fn capture(h: &HotPathAtomics) -> Self {
82        let avg_encrypt_ns = avg(h.encrypt_sum_ns(), h.encrypt_count());
83        let avg_decrypt_ns = avg(h.decrypt_sum_ns(), h.decrypt_count());
84
85        let per_leg_packets = [
86            (
87                LegType::Kcp,
88                h.packets_per_leg(DIR_SEND, LegType::Kcp),
89                h.packets_per_leg(DIR_RECV, LegType::Kcp),
90            ),
91            (
92                LegType::Tcp,
93                h.packets_per_leg(DIR_SEND, LegType::Tcp),
94                h.packets_per_leg(DIR_RECV, LegType::Tcp),
95            ),
96            (
97                LegType::FakeTls,
98                h.packets_per_leg(DIR_SEND, LegType::FakeTls),
99                h.packets_per_leg(DIR_RECV, LegType::FakeTls),
100            ),
101        ];
102        let per_leg_bytes = [
103            (
104                LegType::Kcp,
105                h.bytes_per_leg(DIR_SEND, LegType::Kcp),
106                h.bytes_per_leg(DIR_RECV, LegType::Kcp),
107            ),
108            (
109                LegType::Tcp,
110                h.bytes_per_leg(DIR_SEND, LegType::Tcp),
111                h.bytes_per_leg(DIR_RECV, LegType::Tcp),
112            ),
113            (
114                LegType::FakeTls,
115                h.bytes_per_leg(DIR_SEND, LegType::FakeTls),
116                h.bytes_per_leg(DIR_RECV, LegType::FakeTls),
117            ),
118        ];
119
120        Self {
121            packets_sent: h.packets_total(DIR_SEND),
122            packets_recv: h.packets_total(DIR_RECV),
123            bytes_sent: h.bytes_total(DIR_SEND),
124            bytes_recv: h.bytes_total(DIR_RECV),
125            per_leg_packets,
126            per_leg_bytes,
127            avg_encrypt_ns,
128            avg_decrypt_ns,
129            encrypt_count: h.encrypt_count(),
130            decrypt_count: h.decrypt_count(),
131            rtt_us_path_0: h.rtt_us(0),
132            active_sessions: h.active_sessions(),
133            active_streams: h.active_streams(),
134            handshakes_success: h.handshake_success_count(),
135            handshakes_failure: h.handshake_failure_count(),
136            handshake_latency_ns_sum: h.handshake_latency_ns_sum(),
137            handshake_latency_count: h.handshake_latency_count(),
138            uptime_secs: h.uptime_secs(),
139        }
140    }
141}
142
143fn avg(sum: u64, count: u64) -> u64 {
144    sum.checked_div(count).unwrap_or(0)
145}
146
147impl std::fmt::Display for MetricsSnapshot {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        write!(
150            f,
151            "tx={} rx={} bytes_tx={} bytes_rx={} encrypt={}ns decrypt={}ns sessions={} streams={} up={}s",
152            self.packets_sent,
153            self.packets_recv,
154            self.bytes_sent,
155            self.bytes_recv,
156            self.avg_encrypt_ns,
157            self.avg_decrypt_ns,
158            self.active_sessions,
159            self.active_streams,
160            self.uptime_secs,
161        )
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::observability::atomics::HotPathAtomics;
169
170    #[test]
171    fn snapshot_zero_state() {
172        let h = HotPathAtomics::new();
173        let s = MetricsSnapshot::capture(&h);
174        assert_eq!(s.packets_sent, 0);
175        assert_eq!(s.avg_encrypt_ns, 0);
176        assert_eq!(s.active_sessions, 0);
177    }
178
179    #[test]
180    fn snapshot_after_recording() {
181        let h = HotPathAtomics::new();
182        h.record_send(1024, LegType::Tcp);
183        h.record_send(2048, LegType::Kcp);
184        h.record_recv(512, LegType::Tcp);
185        h.record_encrypt_ns(100);
186        h.record_encrypt_ns(200);
187        h.session_opened();
188        h.stream_opened();
189
190        let s = MetricsSnapshot::capture(&h);
191        assert_eq!(s.packets_sent, 2);
192        assert_eq!(s.packets_recv, 1);
193        assert_eq!(s.bytes_sent, 3072);
194        assert_eq!(s.avg_encrypt_ns, 150);
195        assert_eq!(s.encrypt_count, 2);
196        assert_eq!(s.active_sessions, 1);
197        assert_eq!(s.active_streams, 1);
198    }
199
200    #[test]
201    fn display_is_one_line() {
202        let h = HotPathAtomics::new();
203        let s = MetricsSnapshot::capture(&h);
204        let text = format!("{}", s);
205        assert!(!text.contains('\n'));
206        assert!(text.contains("tx=0"));
207    }
208}