Skip to main content

phantom_protocol/observability/
attrs.rs

1//! Typed attribute values for OpenTelemetry instruments.
2//!
3//! Every dimension the library slices a metric by is a small `Copy` enum
4//! here with a `const fn as_str()` returning a `&'static str`. Recording
5//! sites pass the enum; the instrument layer builds the `KeyValue` list.
6//!
7//! Why typed enums rather than free-form strings: the enum *is* the
8//! cardinality contract. A metric can only be labeled by a value that
9//! exists as an enum variant, so the unbounded-cardinality offenders
10//! (`peer_ip`, `session_id`, `stream_id`) simply cannot be passed — there
11//! is no enum that admits them. See `docs/observability/refactor-plan.md`
12//! §4 "Cardinality contract".
13//!
14//! Performance: the labeled instruments these feed (`record_handshake`,
15//! `record_replay_rejected`, …) are all cold / low-frequency event paths,
16//! so the `KeyValue` list is built per-call. The genuinely hot path
17//! (per-packet counts) does NOT go through here — it uses lock-free
18//! atomics drained by `ObservableCounter` callbacks (`bridge.rs`), which
19//! build their `KeyValue`s once per SDK collection cycle, not per packet.
20//! Per-call attribute construction on the cold paths is not worth
21//! interning away.
22//!
23//! `as_str()` is `const` and the enums are feature-independent, so this
24//! module compiles identically with or without `telemetry-otel` — call
25//! sites need no `#[cfg]` guards.
26
27use crate::transport::types::LegType;
28
29/// Direction of an I/O operation.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum Direction {
32    Send,
33    Recv,
34}
35
36impl Direction {
37    pub const fn as_str(self) -> &'static str {
38        match self {
39            Self::Send => "send",
40            Self::Recv => "recv",
41        }
42    }
43}
44
45/// Stable string label for a `LegType`, used as an OTel attribute value.
46///
47/// These strings are part of the metric cardinality contract and must stay
48/// stable across releases. `"kcp"` and `"faketls"` are retained label values
49/// for legs that no longer exist as transports (the KCP leg and the FakeTLS
50/// leg were removed in the PhantomUDP rewrite); they appear in the contract
51/// only so the instrument set is wire-format-stable. The live production
52/// transport reports as `"udp"` (PhantomUDP); `"tcp"` covers
53/// `TcpSessionTransport`.
54pub fn leg_str(leg: LegType) -> &'static str {
55    match leg {
56        LegType::Kcp => "kcp",
57        LegType::Tcp => "tcp",
58        LegType::FakeTls => "faketls",
59        LegType::Udp => "udp",
60    }
61}
62
63/// Outcome of a handshake attempt.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum HandshakeOutcome {
66    Success,
67    Failure,
68}
69
70impl HandshakeOutcome {
71    pub const fn as_str(self) -> &'static str {
72        match self {
73            Self::Success => "success",
74            Self::Failure => "failure",
75        }
76    }
77}
78
79/// Wire-protocol version label for handshake metrics. Pinned — the protocol
80/// is not negotiated (one wire version), so this is always `Current`. The
81/// variant is kept (rather than dropping the metric attribute) so dashboards
82/// retain a stable `version` dimension across a future, deliberate bump.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84pub enum ProtocolVersion {
85    /// The sole, pinned wire protocol.
86    Current,
87}
88
89impl ProtocolVersion {
90    pub const fn as_str(self) -> &'static str {
91        match self {
92            Self::Current => "v1",
93        }
94    }
95}
96
97/// AEAD algorithm used at the record-protection layer.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub enum AeadAlgorithm {
100    Aes256Gcm,
101    ChaCha20Poly1305,
102}
103
104impl AeadAlgorithm {
105    pub const fn as_str(self) -> &'static str {
106        match self {
107            Self::Aes256Gcm => "aes-256-gcm",
108            Self::ChaCha20Poly1305 => "chacha20-poly1305",
109        }
110    }
111}
112
113/// Reason a replay-rejected packet was dropped.
114///
115/// Keyed on the per-direction `u64` packet number checked by the single
116/// `ReplayWindow` after a successful AEAD open (see `security::replay_window`).
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub enum ReplayReason {
119    /// Packet number falls below the window's lower edge.
120    Old,
121    /// Packet number inside the window but already marked seen.
122    Duplicate,
123}
124
125impl ReplayReason {
126    pub const fn as_str(self) -> &'static str {
127        match self {
128            Self::Old => "old",
129            Self::Duplicate => "duplicate",
130        }
131    }
132}
133
134/// Outcome of a stateless-cookie validation.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136pub enum CookieOutcome {
137    Issued,
138    ValidatedOk,
139    ValidatedMismatch,
140}
141
142impl CookieOutcome {
143    pub const fn as_str(self) -> &'static str {
144        match self {
145            Self::Issued => "issued",
146            Self::ValidatedOk => "validated_ok",
147            Self::ValidatedMismatch => "validated_mismatch",
148        }
149    }
150}
151
152/// Outcome of a proof-of-work challenge.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
154pub enum PowOutcome {
155    Solved,
156    Rejected,
157}
158
159impl PowOutcome {
160    pub const fn as_str(self) -> &'static str {
161        match self {
162            Self::Solved => "solved",
163            Self::Rejected => "rejected",
164        }
165    }
166}
167
168/// 0-RTT early-data outcome.
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
170pub enum EarlyDataOutcome {
171    Accepted,
172    RejectedUnknownTicket,
173    RejectedOversized,
174    RejectedAead,
175    RejectedReplay,
176}
177
178impl EarlyDataOutcome {
179    pub const fn as_str(self) -> &'static str {
180        match self {
181            Self::Accepted => "accepted",
182            Self::RejectedUnknownTicket => "rejected_unknown_ticket",
183            Self::RejectedOversized => "rejected_oversized",
184            Self::RejectedAead => "rejected_aead",
185            Self::RejectedReplay => "rejected_replay",
186        }
187    }
188}
189
190/// Resumption mode for the handshake counter.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192pub enum ResumptionMode {
193    OneRtt,
194    ZeroRtt,
195}
196
197impl ResumptionMode {
198    pub const fn as_str(self) -> &'static str {
199        match self {
200            Self::OneRtt => "1rtt",
201            Self::ZeroRtt => "0rtt",
202        }
203    }
204}
205
206/// Outcome of a `PATH_VALIDATION` exchange.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208pub enum PathValidationOutcome {
209    Success,
210    Failure,
211}
212
213impl PathValidationOutcome {
214    pub const fn as_str(self) -> &'static str {
215        match self {
216            Self::Success => "success",
217            Self::Failure => "failure",
218        }
219    }
220}
221
222/// Reason a transport fallback (switch from one leg to another) was
223/// triggered. Defined as a stable telemetry dimension; the library does no
224/// multipath aggregation (single-path connection migration only), so this is
225/// currently an unwired attribute surface awaiting a fallback-driving caller.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
227pub enum FallbackReason {
228    LossThreshold,
229    RttThreshold,
230    PathFailure,
231    Explicit,
232}
233
234impl FallbackReason {
235    pub const fn as_str(self) -> &'static str {
236        match self {
237            Self::LossThreshold => "loss_threshold",
238            Self::RttThreshold => "rtt_threshold",
239            Self::PathFailure => "path_failure",
240            Self::Explicit => "explicit",
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn direction_strings_are_stable() {
251        assert_eq!(Direction::Send.as_str(), "send");
252        assert_eq!(Direction::Recv.as_str(), "recv");
253    }
254
255    #[test]
256    fn leg_str_covers_all_variants() {
257        assert_eq!(leg_str(LegType::Kcp), "kcp");
258        assert_eq!(leg_str(LegType::Tcp), "tcp");
259        assert_eq!(leg_str(LegType::FakeTls), "faketls");
260        assert_eq!(leg_str(LegType::Udp), "udp");
261    }
262
263    #[test]
264    fn protocol_version_strings() {
265        assert_eq!(ProtocolVersion::Current.as_str(), "v1");
266    }
267}