gotham/state/
request_id.rs

1//! Defines a unique id per `Request` that should be output with all logging.
2
3use hyper::header::HeaderMap;
4use log::trace;
5use uuid::Uuid;
6
7use crate::state::{FromState, State};
8
9/// A container type for the value returned by `request_id`.
10pub(super) struct RequestId {
11    val: String,
12}
13
14/// Sets a unique identifier for the request if it has not already been stored.
15///
16/// The unique identifier chosen depends on the the request headers:
17///
18/// 1. If the header `X-Request-ID` is provided this value is used as-is;
19/// 2. Alternatively creates and stores a UUID v4 value.
20///
21/// This function is invoked by `GothamService` before handing control to its `Router`, to ensure
22/// that a value for `RequestId` is always available.
23pub(crate) fn set_request_id<'a>(state: &'a mut State) -> &'a str {
24    if !state.has::<RequestId>() {
25        let request_id = match HeaderMap::borrow_from(state).get("X-Request-ID") {
26            Some(ex_req_id) => {
27                let id = String::from_utf8(ex_req_id.as_bytes().into()).unwrap();
28                trace!(
29                    "[{}] RequestId set from external source via X-Request-ID header",
30                    id
31                );
32                RequestId { val: id }
33            }
34            None => {
35                let val = Uuid::new_v4().hyphenated().to_string();
36                trace!("[{}] RequestId generated internally", val);
37                RequestId { val }
38            }
39        };
40        state.put(request_id);
41    };
42
43    request_id(state)
44}
45
46/// Returns the request ID associated with the current request.
47///
48/// This is typically used for logging and correlating events that occurred within a request.
49///
50/// # Panics
51///
52/// Will panic if `State` does not contain a request ID, which is an invalid state. The request ID
53/// should always be populated by Gotham before a `Router` is invoked.
54pub fn request_id(state: &State) -> &str {
55    match RequestId::try_borrow_from(state) {
56        Some(request_id) => &request_id.val,
57        None => panic!("RequestId must be populated before application code is invoked"),
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    #[should_panic(expected = "RequestId must be populated before application code is invoked")]
67    fn panics_before_request_id_set() {
68        let state = State::new();
69        request_id(&state);
70    }
71
72    #[test]
73    fn uses_an_external_request_id() {
74        let mut state = State::new();
75
76        let mut headers = HeaderMap::new();
77        headers.insert("X-Request-ID", "1-2-3-4".to_owned().parse().unwrap());
78        state.put(headers);
79
80        {
81            let r = set_request_id(&mut state);
82            assert_eq!("1-2-3-4", r);
83        };
84        assert_eq!("1-2-3-4", request_id(&state));
85    }
86
87    #[test]
88    fn sets_a_unique_request_id() {
89        let mut state = State::new();
90        state.put(HeaderMap::new());
91
92        {
93            let r = set_request_id(&mut state);
94            assert_eq!(4, Uuid::parse_str(r).unwrap().get_version_num());
95        };
96        assert_eq!(
97            4,
98            Uuid::parse_str(request_id(&state))
99                .unwrap()
100                .get_version_num()
101        );
102    }
103
104    #[test]
105    fn does_not_overwrite_existant_request_id() {
106        let mut state = State::new();
107        state.put(RequestId {
108            val: "1-2-3-4".to_string(),
109        });
110
111        {
112            set_request_id(&mut state);
113        }
114        assert_eq!("1-2-3-4", request_id(&state));
115    }
116}