-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcorrelation.rs
More file actions
93 lines (76 loc) · 2.79 KB
/
Copy pathcorrelation.rs
File metadata and controls
93 lines (76 loc) · 2.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
use axum::{
extract::Request,
http::HeaderValue,
middleware::Next,
response::Response,
};
use uuid::Uuid;
pub const REQUEST_ID_HEADER: &str = "x-request-id";
/// Maximum allowed header length for the correlation/correlation ID.
///
/// UUIDs in canonical string form are 36 bytes (e.g. `550e8400-e29b-41d4-a716-446655440000`).
pub const REQUEST_ID_MAX_LEN: usize = 64;
fn parse_valid_request_id(header_value: &str) -> Option<String> {
if header_value.len() > REQUEST_ID_MAX_LEN {
return None;
}
// Validate as UUID v4.
let uuid = Uuid::parse_str(header_value).ok()?;
if uuid.get_version_num() != 4 {
return None;
}
Some(uuid.to_string())
}
/// Middleware that attaches a correlation ID to every request.
///
/// - Reads `X-Request-ID` from the incoming request if present and validates it as UUID v4.
/// Otherwise generates a new UUID v4.
/// - Records the ID as a `request_id` field on the current tracing span so
/// every log line emitted within the request carries it automatically.
/// - Echoes the ID back in the `X-Request-ID` response header.
pub async fn correlation_id_middleware(mut req: Request, next: Next) -> Response {
let id = req
.headers()
.get(REQUEST_ID_HEADER)
.and_then(|v| v.to_str().ok())
.and_then(parse_valid_request_id)
.unwrap_or_else(|| Uuid::new_v4().to_string());
// Normalise: ensure the header is present on the request for downstream handlers.
// (If we ever failed to create a HeaderValue, fall back to not inserting.)
if let Ok(val) = HeaderValue::from_str(&id) {
req.headers_mut().insert(REQUEST_ID_HEADER, val);
}
let span = tracing::Span::current();
span.record("request_id", &id.as_str());
let mut response = next.run(req).await;
if let Ok(val) = HeaderValue::from_str(&id) {
response.headers_mut().insert(REQUEST_ID_HEADER, val);
}
response
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_uuid_v4_is_accepted() {
let header = "550e8400-e29b-41d4-a716-446655440000"; // version 4
let parsed = parse_valid_request_id(header);
assert_eq!(parsed.as_deref(), Some(header));
}
#[test]
fn malformed_is_rejected_and_replaced() {
assert!(parse_valid_request_id("not-a-uuid").is_none());
assert!(parse_valid_request_id("550e8400-e29b").is_none());
}
#[test]
fn uuid_non_v4_is_rejected() {
// Version 1 UUID string example
let header = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
assert!(parse_valid_request_id(header).is_none());
}
#[test]
fn too_long_is_rejected() {
let long = format!("{}{}", "550e8400-e29b-41d4-a716-446655440000", "x".repeat(100));
assert!(parse_valid_request_id(&long).is_none());
}
}