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