dig_rpc/middleware/request_id.rs
1//! Attach a UUID v7 (time-ordered) to every request.
2//!
3//! The request id is propagated through tracing spans and the audit log
4//! so a single request can be traced end-to-end. It is also returned as
5//! the `x-request-id` response header for client-side correlation.
6
7use uuid::Uuid;
8
9/// An opaque per-request identifier.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct RequestId(pub Uuid);
12
13impl RequestId {
14 /// Fresh UUID v7 (time-ordered).
15 pub fn new() -> Self {
16 Self(Uuid::now_v7())
17 }
18
19 /// Hex string representation.
20 pub fn to_string_hex(&self) -> String {
21 self.0.simple().to_string()
22 }
23}
24
25impl Default for RequestId {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl std::fmt::Display for RequestId {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 write!(f, "{}", self.to_string_hex())
34 }
35}
36
37/// Zero-sized marker; actual Tower layer integration happens in
38/// [`crate::server::RpcServer::build_router`] where we attach the id to
39/// request extensions before the other layers fire.
40#[derive(Debug, Default, Clone, Copy)]
41pub struct RequestIdLayer;
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46
47 /// **Proves:** two freshly-generated `RequestId`s are distinct (with
48 /// overwhelming probability).
49 ///
50 /// **Why it matters:** The whole point of a request id is uniqueness.
51 /// UUIDv7 gives us both ordering and uniqueness; a regression to a
52 /// constant / counter would break correlation across restarts.
53 ///
54 /// **Catches:** accidental `Uuid::nil()` or `Uuid::from_u128(0)`.
55 #[test]
56 fn requests_are_unique() {
57 let a = RequestId::new();
58 let b = RequestId::new();
59 assert_ne!(a, b);
60 }
61
62 /// **Proves:** `to_string_hex` produces a 32-char lowercase hex
63 /// representation (UUID simple form, no hyphens).
64 ///
65 /// **Why it matters:** Dashboards / log aggregators often index on this
66 /// exact form. Any change to hyphenated / uppercase would break those
67 /// queries.
68 ///
69 /// **Catches:** a regression to `Uuid::to_string()` (hyphenated).
70 #[test]
71 fn to_string_hex_is_simple_form() {
72 let r = RequestId::new();
73 let s = r.to_string_hex();
74 assert_eq!(s.len(), 32);
75 assert!(s
76 .chars()
77 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
78 }
79}