rainier_telemetry/
middleware.rs1use rainier_http::{Request, Response};
4use rainier_middleware::{Middleware, Next};
5
6use crate::context::{TraceContext, TRACEPARENT, TRACESTATE};
7
8pub struct Trace {
25 sample: bool,
27 respond_with_header: bool,
29}
30
31impl Default for Trace {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl Trace {
38 pub fn new() -> Self {
40 Self { sample: true, respond_with_header: true }
41 }
42
43 pub fn sampling(mut self, sample: bool) -> Self {
49 self.sample = sample;
50 self
51 }
52
53 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 request.extensions_mut().insert(context);
75 if let Some(state) = state {
76 request.extensions_mut().insert(TraceState(state));
77 }
78
79 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#[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 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 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}