1use crate::error::Result;
39use crate::middleware::{Middleware, Next};
40use crate::request::Request;
41use crate::response::Response;
42use crate::security::random_bytes;
43use async_trait::async_trait;
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct TraceContext {
48 pub trace_id: String,
50 pub span_id: String,
52 pub parent_span_id: Option<String>,
54 pub sampled: bool,
56 pub tracestate: Option<String>,
58}
59
60fn hex(bytes: &[u8]) -> String {
61 bytes.iter().map(|b| format!("{b:02x}")).collect()
62}
63
64fn random_hex(len: usize) -> Result<String> {
65 loop {
67 let bytes = random_bytes(len)?;
68 if bytes.iter().any(|b| *b != 0) {
69 return Ok(hex(&bytes));
70 }
71 }
72}
73
74fn is_hex(s: &str, len: usize) -> bool {
75 s.len() == len && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
76}
77
78impl TraceContext {
79 pub fn parse(header: &str) -> Option<(String, String, bool)> {
81 let mut parts = header.trim().split('-');
82 let (version, trace, span, flags) =
83 (parts.next()?, parts.next()?, parts.next()?, parts.next()?);
84 if !is_hex(version, 2) || version == "ff" || (version == "00" && parts.next().is_some()) {
86 return None;
87 }
88 if !is_hex(trace, 32) || trace.bytes().all(|b| b == b'0') {
89 return None;
90 }
91 if !is_hex(span, 16) || span.bytes().all(|b| b == b'0') {
92 return None;
93 }
94 if !is_hex(flags, 2) {
95 return None;
96 }
97 let sampled = u8::from_str_radix(flags, 16).ok()? & 1 == 1;
98 Some((trace.to_string(), span.to_string(), sampled))
99 }
100
101 pub fn traceparent(&self) -> String {
103 format!(
104 "00-{}-{}-{}",
105 self.trace_id,
106 self.span_id,
107 if self.sampled { "01" } else { "00" }
108 )
109 }
110}
111
112impl Request {
113 pub fn trace(&self) -> Option<&TraceContext> {
115 self.extensions.get::<TraceContext>()
116 }
117}
118
119#[derive(Clone, Debug)]
121pub struct TraceLayer {
122 trust_incoming: bool,
123 echo: bool,
124}
125
126impl Default for TraceLayer {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl TraceLayer {
133 pub fn new() -> Self {
134 Self {
135 trust_incoming: true,
136 echo: false,
137 }
138 }
139
140 pub fn ignore_incoming(mut self) -> Self {
142 self.trust_incoming = false;
143 self
144 }
145
146 pub fn echo_response(mut self) -> Self {
149 self.echo = true;
150 self
151 }
152}
153
154#[async_trait]
155impl Middleware for TraceLayer {
156 async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
157 let incoming = self
158 .trust_incoming
159 .then(|| {
160 req.headers
161 .get("traceparent")
162 .and_then(|v| v.to_str().ok())
163 .and_then(TraceContext::parse)
164 })
165 .flatten();
166 let tracestate = incoming
167 .as_ref()
168 .and_then(|_| req.headers.get("tracestate"))
169 .and_then(|v| v.to_str().ok())
170 .filter(|v| v.len() <= 512)
171 .map(str::to_string);
172 let ctx = match incoming {
173 Some((trace_id, parent, sampled)) => TraceContext {
174 trace_id,
175 span_id: random_hex(8)?,
176 parent_span_id: Some(parent),
177 sampled,
178 tracestate,
179 },
180 None => TraceContext {
181 trace_id: random_hex(16)?,
182 span_id: random_hex(8)?,
183 parent_span_id: None,
184 sampled: true,
185 tracestate: None,
186 },
187 };
188 let own = ctx.traceparent();
189 req.extensions.insert(ctx);
190 let res = next.run(req).await.unwrap_or_else(Response::from_error);
191 Ok(if self.echo {
192 res.with_header("traceparent", own)
193 } else {
194 res
195 })
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 const TRACE: &str = "4bf92f3577b34da6a3ce929d0e0e4736";
204 const SPAN: &str = "00f067aa0ba902b7";
205
206 #[test]
207 fn parses_valid_headers() {
208 let (t, s, sampled) = TraceContext::parse(&format!("00-{TRACE}-{SPAN}-01")).unwrap();
209 assert_eq!((t.as_str(), s.as_str(), sampled), (TRACE, SPAN, true));
210 let (_, _, sampled) = TraceContext::parse(&format!("00-{TRACE}-{SPAN}-00")).unwrap();
211 assert!(!sampled);
212 assert!(TraceContext::parse(&format!("01-{TRACE}-{SPAN}-01-extra")).is_some());
214 }
215
216 #[test]
217 fn rejects_malformed_headers() {
218 for bad in [
219 "".to_string(),
220 "garbage".to_string(),
221 format!("ff-{TRACE}-{SPAN}-01"),
222 format!("00-{TRACE}-{SPAN}"),
223 format!("00-{TRACE}-{SPAN}-01-extra"),
224 format!("00-{}-{SPAN}-01", "0".repeat(32)),
225 format!("00-{TRACE}-{}-01", "0".repeat(16)),
226 format!("00-{}-{SPAN}-01", TRACE.to_uppercase()),
227 format!("00-{}-{SPAN}-01", &TRACE[..30]),
228 format!("00-{TRACE}-{SPAN}-0g"),
229 ] {
230 assert!(TraceContext::parse(&bad).is_none(), "{bad}");
231 }
232 }
233
234 #[test]
235 fn own_traceparent_names_the_span() {
236 let ctx = TraceContext {
237 trace_id: TRACE.into(),
238 span_id: SPAN.into(),
239 parent_span_id: None,
240 sampled: true,
241 tracestate: None,
242 };
243 assert_eq!(ctx.traceparent(), format!("00-{TRACE}-{SPAN}-01"));
244 let unsampled = TraceContext {
245 sampled: false,
246 ..ctx
247 };
248 assert_eq!(unsampled.traceparent(), format!("00-{TRACE}-{SPAN}-00"));
249 }
250}