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/// String labels for `LegType`. Stable strings used as OTel attribute
46/// values; never user-facing.
47pub fn leg_str(leg: LegType) -> &'static str {
48    match leg {
49        LegType::Kcp => "kcp",
50        LegType::Tcp => "tcp",
51        LegType::FakeTls => "faketls",
52    }
53}
54
55/// Outcome of a handshake attempt.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum HandshakeOutcome {
58    Success,
59    Failure,
60}
61
62impl HandshakeOutcome {
63    pub const fn as_str(self) -> &'static str {
64        match self {
65            Self::Success => "success",
66            Self::Failure => "failure",
67        }
68    }
69}
70
71/// Wire-protocol version label for handshake metrics. Pinned — the protocol
72/// is not negotiated (one wire version), so this is always `Current`. The
73/// variant is kept (rather than dropping the metric attribute) so dashboards
74/// retain a stable `version` dimension across a future, deliberate bump.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub enum ProtocolVersion {
77    /// The sole, pinned wire protocol.
78    Current,
79}
80
81impl ProtocolVersion {
82    pub const fn as_str(self) -> &'static str {
83        match self {
84            Self::Current => "v1",
85        }
86    }
87}
88
89/// AEAD algorithm used at the record-protection layer.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub enum AeadAlgorithm {
92    Aes256Gcm,
93    ChaCha20Poly1305,
94}
95
96impl AeadAlgorithm {
97    pub const fn as_str(self) -> &'static str {
98        match self {
99            Self::Aes256Gcm => "aes-256-gcm",
100            Self::ChaCha20Poly1305 => "chacha20-poly1305",
101        }
102    }
103}
104
105/// Reason a replay-rejected packet was dropped.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum ReplayReason {
108    /// Sequence number falls below the window's lower edge.
109    Old,
110    /// Sequence number inside the window but already marked seen.
111    Duplicate,
112}
113
114impl ReplayReason {
115    pub const fn as_str(self) -> &'static str {
116        match self {
117            Self::Old => "old",
118            Self::Duplicate => "duplicate",
119        }
120    }
121}
122
123/// Outcome of a stateless-cookie validation.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub enum CookieOutcome {
126    Issued,
127    ValidatedOk,
128    ValidatedMismatch,
129}
130
131impl CookieOutcome {
132    pub const fn as_str(self) -> &'static str {
133        match self {
134            Self::Issued => "issued",
135            Self::ValidatedOk => "validated_ok",
136            Self::ValidatedMismatch => "validated_mismatch",
137        }
138    }
139}
140
141/// Outcome of a proof-of-work challenge.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143pub enum PowOutcome {
144    Solved,
145    Rejected,
146}
147
148impl PowOutcome {
149    pub const fn as_str(self) -> &'static str {
150        match self {
151            Self::Solved => "solved",
152            Self::Rejected => "rejected",
153        }
154    }
155}
156
157/// 0-RTT early-data outcome.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
159pub enum EarlyDataOutcome {
160    Accepted,
161    RejectedUnknownTicket,
162    RejectedOversized,
163    RejectedAead,
164    RejectedReplay,
165}
166
167impl EarlyDataOutcome {
168    pub const fn as_str(self) -> &'static str {
169        match self {
170            Self::Accepted => "accepted",
171            Self::RejectedUnknownTicket => "rejected_unknown_ticket",
172            Self::RejectedOversized => "rejected_oversized",
173            Self::RejectedAead => "rejected_aead",
174            Self::RejectedReplay => "rejected_replay",
175        }
176    }
177}
178
179/// Resumption mode for the handshake counter.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
181pub enum ResumptionMode {
182    OneRtt,
183    ZeroRtt,
184}
185
186impl ResumptionMode {
187    pub const fn as_str(self) -> &'static str {
188        match self {
189            Self::OneRtt => "1rtt",
190            Self::ZeroRtt => "0rtt",
191        }
192    }
193}
194
195/// Outcome of a `PATH_VALIDATION` exchange.
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
197pub enum PathValidationOutcome {
198    Success,
199    Failure,
200}
201
202impl PathValidationOutcome {
203    pub const fn as_str(self) -> &'static str {
204        match self {
205            Self::Success => "success",
206            Self::Failure => "failure",
207        }
208    }
209}
210
211/// Reason a multi-path fallback was triggered.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
213pub enum FallbackReason {
214    LossThreshold,
215    RttThreshold,
216    PathFailure,
217    Explicit,
218}
219
220impl FallbackReason {
221    pub const fn as_str(self) -> &'static str {
222        match self {
223            Self::LossThreshold => "loss_threshold",
224            Self::RttThreshold => "rtt_threshold",
225            Self::PathFailure => "path_failure",
226            Self::Explicit => "explicit",
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn direction_strings_are_stable() {
237        assert_eq!(Direction::Send.as_str(), "send");
238        assert_eq!(Direction::Recv.as_str(), "recv");
239    }
240
241    #[test]
242    fn leg_str_covers_all_variants() {
243        assert_eq!(leg_str(LegType::Kcp), "kcp");
244        assert_eq!(leg_str(LegType::Tcp), "tcp");
245        assert_eq!(leg_str(LegType::FakeTls), "faketls");
246    }
247
248    #[test]
249    fn protocol_version_strings() {
250        assert_eq!(ProtocolVersion::Current.as_str(), "v1");
251    }
252}