1use std::fmt;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub struct TraceId([u8; 16]);
25
26impl TraceId {
27 pub fn from_bytes(bytes: [u8; 16]) -> Option<Self> {
30 (bytes != [0; 16]).then_some(Self(bytes))
31 }
32
33 pub fn to_bytes(self) -> [u8; 16] {
35 self.0
36 }
37}
38
39impl fmt::Display for TraceId {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 write_hex(f, &self.0)
45 }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub struct SpanId([u8; 8]);
52
53impl SpanId {
54 pub fn from_bytes(bytes: [u8; 8]) -> Option<Self> {
56 (bytes != [0; 8]).then_some(Self(bytes))
57 }
58
59 pub fn to_bytes(self) -> [u8; 8] {
61 self.0
62 }
63}
64
65impl fmt::Display for SpanId {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write_hex(f, &self.0)
68 }
69}
70
71fn write_hex(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
72 const HEX: &[u8; 16] = b"0123456789abcdef";
73 let mut buf = [0u8; 32];
75 for (i, b) in bytes.iter().enumerate() {
76 buf[i * 2] = HEX[(b >> 4) as usize];
77 buf[i * 2 + 1] = HEX[(b & 0x0f) as usize];
78 }
79 f.write_str(std::str::from_utf8(&buf[..bytes.len() * 2]).map_err(|_| fmt::Error)?)
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
89#[non_exhaustive]
90pub struct TraceContext {
91 pub trace_id: TraceId,
93 pub parent_id: Option<SpanId>,
96 pub flags: u8,
99 pub tracestate: Option<http::HeaderValue>,
103}
104
105impl TraceContext {
106 pub fn root(trace_id: TraceId) -> Self {
108 Self {
109 trace_id,
110 parent_id: None,
111 flags: 0,
112 tracestate: None,
113 }
114 }
115
116 pub fn from_headers(headers: &http::HeaderMap) -> Option<Self> {
123 let mut values = headers.get_all("traceparent").iter();
124 let traceparent = values.next()?;
125 if values.next().is_some() {
126 return None;
127 }
128 let (trace_id, parent_id, flags) = parse_traceparent(traceparent.as_bytes())?;
129
130 let mut states = headers.get_all("tracestate").iter();
131 let tracestate = match (states.next(), states.next()) {
132 (None, _) => None,
133 (Some(only), None) => Some(only.clone()),
134 (Some(first), Some(second)) => {
136 let mut joined = first.as_bytes().to_vec();
137 for value in std::iter::once(second).chain(states) {
138 joined.push(b',');
139 joined.extend_from_slice(value.as_bytes());
140 }
141 http::HeaderValue::from_bytes(&joined).ok()
142 }
143 };
144
145 Some(Self {
146 trace_id,
147 parent_id: Some(parent_id),
148 flags,
149 tracestate,
150 })
151 }
152
153 pub fn sampled(&self) -> bool {
155 self.flags & 0x01 != 0
156 }
157}
158
159pub fn parse_traceparent(value: &[u8]) -> Option<(TraceId, SpanId, u8)> {
168 if value.len() < 55 {
169 return None;
170 }
171 let version = hex_byte(&value[0..2])?;
172 if version == 0xff {
173 return None;
174 }
175 if version == 0x00 && value.len() != 55 {
176 return None;
177 }
178 if value.len() > 55 && value[55] != b'-' {
179 return None;
180 }
181 if value[2] != b'-' || value[35] != b'-' || value[52] != b'-' {
182 return None;
183 }
184
185 let mut trace = [0u8; 16];
186 for (i, byte) in trace.iter_mut().enumerate() {
187 *byte = hex_byte(&value[3 + i * 2..5 + i * 2])?;
188 }
189 let mut parent = [0u8; 8];
190 for (i, byte) in parent.iter_mut().enumerate() {
191 *byte = hex_byte(&value[36 + i * 2..38 + i * 2])?;
192 }
193 let flags = hex_byte(&value[53..55])?;
194
195 Some((
196 TraceId::from_bytes(trace)?,
197 SpanId::from_bytes(parent)?,
198 flags,
199 ))
200}
201
202fn hex_byte(pair: &[u8]) -> Option<u8> {
205 fn digit(c: u8) -> Option<u8> {
206 match c {
207 b'0'..=b'9' => Some(c - b'0'),
208 b'a'..=b'f' => Some(c - b'a' + 10),
209 _ => None,
210 }
211 }
212 Some((digit(pair[0])? << 4) | digit(pair[1])?)
213}
214
215pub const MAX_REQUEST_ID_LEN: usize = 128;
220
221pub fn valid_request_id(id: &[u8]) -> bool {
228 (1..=MAX_REQUEST_ID_LEN).contains(&id.len()) && id.iter().all(|b| (0x21..=0x7e).contains(b))
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 const VALID: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
236
237 #[test]
238 fn a_valid_traceparent_parses() {
239 let (trace, parent, flags) = parse_traceparent(VALID.as_bytes()).unwrap();
240 assert_eq!(trace.to_string(), "4bf92f3577b34da6a3ce929d0e0e4736");
241 assert_eq!(parent.to_string(), "00f067aa0ba902b7");
242 assert_eq!(flags, 0x01);
243 }
244
245 #[test]
246 fn version_ff_is_refused() {
247 let value = VALID.replacen("00", "ff", 1);
248 assert_eq!(parse_traceparent(value.as_bytes()), None);
249 }
250
251 #[test]
252 fn all_zero_ids_are_refused() {
253 let zero_trace = "00-00000000000000000000000000000000-00f067aa0ba902b7-01";
254 let zero_parent = "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01";
255 assert_eq!(parse_traceparent(zero_trace.as_bytes()), None);
256 assert_eq!(parse_traceparent(zero_parent.as_bytes()), None);
257 }
258
259 #[test]
260 fn malformed_values_are_refused() {
261 for value in [
262 "",
263 "00",
264 "00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01",
266 "00-4bf92f3577b34da6a3ce929d0e0e4736x00f067aa0ba902b7-01",
268 "00-4bf92f3577b34da6a3ce929d0e0e473g-00f067aa0ba902b7-01",
270 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1",
272 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra",
274 "cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01x",
276 ] {
277 assert_eq!(parse_traceparent(value.as_bytes()), None, "{value:?}");
278 }
279 }
280
281 #[test]
282 fn a_later_version_is_read_as_00() {
283 let value = "cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-what-the-future-holds";
284 let (trace, _, _) = parse_traceparent(value.as_bytes()).unwrap();
285 assert_eq!(trace.to_string(), "4bf92f3577b34da6a3ce929d0e0e4736");
286 assert!(parse_traceparent(VALID.replacen("00", "01", 1).as_bytes()).is_some());
288 }
289
290 #[test]
291 fn trace_context_comes_from_headers() {
292 let mut headers = http::HeaderMap::new();
293 headers.insert("traceparent", VALID.parse().unwrap());
294 headers.append("tracestate", "rojo=00f067aa0ba902b7".parse().unwrap());
295 headers.append("tracestate", "congo=t61rcWkgMzE".parse().unwrap());
296 let context = TraceContext::from_headers(&headers).unwrap();
297 assert_eq!(
298 context.trace_id.to_string(),
299 "4bf92f3577b34da6a3ce929d0e0e4736"
300 );
301 assert!(context.sampled());
302 assert_eq!(
303 context.tracestate.unwrap(),
304 "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
305 );
306 }
307
308 #[test]
309 fn two_traceparents_are_no_traceparent() {
310 let mut headers = http::HeaderMap::new();
311 headers.append("traceparent", VALID.parse().unwrap());
312 headers.append("traceparent", VALID.parse().unwrap());
313 assert_eq!(TraceContext::from_headers(&headers), None);
314 }
315
316 #[test]
317 fn tracestate_alone_means_nothing() {
318 let mut headers = http::HeaderMap::new();
319 headers.insert("tracestate", "rojo=00f067aa0ba902b7".parse().unwrap());
320 assert_eq!(TraceContext::from_headers(&headers), None);
321 }
322
323 #[test]
324 fn request_ids_are_bounded_visible_ascii() {
325 assert!(valid_request_id(b"0f8fad5b-d9cb-469f-a165-70867728950e"));
326 assert!(valid_request_id(b"a"));
327 assert!(valid_request_id(&[b'x'; MAX_REQUEST_ID_LEN]));
328 assert!(!valid_request_id(b""));
329 assert!(!valid_request_id(&[b'x'; MAX_REQUEST_ID_LEN + 1]));
330 assert!(!valid_request_id(b"has space"));
331 assert!(!valid_request_id(b"tab\there"));
332 assert!(!valid_request_id("caf\u{e9}".as_bytes()));
333 }
334}