Skip to main content

sova_core/
request_id.rs

1//! Per-request correlation id (`x-request-id`).
2
3use crate::middleware::{named, MwEntry, Next};
4use crate::request::Request;
5use tracing::Instrument;
6
7/// Per-request correlation id (inbound `x-request-id` or generated).
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct RequestId(pub String);
10
11impl std::fmt::Display for RequestId {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        self.0.fmt(f)
14    }
15}
16
17impl AsRef<str> for RequestId {
18    fn as_ref(&self) -> &str {
19        &self.0
20    }
21}
22
23/// Ensure [`RequestId`], echo `x-request-id`, wrap in an `http.server` span.
24pub fn request_id() -> MwEntry {
25    named("request-id", |mut req: Request, next: Next| async move {
26        ensure_request_id(&mut req);
27        let id = req
28            .get::<RequestId>()
29            .map(|r| r.0.clone())
30            .unwrap_or_default();
31        let method = req.method.as_str().to_string();
32        let path = req.path.clone();
33        let span = tracing::info_span!(
34            "http.server",
35            request_id = %id,
36            method = %method,
37            path = %path,
38            otel.kind = "server",
39        );
40        async move {
41            let mut res = next(req).await;
42            if !id.is_empty() {
43                res = res.header("x-request-id", &id);
44            }
45            res
46        }
47        .instrument(span)
48        .await
49    })
50}
51
52/// Set [`RequestId`] from `x-request-id` or generate one (idempotent).
53pub fn ensure_request_id(req: &mut Request) {
54    if req.get::<RequestId>().is_some() {
55        return;
56    }
57    let id = req
58        .header("x-request-id")
59        .filter(|s| !s.is_empty())
60        .map(str::to_owned)
61        .unwrap_or_else(generate_request_id);
62    req.set(RequestId(id));
63}
64
65fn generate_request_id() -> String {
66    use std::sync::atomic::{AtomicU64, Ordering};
67    static COUNTER: AtomicU64 = AtomicU64::new(1);
68    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
69    let mut bytes = [0u8; 8];
70    let _ = getrandom::getrandom(&mut bytes);
71    let entropy = u64::from_le_bytes(bytes);
72    format!("req-{entropy:016x}-{n:x}")
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::middleware::build_chain;
79    use crate::response::Response;
80    use http::Method;
81    use std::sync::Arc;
82
83    #[tokio::test]
84    async fn echoes_and_reuses_inbound() {
85        let leaf: crate::handler::Handler = Arc::new(|req: Request| {
86            Box::pin(async move {
87                let id = req.get::<RequestId>().unwrap().0.clone();
88                Response::text(id)
89            })
90        });
91        let chain = build_chain(&[request_id().mw], leaf);
92        let mut req = Request::new(Method::GET, "/");
93        req.headers
94            .insert("x-request-id", "abc-123".parse().unwrap());
95        let res = chain(req).await;
96        assert_eq!(res.body_bytes(), Some(b"abc-123".as_slice()));
97        assert_eq!(
98            res.headers
99                .get("x-request-id")
100                .and_then(|v| v.to_str().ok()),
101            Some("abc-123")
102        );
103    }
104
105    #[tokio::test]
106    async fn generates_when_missing() {
107        let leaf: crate::handler::Handler = Arc::new(|req: Request| {
108            Box::pin(async move {
109                let id = req.get::<RequestId>().unwrap().0.clone();
110                Response::text(id)
111            })
112        });
113        let chain = build_chain(&[request_id().mw.clone()], leaf);
114        let res = chain(Request::new(Method::GET, "/")).await;
115        let body = String::from_utf8(res.body_bytes().unwrap().to_vec()).unwrap();
116        assert!(body.starts_with("req-"), "{body}");
117        assert_eq!(
118            res.headers
119                .get("x-request-id")
120                .and_then(|v| v.to_str().ok()),
121            Some(body.as_str())
122        );
123    }
124}