Skip to main content

rainier_telemetry/
middleware.rs

1//! [`Trace`] — the middleware that puts a request in a trace.
2
3use rainier_http::{Request, Response};
4use rainier_middleware::{Middleware, Next};
5
6use crate::context::{TraceContext, TRACEPARENT, TRACESTATE};
7
8/// Joins each request to its trace, and puts the trace on the log line.
9///
10/// Outermost in the global stack, so every log emitted while handling the
11/// request carries the trace id — including the ones from middleware that
12/// rejected it, which are the lines you most want to find later.
13///
14/// ```ignore
15/// MiddlewareStack::new().push(Trace::new()).push(RecordMetrics::new(metrics))
16/// ```
17///
18/// # What it does without an exporter
19///
20/// Everything except send spans anywhere. The trace id is on the request, on
21/// the response and on every log line, so a request can be followed across
22/// services by grepping — which is most of the value, and it costs no
23/// dependency. The [`otlp`](crate) feature adds the exporter.
24pub struct Trace {
25    /// Whether to sample when nothing upstream has decided.
26    sample: bool,
27    /// Echo the trace id back on the response.
28    respond_with_header: bool,
29}
30
31impl Default for Trace {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl Trace {
38    /// Sampling everything, and echoing the header back.
39    pub fn new() -> Self {
40        Self { sample: true, respond_with_header: true }
41    }
42
43    /// Whether to sample a trace this service starts.
44    ///
45    /// Only applies when there is no incoming `traceparent`: a decision made
46    /// upstream is honoured, because a trace sampled in half its services is a
47    /// trace with holes in it.
48    pub fn sampling(mut self, sample: bool) -> Self {
49        self.sample = sample;
50        self
51    }
52
53    /// Whether to return `traceparent` on the response.
54    ///
55    /// On by default, and it is what lets somebody paste a trace id from a
56    /// browser's network tab into a trace viewer. Turn it off if you would
57    /// rather not tell a client your trace ids.
58    pub fn respond_with_header(mut self, respond: bool) -> Self {
59        self.respond_with_header = respond;
60        self
61    }
62}
63
64#[async_trait::async_trait]
65impl Middleware for Trace {
66    async fn handle(&self, mut request: Request, next: Next) -> Response {
67        let context = TraceContext::extract(request.header(TRACEPARENT), self.sample);
68        let state = request.header(TRACESTATE).map(str::to_string);
69
70        let header = context.to_header();
71        let trace_id = context.trace_id().to_string();
72
73        // On the request, so a handler can propagate it to whatever it calls.
74        request.extensions_mut().insert(context);
75        if let Some(state) = state {
76            request.extensions_mut().insert(TraceState(state));
77        }
78
79        // A span rather than a field on each event: everything logged inside
80        // it inherits the id, including from code that knows nothing about
81        // tracing contexts.
82        let span = tracing::info_span!("http.request", trace_id = %trace_id);
83        let _entered = span.enter();
84
85        let response = next.run(request).await;
86
87        if self.respond_with_header {
88            return response.with_header(TRACEPARENT, &header);
89        }
90        response
91    }
92
93    fn name(&self) -> &'static str {
94        "Trace"
95    }
96}
97
98/// The `tracestate` header, carried through untouched.
99///
100/// Vendor-specific, and the specification is explicit that a service which does
101/// not understand an entry must pass it on rather than drop it.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct TraceState(pub String);
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use rainier_http::StatusCode;
109    use rainier_middleware::Pipeline;
110
111    const EXAMPLE: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
112
113    async fn through(trace: Trace, request: Request) -> Response {
114        Pipeline::new()
115            .through(trace)
116            .then(|request: Request| {
117                Box::pin(async move {
118                    let context = request
119                        .extension::<TraceContext>()
120                        .map(|c| c.trace_id().to_string())
121                        .unwrap_or_default();
122
123                    Response::ok(context)
124                })
125            })
126            .run(request)
127            .await
128    }
129
130    async fn body_of(response: Response) -> String {
131        let bytes = response.into_http().into_body().collect().await.expect("a body");
132        String::from_utf8_lossy(&bytes).to_string()
133    }
134
135    #[tokio::test]
136    async fn an_incoming_trace_is_joined_rather_than_replaced() {
137        let request = Request::builder().header(TRACEPARENT, EXAMPLE).build();
138
139        let response = through(Trace::new(), request).await;
140
141        assert_eq!(response.status(), StatusCode::OK);
142        assert_eq!(body_of(response).await, "4bf92f3577b34da6a3ce929d0e0e4736");
143    }
144
145    #[tokio::test]
146    async fn a_request_with_no_trace_gets_one() {
147        let response = through(Trace::new(), Request::builder().build()).await;
148
149        assert_eq!(body_of(response).await.len(), 32);
150    }
151
152    #[tokio::test]
153    async fn a_broken_trace_does_not_fail_the_request() {
154        // An upstream with a misconfigured proxy is not this service's problem
155        // to turn into a 400.
156        let request = Request::builder().header(TRACEPARENT, "garbage").build();
157
158        let response = through(Trace::new(), request).await;
159
160        assert_eq!(response.status(), StatusCode::OK);
161        assert_eq!(body_of(response).await.len(), 32, "a fresh trace");
162    }
163
164    #[tokio::test]
165    async fn the_trace_comes_back_on_the_response() {
166        let request = Request::builder().header(TRACEPARENT, EXAMPLE).build();
167
168        let response = through(Trace::new(), request).await;
169
170        assert_eq!(response.header(TRACEPARENT), Some(EXAMPLE));
171    }
172
173    #[tokio::test]
174    async fn the_response_header_can_be_turned_off() {
175        let request = Request::builder().header(TRACEPARENT, EXAMPLE).build();
176
177        let response = through(Trace::new().respond_with_header(false), request).await;
178
179        assert_eq!(response.header(TRACEPARENT), None);
180    }
181
182    #[tokio::test]
183    async fn an_upstream_sampling_decision_is_honoured_over_the_local_one() {
184        // A trace sampled in half its services is a trace with holes in it.
185        let unsampled = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00";
186        let request = Request::builder().header(TRACEPARENT, unsampled).build();
187
188        let response = through(Trace::new().sampling(true), request).await;
189
190        assert_eq!(response.header(TRACEPARENT), Some(unsampled));
191    }
192
193    #[tokio::test]
194    async fn a_locally_started_trace_follows_the_sampling_setting() {
195        let response = through(Trace::new().sampling(false), Request::builder().build()).await;
196
197        let header = response.header(TRACEPARENT).expect("a header").to_string();
198        assert!(header.ends_with("-00"), "{header}");
199    }
200}