rust_web_server/request_id/mod.rs
1//! Request ID / correlation ID middleware.
2//!
3//! Distributed tracing ([`crate::otel`]) creates a span per request but
4//! doesn't give handlers a simple, stable identifier to put in their own log
5//! lines — and doesn't propagate one across service boundaries unless the
6//! caller already sends a W3C `traceparent`. [`RequestIdLayer`] fills that
7//! gap: it's a plain string ID, present on both the request (so your handler
8//! can read and log it) and the response (so the caller can log the same
9//! value), that survives even in builds/setups without OpenTelemetry wired up.
10//!
11//! # Example
12//!
13//! ```rust,no_run
14//! use rust_web_server::app::App;
15//! use rust_web_server::core::New;
16//! use rust_web_server::request_id::RequestIdLayer;
17//!
18//! let app = App::new().wrap(RequestIdLayer::new());
19//! ```
20//!
21//! Reading it in a handler — the header is just a normal request header,
22//! visible through any of the usual ways to read one:
23//!
24//! ```rust,no_run
25//! use rust_web_server::request::Request;
26//! use rust_web_server::request_id::DEFAULT_HEADER;
27//!
28//! fn handler(request: &Request) {
29//! let id = request.get_header(DEFAULT_HEADER.to_string()).map(|h| h.value.as_str()).unwrap_or("");
30//! println!("[{}] handling request", id);
31//! }
32//! ```
33
34#[cfg(test)]
35mod tests;
36
37use std::sync::atomic::{AtomicU64, Ordering};
38use std::time::{SystemTime, UNIX_EPOCH};
39
40use crate::application::Application;
41use crate::header::Header;
42use crate::middleware::Middleware;
43use crate::request::Request;
44use crate::response::Response;
45use crate::server::ConnectionInfo;
46
47/// Default header name used for the request ID, both incoming and outgoing.
48pub const DEFAULT_HEADER: &str = "X-Request-Id";
49
50static ID_COUNTER: AtomicU64 = AtomicU64::new(0);
51
52/// Generates a UUID-v4-*shaped* identifier (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`).
53///
54/// Not cryptographically random and not a spec-compliant UUID (version/variant
55/// bits aren't forced) — built from a monotonic counter mixed with the
56/// current time via a splitmix64 finalizer, the same non-crypto technique
57/// already used elsewhere in this crate for unique-but-not-secret IDs (e.g.
58/// session IDs). Good for correlating log lines across services; do not use
59/// this as a security token, session ID, or anywhere uniqueness must be
60/// adversarially guaranteed.
61pub fn generate_request_id() -> String {
62 let nanos = SystemTime::now()
63 .duration_since(UNIX_EPOCH)
64 .map(|d| d.as_nanos() as u64)
65 .unwrap_or(0);
66 let count = ID_COUNTER.fetch_add(1, Ordering::Relaxed);
67
68 let mut x = nanos ^ count.wrapping_mul(0x9e3779b97f4a7c15);
69 x ^= x >> 30;
70 x = x.wrapping_mul(0xbf58476d1ce4e5b9);
71 x ^= x >> 27;
72 x = x.wrapping_mul(0x94d049bb133111eb);
73 x ^= x >> 31;
74
75 let mut y = count ^ nanos.wrapping_mul(0x517cc1b727220a95);
76 y ^= y >> 30;
77 y = y.wrapping_mul(0xbf58476d1ce4e5b9);
78 y ^= y >> 27;
79 y = y.wrapping_mul(0x94d049bb133111eb);
80 y ^= y >> 31;
81
82 format!(
83 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
84 (x >> 32) as u32,
85 ((x >> 16) & 0xffff) as u16,
86 (x & 0xffff) as u16,
87 ((y >> 48) & 0xffff) as u16,
88 y & 0xffff_ffff_ffff,
89 )
90}
91
92/// Middleware that ensures every request/response pair carries a stable
93/// correlation ID.
94///
95/// - If the incoming request already has the header (e.g. set by an
96/// upstream gateway, load balancer, or calling service), that exact value
97/// is kept and echoed back unchanged — this lets one ID follow a request
98/// across multiple services instead of getting a new one at each hop.
99/// - Otherwise, a fresh ID is generated with [`generate_request_id`] and
100/// injected into the request *before* it reaches your handler, so handlers
101/// can read it like any other header.
102/// - The same value is always set on the response, so the caller can log it
103/// too — even if it arrived with no ID and this middleware generated one.
104pub struct RequestIdLayer {
105 header: String,
106}
107
108impl RequestIdLayer {
109 /// Uses the default header name, [`DEFAULT_HEADER`] (`X-Request-Id`).
110 pub fn new() -> Self {
111 RequestIdLayer { header: DEFAULT_HEADER.to_string() }
112 }
113
114 /// Use a different header name, e.g. `"X-Correlation-Id"`.
115 pub fn header(mut self, name: impl Into<String>) -> Self {
116 self.header = name.into();
117 self
118 }
119}
120
121impl Default for RequestIdLayer {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl Middleware for RequestIdLayer {
128 fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
129 let existing = request.get_header(self.header.clone()).map(|h| h.value.clone());
130
131 let (id, mut response) = match existing {
132 Some(id) => (id, next.execute(request, connection)?),
133 None => {
134 let id = generate_request_id();
135 let mut req = request.clone();
136 req.headers.push(Header { name: self.header.clone(), value: id.clone() });
137 (id, next.execute(&req, connection)?)
138 }
139 };
140
141 response.headers.push(Header { name: self.header.clone(), value: id });
142 Ok(response)
143 }
144}