1use crate::context::{DatadogContext, Strategy, TraceContextExt};
7use http::{HeaderMap, HeaderName, HeaderValue};
8
9impl TraceContextExt for HeaderMap {}
10
11pub struct W3CTraceContextHeaders;
32
33const W3C_TRACEPARENT_HEADER: HeaderName = HeaderName::from_static("traceparent");
34
35impl Strategy<HeaderMap> for W3CTraceContextHeaders {
36 fn inject(headers: &mut HeaderMap, context: DatadogContext) {
37 if context.is_empty() {
38 return;
39 }
40
41 let header = format!(
42 "{version:02x}-{trace_id:032x}-{parent_id:016x}-{trace_flags:02x}",
43 version = 0,
44 trace_id = context.trace_id,
45 parent_id = context.parent_id,
46 trace_flags = 1,
47 );
48
49 headers.insert(W3C_TRACEPARENT_HEADER, header.parse().unwrap());
50 }
51
52 fn extract(headers: &HeaderMap) -> DatadogContext {
53 move || -> Option<DatadogContext> {
54 let header = headers.get(W3C_TRACEPARENT_HEADER)?.to_str().ok()?;
55
56 let parts: Vec<&str> = header.split('-').collect();
57 if parts.len() != 4 {
58 return None;
59 }
60
61 let Some(0) = u8::from_str_radix(parts[0], 16).ok() else {
62 return None;
64 };
65
66 let Some(0x01) = u8::from_str_radix(parts[3], 16).ok().map(|n| n & 0x01) else {
67 return None;
69 };
70
71 let trace_id = u128::from_str_radix(parts[1], 16).ok()?;
72 let parent_id = u64::from_str_radix(parts[2], 16).ok()?;
73
74 Some(DatadogContext {
75 trace_id,
76 parent_id,
77 })
78 }()
79 .unwrap_or_default()
80 }
81}
82
83pub struct DatadogHeaders;
104
105const DATADOG_TRACE_ID_HEADER: HeaderName = HeaderName::from_static("x-datadog-trace-id");
106const DATADOG_PARENT_ID_HEADER: HeaderName = HeaderName::from_static("x-datadog-parent-id");
107const DATADOG_SAMPLING_PRIORITY_HEADER: HeaderName =
108 HeaderName::from_static("x-datadog-sampling-priority");
109const DATADOG_TAGS_HEADER: HeaderName = HeaderName::from_static("x-datadog-tags");
110
111impl Strategy<HeaderMap> for DatadogHeaders {
112 fn inject(headers: &mut HeaderMap, context: DatadogContext) {
113 if context.is_empty() {
114 return;
115 }
116
117 let lower_64_bits = context.trace_id as u64;
118 let upper_64_bits = (context.trace_id >> 64) as u64;
119
120 headers.insert(
121 DATADOG_TRACE_ID_HEADER,
122 lower_64_bits.to_string().parse().unwrap(),
123 );
124 headers.insert(
125 DATADOG_PARENT_ID_HEADER,
126 context.parent_id.to_string().parse().unwrap(),
127 );
128 headers.insert(
129 DATADOG_SAMPLING_PRIORITY_HEADER,
130 HeaderValue::from_static("1"),
131 );
132 headers.insert(
133 DATADOG_TAGS_HEADER,
134 format!("_dd.p.tid={upper_64_bits:016x}")
135 .parse()
136 .ok()
137 .unwrap(),
138 );
139 }
140
141 fn extract(headers: &HeaderMap) -> DatadogContext {
142 move || -> Option<DatadogContext> {
143 if headers
144 .get(DATADOG_SAMPLING_PRIORITY_HEADER)?
145 .to_str()
146 .ok()?
147 .parse::<u8>()
148 .ok()?
149 < 1
150 {
151 return None;
152 }
153
154 let lower_64_bits = headers
155 .get(DATADOG_TRACE_ID_HEADER)?
156 .to_str()
157 .ok()?
158 .parse::<u64>()
159 .ok()? as u128;
160 let parent_id = headers
161 .get(DATADOG_PARENT_ID_HEADER)?
162 .to_str()
163 .ok()?
164 .parse()
165 .ok()?;
166
167 let upper_64_bits: u128 = headers
168 .get(DATADOG_TAGS_HEADER)
169 .and_then(|header| {
170 header.to_str().ok()?.split(',').find_map(|pair| {
171 pair.strip_prefix("_dd.p.tid=").and_then(|hex_value| {
172 u64::from_str_radix(hex_value, 16).map(|x| x as u128).ok()
173 })
174 })
175 })
176 .unwrap_or_default();
177
178 let trace_id = (upper_64_bits << 64) | lower_64_bits;
179
180 Some(DatadogContext {
181 trace_id,
182 parent_id,
183 })
184 }()
185 .unwrap_or_default()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use rand::random_range;
193
194 #[test]
195 fn w3c_trace_header_round_trip() {
196 let context = DatadogContext {
197 trace_id: random_range(1..=u128::MAX),
198 parent_id: random_range(1..=u64::MAX),
199 };
200
201 let mut headers = HeaderMap::new();
202 headers.inject_trace_context::<W3CTraceContextHeaders>(context);
203 let parsed = headers.extract_trace_context::<W3CTraceContextHeaders>();
204
205 assert_eq!(context.trace_id, parsed.trace_id);
206 assert_eq!(context.parent_id, parsed.parent_id);
207 }
208
209 #[test]
210 fn empty_context_doesnt_produce_w3c_trace_header() {
211 let mut headers = HeaderMap::new();
212 headers.inject_trace_context::<W3CTraceContextHeaders>(DatadogContext::default());
213 assert!(headers.is_empty());
214 }
215
216 #[test]
217 fn w3c_trace_header_with_wrong_version_produces_empty_context() {
218 let headers = HeaderMap::from_iter([(
219 W3C_TRACEPARENT_HEADER,
220 "01-00000000000000000000000000000001-0000000000000001-01"
221 .parse()
222 .unwrap(),
223 )]);
224 let context = headers.extract_trace_context::<W3CTraceContextHeaders>();
225 assert!(context.is_empty());
226 }
227
228 #[test]
229 fn w3c_trace_header_without_sampling_flag_produces_empty_context() {
230 let headers = HeaderMap::from_iter([(
231 W3C_TRACEPARENT_HEADER,
232 "00-00000000000000000000000000000001-0000000000000001-00"
233 .parse()
234 .unwrap(),
235 )]);
236 let context = headers.extract_trace_context::<W3CTraceContextHeaders>();
237 assert!(context.is_empty());
238 }
239
240 #[test]
241 fn datadog_headers_round_trip() {
242 let context = DatadogContext {
243 trace_id: random_range((u64::MAX as u128 + 1)..=u128::MAX),
245 parent_id: random_range(1..=u64::MAX),
246 };
247
248 let mut headers = HeaderMap::new();
249 headers.inject_trace_context::<DatadogHeaders>(context);
250 let parsed = headers.extract_trace_context::<DatadogHeaders>();
251
252 assert_eq!(context.trace_id, parsed.trace_id);
253 assert_eq!(context.parent_id, parsed.parent_id);
254 }
255
256 #[test]
257 fn empty_context_doesnt_produce_datadog_headers() {
258 let mut headers = HeaderMap::new();
259 headers.inject_trace_context::<DatadogHeaders>(DatadogContext::default());
260 assert!(headers.is_empty());
261 }
262
263 #[test]
264 fn datadog_headers_without_sampling_produce_empty_context() {
265 let headers = HeaderMap::from_iter([(
266 DATADOG_SAMPLING_PRIORITY_HEADER,
267 HeaderValue::from_static("0"),
268 )]);
269 let context = headers.extract_trace_context::<DatadogHeaders>();
270 assert!(context.is_empty());
271 }
272
273 #[test]
274 fn from_datadog_headers_works_without_tags_header() {
275 let headers = HeaderMap::from_iter([
276 (
277 DATADOG_TRACE_ID_HEADER,
278 HeaderValue::from_static("0000000000000001"),
279 ),
280 (
281 DATADOG_PARENT_ID_HEADER,
282 HeaderValue::from_static("0000000000000001"),
283 ),
284 (
285 DATADOG_SAMPLING_PRIORITY_HEADER,
286 HeaderValue::from_static("1"),
287 ),
288 ]);
289 let context = headers.extract_trace_context::<DatadogHeaders>();
290 assert_eq!(context.trace_id, 0x0000000000000001);
291 assert_eq!(context.parent_id, 0x0000000000000001);
292 }
293
294 #[test]
295 fn from_datadog_header_works_with_other_tags() {
296 let headers = HeaderMap::from_iter([
297 (
298 DATADOG_TRACE_ID_HEADER,
299 HeaderValue::from_static("0000000000000001"),
300 ),
301 (
302 DATADOG_PARENT_ID_HEADER,
303 HeaderValue::from_static("0000000000000001"),
304 ),
305 (
306 DATADOG_SAMPLING_PRIORITY_HEADER,
307 HeaderValue::from_static("1"),
308 ),
309 (
310 DATADOG_TAGS_HEADER,
311 HeaderValue::from_static("other=tags,_dd.p.tid=0000000000000002,more=tags"),
312 ),
313 ]);
314 let context = headers.extract_trace_context::<DatadogHeaders>();
315 assert_eq!(context.trace_id, 0x20000000000000001);
316 assert_eq!(context.parent_id, 0x0000000000000001);
317 }
318}