tower_http_cache/
request_id.rs

1//! Request ID infrastructure for request correlation and tracing.
2//!
3//! This module provides a `RequestId` type for tracking requests across
4//! the caching layer and downstream services. Request IDs can be extracted
5//! from headers (e.g., X-Request-ID) or generated automatically.
6
7use http::{HeaderValue, Request};
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11use uuid::Uuid;
12
13/// Unique identifier for tracking a request through the system.
14///
15/// Request IDs enable correlation of logs, metrics, and traces across
16/// different components and services. They can be extracted from incoming
17/// headers or generated automatically.
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
19pub struct RequestId(String);
20
21impl RequestId {
22    /// Generates a new random request ID using UUID v4.
23    pub fn new() -> Self {
24        Self(Uuid::new_v4().to_string())
25    }
26
27    /// Creates a request ID from a string value.
28    ///
29    /// This is useful when extracting request IDs from headers or
30    /// other sources. For a fallible variant, use `RequestId::from_str()` from the `FromStr` trait.
31    pub fn from_string(s: impl Into<String>) -> Self {
32        Self(s.into())
33    }
34
35    /// Attempts to extract a request ID from an HTTP header.
36    ///
37    /// Returns `None` if the header value is not valid UTF-8.
38    pub fn from_header(header: &HeaderValue) -> Option<Self> {
39        header.to_str().ok().map(|s| Self(s.to_owned()))
40    }
41
42    /// Extracts a request ID from the request headers, or generates a new one.
43    ///
44    /// Looks for the `X-Request-ID` header first. If not present or invalid,
45    /// generates a new random ID.
46    pub fn from_request<B>(req: &Request<B>) -> Self {
47        req.headers()
48            .get("x-request-id")
49            .and_then(Self::from_header)
50            .unwrap_or_default()
51    }
52
53    /// Returns the request ID as a string slice.
54    pub fn as_str(&self) -> &str {
55        &self.0
56    }
57
58    /// Consumes the request ID and returns the inner string.
59    pub fn into_string(self) -> String {
60        self.0
61    }
62}
63
64impl Default for RequestId {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl fmt::Display for RequestId {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "{}", self.0)
73    }
74}
75
76impl From<String> for RequestId {
77    fn from(s: String) -> Self {
78        Self(s)
79    }
80}
81
82impl From<RequestId> for String {
83    fn from(id: RequestId) -> Self {
84        id.0
85    }
86}
87
88impl FromStr for RequestId {
89    type Err = std::convert::Infallible;
90
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        Ok(Self(s.to_owned()))
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use http::Request;
100
101    #[test]
102    fn new_generates_valid_uuid() {
103        let id1 = RequestId::new();
104        let id2 = RequestId::new();
105        assert_ne!(id1, id2);
106        assert!(Uuid::parse_str(id1.as_str()).is_ok());
107    }
108
109    #[test]
110    fn from_str_creates_request_id() {
111        let id = "custom-id-123".parse::<RequestId>().unwrap();
112        assert_eq!(id.as_str(), "custom-id-123");
113    }
114
115    #[test]
116    fn from_header_extracts_valid_value() {
117        let header = HeaderValue::from_static("test-request-id");
118        let id = RequestId::from_header(&header).unwrap();
119        assert_eq!(id.as_str(), "test-request-id");
120    }
121
122    #[test]
123    fn from_request_extracts_header() {
124        let mut req = Request::builder().body(()).unwrap();
125        req.headers_mut()
126            .insert("x-request-id", HeaderValue::from_static("header-id"));
127
128        let id = RequestId::from_request(&req);
129        assert_eq!(id.as_str(), "header-id");
130    }
131
132    #[test]
133    fn from_request_generates_when_missing() {
134        let req = Request::builder().body(()).unwrap();
135        let id = RequestId::from_request(&req);
136        assert!(Uuid::parse_str(id.as_str()).is_ok());
137    }
138
139    #[test]
140    fn display_implementation() {
141        let id = "test-id".parse::<RequestId>().unwrap();
142        assert_eq!(format!("{}", id), "test-id");
143    }
144
145    #[test]
146    fn string_conversions() {
147        let original = "test-id".to_string();
148        let id = RequestId::from(original.clone());
149        let converted: String = id.into();
150        assert_eq!(original, converted);
151    }
152}