opentelemetry_lambda_extension/
tracing.rs1use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct XRayTraceHeader {
12 pub root: String,
14 pub parent: Option<String>,
16 pub sampled: Option<bool>,
18}
19
20impl XRayTraceHeader {
21 pub fn parse(header: &str) -> Option<Self> {
45 let mut root = None;
46 let mut parent = None;
47 let mut sampled = None;
48
49 for part in header.split(';') {
50 let part = part.trim();
51 if let Some((key, value)) = part.split_once('=') {
52 match key {
53 "Root" => root = Some(value.to_string()),
54 "Parent" => parent = Some(value.to_string()),
55 "Sampled" => {
56 sampled = match value {
57 "1" => Some(true),
58 "0" => Some(false),
59 _ => None,
60 }
61 }
62 _ => {} }
64 }
65 }
66
67 root.map(|root| Self {
68 root,
69 parent,
70 sampled,
71 })
72 }
73
74 pub fn to_w3c(&self) -> Option<W3CTraceContext> {
88 let parent = self.parent.as_ref()?;
89
90 let trace_id = self.xray_root_to_trace_id()?;
94
95 if parent.len() != 16 || !parent.chars().all(|c| c.is_ascii_hexdigit()) {
98 return None;
99 }
100
101 Some(W3CTraceContext {
102 trace_id,
103 span_id: parent.clone(),
104 sampled: self.sampled.unwrap_or(false),
105 })
106 }
107
108 fn xray_root_to_trace_id(&self) -> Option<String> {
115 let parts: Vec<&str> = self.root.split('-').collect();
116 if parts.len() != 3 {
117 return None;
118 }
119
120 let version = parts[0];
121 let timestamp = parts[1];
122 let random = parts[2];
123
124 if version != "1" {
126 return None;
127 }
128
129 if timestamp.len() != 8 || !timestamp.chars().all(|c| c.is_ascii_hexdigit()) {
131 return None;
132 }
133
134 if random.len() != 24 || !random.chars().all(|c| c.is_ascii_hexdigit()) {
136 return None;
137 }
138
139 Some(format!("{}{}", timestamp, random))
141 }
142
143 pub fn to_header_string(&self) -> String {
145 let mut parts = vec![format!("Root={}", self.root)];
146
147 if let Some(ref parent) = self.parent {
148 parts.push(format!("Parent={}", parent));
149 }
150
151 if let Some(sampled) = self.sampled {
152 parts.push(format!("Sampled={}", if sampled { "1" } else { "0" }));
153 }
154
155 parts.join(";")
156 }
157}
158
159impl fmt::Display for XRayTraceHeader {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(f, "{}", self.to_header_string())
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct W3CTraceContext {
168 pub trace_id: String,
170 pub span_id: String,
172 pub sampled: bool,
174}
175
176impl W3CTraceContext {
177 pub fn trace_id_bytes(&self) -> Option<[u8; 16]> {
181 let bytes = hex::decode(&self.trace_id).ok()?;
182 if bytes.len() != 16 {
183 return None;
184 }
185 let mut arr = [0u8; 16];
186 arr.copy_from_slice(&bytes);
187 Some(arr)
188 }
189
190 pub fn span_id_bytes(&self) -> Option<[u8; 8]> {
194 let bytes = hex::decode(&self.span_id).ok()?;
195 if bytes.len() != 8 {
196 return None;
197 }
198 let mut arr = [0u8; 8];
199 arr.copy_from_slice(&bytes);
200 Some(arr)
201 }
202
203 pub fn to_traceparent(&self) -> String {
207 let flags = if self.sampled { "01" } else { "00" };
208 format!("00-{}-{}-{}", self.trace_id, self.span_id, flags)
209 }
210}
211
212impl fmt::Display for W3CTraceContext {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 write!(f, "{}", self.to_traceparent())
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use proptest::prelude::*;
222
223 fn valid_timestamp() -> impl Strategy<Value = String> {
224 "[0-9a-f]{8}".prop_map(|s| s.to_lowercase())
225 }
226
227 fn valid_random() -> impl Strategy<Value = String> {
228 "[0-9a-f]{24}".prop_map(|s| s.to_lowercase())
229 }
230
231 fn valid_parent() -> impl Strategy<Value = String> {
232 "[0-9a-f]{16}".prop_map(|s| s.to_lowercase())
233 }
234
235 proptest! {
236 #[test]
237 fn parse_roundtrips(
238 timestamp in valid_timestamp(),
239 random in valid_random(),
240 parent in valid_parent(),
241 sampled in prop::bool::ANY
242 ) {
243 let root = format!("1-{}-{}", timestamp, random);
244 let header_str = format!(
245 "Root={};Parent={};Sampled={}",
246 root,
247 parent,
248 if sampled { "1" } else { "0" }
249 );
250
251 let parsed = XRayTraceHeader::parse(&header_str).unwrap();
252
253 prop_assert_eq!(parsed.root, root);
254 prop_assert_eq!(parsed.parent, Some(parent));
255 prop_assert_eq!(parsed.sampled, Some(sampled));
256 }
257
258 #[test]
259 fn w3c_conversion_produces_valid_ids(
260 timestamp in valid_timestamp(),
261 random in valid_random(),
262 parent in valid_parent(),
263 ) {
264 let header = XRayTraceHeader {
265 root: format!("1-{}-{}", timestamp, random),
266 parent: Some(parent.clone()),
267 sampled: Some(true),
268 };
269
270 let w3c = header.to_w3c().unwrap();
271
272 prop_assert_eq!(w3c.trace_id.len(), 32);
273 prop_assert_eq!(w3c.span_id.len(), 16);
274 prop_assert_eq!(w3c.span_id, parent);
275 prop_assert!(w3c.trace_id.chars().all(|c| c.is_ascii_hexdigit()));
276 }
277
278 #[test]
279 fn trace_id_bytes_roundtrips(
280 timestamp in valid_timestamp(),
281 random in valid_random(),
282 parent in valid_parent(),
283 ) {
284 let header = XRayTraceHeader {
285 root: format!("1-{}-{}", timestamp, random),
286 parent: Some(parent),
287 sampled: Some(true),
288 };
289
290 let w3c = header.to_w3c().unwrap();
291 let bytes = w3c.trace_id_bytes().unwrap();
292
293 prop_assert_eq!(bytes.len(), 16);
294 let hex_back = hex::encode(bytes);
295 prop_assert_eq!(hex_back, w3c.trace_id);
296 }
297 }
298
299 #[test]
300 fn test_parse_full_header() {
301 let header = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1";
302 let parsed = XRayTraceHeader::parse(header).unwrap();
303
304 assert_eq!(parsed.root, "1-5759e988-bd862e3fe1be46a994272793");
305 assert_eq!(parsed.parent, Some("53995c3f42cd8ad8".to_string()));
306 assert_eq!(parsed.sampled, Some(true));
307 }
308
309 #[test]
310 fn test_parse_header_unsampled() {
311 let header = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=0";
312 let parsed = XRayTraceHeader::parse(header).unwrap();
313
314 assert_eq!(parsed.sampled, Some(false));
315 }
316
317 #[test]
318 fn test_parse_header_no_parent() {
319 let header = "Root=1-5759e988-bd862e3fe1be46a994272793;Sampled=1";
320 let parsed = XRayTraceHeader::parse(header).unwrap();
321
322 assert_eq!(parsed.root, "1-5759e988-bd862e3fe1be46a994272793");
323 assert!(parsed.parent.is_none());
324 assert_eq!(parsed.sampled, Some(true));
325 }
326
327 #[test]
328 fn test_parse_header_root_only() {
329 let header = "Root=1-5759e988-bd862e3fe1be46a994272793";
330 let parsed = XRayTraceHeader::parse(header).unwrap();
331
332 assert_eq!(parsed.root, "1-5759e988-bd862e3fe1be46a994272793");
333 assert!(parsed.parent.is_none());
334 assert!(parsed.sampled.is_none());
335 }
336
337 #[test]
338 fn test_parse_invalid_header() {
339 assert!(XRayTraceHeader::parse("").is_none());
340 assert!(XRayTraceHeader::parse("Parent=123").is_none());
341 assert!(XRayTraceHeader::parse("invalid").is_none());
342 }
343
344 #[test]
345 fn test_to_w3c() {
346 let header = XRayTraceHeader {
347 root: "1-5759e988-bd862e3fe1be46a994272793".to_string(),
348 parent: Some("53995c3f42cd8ad8".to_string()),
349 sampled: Some(true),
350 };
351
352 let w3c = header.to_w3c().unwrap();
353
354 assert_eq!(w3c.trace_id, "5759e988bd862e3fe1be46a994272793");
356 assert_eq!(w3c.span_id, "53995c3f42cd8ad8");
357 assert!(w3c.sampled);
358 }
359
360 #[test]
361 fn test_to_w3c_no_parent() {
362 let header = XRayTraceHeader {
363 root: "1-5759e988-bd862e3fe1be46a994272793".to_string(),
364 parent: None,
365 sampled: Some(true),
366 };
367
368 assert!(header.to_w3c().is_none());
369 }
370
371 #[test]
372 fn test_to_w3c_invalid_parent() {
373 let header = XRayTraceHeader {
374 root: "1-5759e988-bd862e3fe1be46a994272793".to_string(),
375 parent: Some("invalid".to_string()),
376 sampled: Some(true),
377 };
378
379 assert!(header.to_w3c().is_none());
380 }
381
382 #[test]
383 fn test_to_w3c_invalid_root() {
384 let header = XRayTraceHeader {
385 root: "invalid-root".to_string(),
386 parent: Some("53995c3f42cd8ad8".to_string()),
387 sampled: Some(true),
388 };
389
390 assert!(header.to_w3c().is_none());
391 }
392
393 #[test]
394 fn test_w3c_to_traceparent() {
395 let ctx = W3CTraceContext {
396 trace_id: "5759e988bd862e3fe1be46a994272793".to_string(),
397 span_id: "53995c3f42cd8ad8".to_string(),
398 sampled: true,
399 };
400
401 assert_eq!(
402 ctx.to_traceparent(),
403 "00-5759e988bd862e3fe1be46a994272793-53995c3f42cd8ad8-01"
404 );
405 }
406
407 #[test]
408 fn test_w3c_to_traceparent_unsampled() {
409 let ctx = W3CTraceContext {
410 trace_id: "5759e988bd862e3fe1be46a994272793".to_string(),
411 span_id: "53995c3f42cd8ad8".to_string(),
412 sampled: false,
413 };
414
415 assert_eq!(
416 ctx.to_traceparent(),
417 "00-5759e988bd862e3fe1be46a994272793-53995c3f42cd8ad8-00"
418 );
419 }
420
421 #[test]
422 fn test_trace_id_bytes() {
423 let ctx = W3CTraceContext {
424 trace_id: "5759e988bd862e3fe1be46a994272793".to_string(),
425 span_id: "53995c3f42cd8ad8".to_string(),
426 sampled: true,
427 };
428
429 let bytes = ctx.trace_id_bytes().unwrap();
430 assert_eq!(bytes.len(), 16);
431 assert_eq!(bytes[0], 0x57);
432 assert_eq!(bytes[1], 0x59);
433 }
434
435 #[test]
436 fn test_span_id_bytes() {
437 let ctx = W3CTraceContext {
438 trace_id: "5759e988bd862e3fe1be46a994272793".to_string(),
439 span_id: "53995c3f42cd8ad8".to_string(),
440 sampled: true,
441 };
442
443 let bytes = ctx.span_id_bytes().unwrap();
444 assert_eq!(bytes.len(), 8);
445 assert_eq!(bytes[0], 0x53);
446 assert_eq!(bytes[1], 0x99);
447 }
448
449 #[test]
450 fn test_to_header_string() {
451 let header = XRayTraceHeader {
452 root: "1-5759e988-bd862e3fe1be46a994272793".to_string(),
453 parent: Some("53995c3f42cd8ad8".to_string()),
454 sampled: Some(true),
455 };
456
457 assert_eq!(
458 header.to_header_string(),
459 "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"
460 );
461 }
462
463 #[test]
464 fn test_display() {
465 let header = XRayTraceHeader {
466 root: "1-5759e988-bd862e3fe1be46a994272793".to_string(),
467 parent: Some("53995c3f42cd8ad8".to_string()),
468 sampled: Some(true),
469 };
470
471 let s = format!("{}", header);
472 assert!(s.contains("Root="));
473 assert!(s.contains("Parent="));
474 assert!(s.contains("Sampled=1"));
475 }
476}