toolkit_trace_context/
lib.rs1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
22
23use http::HeaderMap;
24use std::future::Future;
25
26#[cfg(feature = "otel")]
42#[must_use]
43pub fn current_trace_ids() -> Option<(opentelemetry::trace::TraceId, opentelemetry::trace::SpanId)>
44{
45 use opentelemetry::trace::TraceContextExt as _;
46
47 let context = opentelemetry::Context::current();
48 let span = context.span();
49 let span_context = span.span_context();
50 span_context
51 .is_valid()
52 .then(|| (span_context.trace_id(), span_context.span_id()))
53}
54
55#[cfg(feature = "otel")]
60#[must_use]
61pub fn current_trace_id() -> Option<String> {
62 use opentelemetry::trace::TraceContextExt as _;
63
64 let context = opentelemetry::Context::current();
65 let span = context.span();
66 let span_context = span.span_context();
67 span_context
68 .is_valid()
69 .then(|| span_context.trace_id().to_string())
70}
71
72#[cfg(not(feature = "otel"))]
74#[must_use]
75pub fn current_trace_id() -> Option<String> {
76 None
77}
78
79#[must_use]
94pub fn extract_trace_id(headers: &HeaderMap) -> Option<String> {
95 current_trace_id().or_else(|| get_traceparent(headers).and_then(parse_trace_id))
96}
97
98pub const TRACEPARENT: &str = "traceparent";
100
101#[must_use]
103pub fn get_traceparent(headers: &HeaderMap) -> Option<&str> {
104 headers.get(TRACEPARENT)?.to_str().ok()
105}
106
107#[must_use]
115pub fn parse_trace_id(traceparent: &str) -> Option<String> {
116 let parts: Vec<&str> = traceparent.split('-').collect();
117 if parts.len() == 4
118 && parts[0] == "00"
119 && is_valid_trace_id(parts[1])
120 && is_valid_parent_id(parts[2])
121 && is_valid_flags(parts[3])
122 {
123 Some(parts[1].to_owned())
124 } else {
125 None
126 }
127}
128
129fn is_lowercase_hex(s: &str, len: usize) -> bool {
131 s.len() == len
132 && s.bytes()
133 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
134}
135
136fn is_valid_trace_id(s: &str) -> bool {
138 is_lowercase_hex(s, 32) && s.bytes().any(|b| b != b'0')
139}
140
141fn is_valid_parent_id(s: &str) -> bool {
143 is_lowercase_hex(s, 16) && s.bytes().any(|b| b != b'0')
144}
145
146fn is_valid_flags(s: &str) -> bool {
148 is_lowercase_hex(s, 2)
149}
150
151#[cfg(feature = "otel")]
154mod imp {
155 use super::parse_trace_id;
156 use http::{HeaderMap, HeaderName, HeaderValue};
157 use opentelemetry::{
158 Context, global,
159 propagation::{Extractor, Injector},
160 };
161 use tracing::Span;
162 use tracing_opentelemetry::OpenTelemetrySpanExt as _;
163
164 struct HeadersExtractor<'a>(&'a HeaderMap);
166
167 impl Extractor for HeadersExtractor<'_> {
168 fn get(&self, key: &str) -> Option<&str> {
169 self.0.get(key).and_then(|v| v.to_str().ok())
170 }
171
172 fn keys(&self) -> Vec<&str> {
173 self.0.keys().map(http::HeaderName::as_str).collect()
174 }
175 }
176
177 struct HeadersInjector<'a>(&'a mut HeaderMap);
179
180 impl Injector for HeadersInjector<'_> {
181 fn set(&mut self, key: &str, value: String) {
182 if let Ok(name) = HeaderName::from_bytes(key.as_bytes())
183 && let Ok(val) = HeaderValue::from_str(&value)
184 {
185 self.0.insert(name, val);
186 }
187 }
188 }
189
190 pub fn set_parent_from_headers(span: &Span, headers: &HeaderMap) {
200 let parent_cx = global::get_text_map_propagator(|propagator| {
201 propagator.extract(&HeadersExtractor(headers))
202 });
203 _ = span.set_parent(parent_cx);
204
205 if let Some(traceparent) = super::get_traceparent(headers)
206 && let Some(trace_id) = parse_trace_id(traceparent)
207 {
208 span.record("trace_id", &trace_id);
209 span.record("parent.trace_id", &trace_id);
210 }
211 }
212
213 pub fn inject_current_span(headers: &mut HeaderMap) {
216 let cx = Context::current();
217 global::get_text_map_propagator(|propagator| {
218 propagator.inject_context(&cx, &mut HeadersInjector(headers));
219 });
220 }
221}
222
223#[cfg(not(feature = "otel"))]
226mod imp {
227 use super::parse_trace_id;
228 use http::HeaderMap;
229 use tracing::Span;
230
231 pub fn set_parent_from_headers(span: &Span, headers: &HeaderMap) {
235 if let Some(traceparent) = super::get_traceparent(headers)
236 && let Some(trace_id) = parse_trace_id(traceparent)
237 {
238 span.record("trace_id", &trace_id);
239 span.record("parent.trace_id", &trace_id);
240 }
241 }
242
243 pub fn inject_current_span(_headers: &mut HeaderMap) {}
245}
246
247pub use imp::{inject_current_span, set_parent_from_headers};
248
249pub const X_REQUEST_ID: &str = "x-request-id";
256
257tokio::task_local! {
258 static REQUEST_ID: Option<String>
260}
261
262#[must_use]
264pub fn extract_request_id(headers: &HeaderMap) -> Option<String> {
265 headers
266 .get(X_REQUEST_ID)
267 .and_then(|v| v.to_str().ok())
268 .map(str::to_owned)
269}
270
271#[must_use]
276pub fn current_request_id() -> Option<String> {
277 REQUEST_ID.try_with(Clone::clone).ok().flatten()
278}
279
280pub async fn with_request_id<F: Future>(id: Option<String>, f: F) -> F::Output {
289 REQUEST_ID.scope(id, f).await
290}
291
292pub fn inject_request_id(headers: &mut HeaderMap) {
298 if headers.contains_key(X_REQUEST_ID) {
299 return;
300 }
301 if let Some(id) = current_request_id()
302 && let Ok(value) = id.parse()
303 {
304 headers.insert(X_REQUEST_ID, value);
305 }
306}
307
308#[cfg(test)]
309#[cfg_attr(coverage_nightly, coverage(off))]
310mod tests {
311 use super::*;
312 use http::HeaderMap;
313 use tracing::info_span;
314
315 const TID: &str = "4bf92f3577b34da6a3ce929d0e0e4736";
316
317 #[test]
318 fn get_traceparent_none_when_absent() {
319 assert!(get_traceparent(&HeaderMap::new()).is_none());
320 }
321
322 #[test]
323 fn get_traceparent_returns_the_raw_value() {
324 let mut headers = HeaderMap::new();
325 headers.insert(
326 TRACEPARENT,
327 format!("00-{TID}-00f067aa0ba902b7-01")
328 .parse()
329 .expect("valid header"),
330 );
331 assert_eq!(
332 get_traceparent(&headers),
333 Some(format!("00-{TID}-00f067aa0ba902b7-01").as_str())
334 );
335 }
336
337 #[test]
338 fn parses_a_valid_version_00_header() {
339 assert_eq!(
340 parse_trace_id(&format!("00-{TID}-00f067aa0ba902b7-01")).as_deref(),
341 Some(TID)
342 );
343 }
344
345 #[test]
346 fn rejects_obviously_invalid_headers() {
347 assert!(parse_trace_id("invalid").is_none());
348 assert!(parse_trace_id("").is_none());
349 assert!(parse_trace_id("00---").is_none());
350 assert!(parse_trace_id("00-not-hex-at-all-x").is_none());
351 }
352
353 #[test]
354 fn rejects_a_parseable_but_invalid_segment() {
355 assert!(
356 parse_trace_id("00-00000000000000000000000000000000-00f067aa0ba902b7-01").is_none(),
357 "all-zero trace id is invalid per W3C"
358 );
359 assert!(
360 parse_trace_id("00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01").is_none(),
361 "uppercase hex is not the lowercase wire form"
362 );
363 }
364
365 #[test]
366 fn validates_the_whole_version_00_header() {
367 assert!(
368 parse_trace_id(&format!("00-{TID}-00f067aa0ba902b7-01-extra")).is_none(),
369 "a version-00 header must have exactly four fields"
370 );
371 assert!(
372 parse_trace_id(&format!("00-{TID}-00f067aa0ba902-01")).is_none(),
373 "short parent-id must be rejected"
374 );
375 assert!(
376 parse_trace_id(&format!("00-{TID}-zzzzzzzzzzzzzzzz-01")).is_none(),
377 "non-hex parent-id must be rejected"
378 );
379 assert!(
380 parse_trace_id(&format!("00-{TID}-0000000000000000-01")).is_none(),
381 "all-zero parent-id is invalid per W3C"
382 );
383 assert!(
384 parse_trace_id(&format!("00-{TID}-00f067aa0ba902b7-1")).is_none(),
385 "single-digit flags must be rejected"
386 );
387 assert!(
388 parse_trace_id(&format!("00-{TID}-00f067aa0ba902b7-zz")).is_none(),
389 "non-hex flags must be rejected"
390 );
391 }
392
393 #[tokio::test]
396 async fn request_id_is_visible_only_inside_the_scope() {
397 assert!(current_request_id().is_none());
398 let id = with_request_id(Some("req-1".to_owned()), async { current_request_id() }).await;
399 assert_eq!(id.as_deref(), Some("req-1"));
400 assert!(current_request_id().is_none());
401 }
402
403 #[tokio::test]
406 async fn inject_request_id_scoped_explicit_and_absent() {
407 let mut headers = HeaderMap::new();
408 inject_request_id(&mut headers);
409 assert!(
410 headers.is_empty(),
411 "outside a request scope no id is minted"
412 );
413
414 with_request_id(Some("req-9".to_owned()), async {
415 let mut scoped = HeaderMap::new();
416 inject_request_id(&mut scoped);
417 assert_eq!(scoped.get(X_REQUEST_ID).unwrap(), "req-9");
418
419 let mut explicit = HeaderMap::new();
420 explicit.insert(X_REQUEST_ID, "explicit".parse().expect("header"));
421 inject_request_id(&mut explicit);
422 assert_eq!(
423 explicit.get(X_REQUEST_ID).unwrap(),
424 "explicit",
425 "a caller-set header is never overwritten"
426 );
427 })
428 .await;
429 }
430
431 #[test]
434 fn extract_request_id_reads_the_header() {
435 let mut headers = HeaderMap::new();
436 assert!(extract_request_id(&headers).is_none());
437 headers.insert(X_REQUEST_ID, "req-7".parse().expect("header"));
438 assert_eq!(extract_request_id(&headers).as_deref(), Some("req-7"));
439 }
440
441 #[test]
444 fn set_parent_from_headers_records_ids_without_panicking() {
445 let mut headers = HeaderMap::new();
446 headers.insert(
447 TRACEPARENT,
448 format!("00-{TID}-00f067aa0ba902b7-01")
449 .parse()
450 .expect("valid header"),
451 );
452 let span = info_span!(
453 "test",
454 trace_id = tracing::field::Empty,
455 parent.trace_id = tracing::field::Empty
456 );
457 set_parent_from_headers(&span, &headers);
458 }
459
460 #[test]
463 #[cfg(feature = "otel")]
464 fn inject_current_span_does_not_panic() {
465 use opentelemetry::global;
466 use opentelemetry_sdk::propagation::TraceContextPropagator;
467
468 global::set_text_map_propagator(TraceContextPropagator::new());
469 let mut headers = HeaderMap::new();
470 let _span = info_span!("test").entered();
471 inject_current_span(&mut headers);
472 }
473
474 #[test]
476 #[cfg(not(feature = "otel"))]
477 fn inject_current_span_is_a_noop_without_otel() {
478 let mut headers = HeaderMap::new();
479 inject_current_span(&mut headers);
480 assert!(headers.is_empty());
481 }
482}
483
484#[cfg(test)]
488#[cfg(feature = "otel")]
489#[cfg_attr(coverage_nightly, coverage(off))]
490mod resolver_tests {
491 use super::{current_trace_id, extract_trace_id};
492 use http::HeaderMap;
493 use opentelemetry::trace::TracerProvider as _;
494 use tracing_subscriber::layer::SubscriberExt as _;
495
496 const OTHER_TRACE: &str = "0af7651916cd43dd8448eb211c80319c";
499
500 fn header(value: &str) -> HeaderMap {
501 let mut headers = HeaderMap::new();
502 headers.insert("traceparent", value.parse().expect("header value"));
503 headers
504 }
505
506 fn valid_header_from_another_trace() -> HeaderMap {
507 header(&format!("00-{OTHER_TRACE}-b7ad6b7169203331-01"))
508 }
509
510 fn in_active_span<R>(f: impl FnOnce() -> R) -> R {
513 let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder().build();
514 let tracer = provider.tracer("trace-context-test");
515 let subscriber =
516 tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer));
517 tracing::subscriber::with_default(subscriber, || tracing::info_span!("unit").in_scope(f))
518 }
519
520 #[test]
523 fn active_span_wins_over_a_valid_header_from_another_trace() {
524 in_active_span(|| {
525 let span_id = current_trace_id().expect("an active span has a trace id");
526 let resolved = extract_trace_id(&valid_header_from_another_trace());
527 assert_eq!(
528 resolved.as_deref(),
529 Some(span_id.as_str()),
530 "the live span's id, not the header's"
531 );
532 assert_ne!(
533 resolved.as_deref(),
534 Some(OTHER_TRACE),
535 "the header carried a different trace and must not win"
536 );
537 });
538 }
539
540 #[test]
544 fn invalid_header_inside_a_span_yields_the_span_id() {
545 in_active_span(|| {
546 let span_id = current_trace_id().expect("an active span has a trace id");
547 assert_eq!(
548 extract_trace_id(&header("not-a-valid-traceparent")).as_deref(),
549 Some(span_id.as_str())
550 );
551 });
552 }
553
554 #[test]
557 fn header_is_used_only_when_no_span_is_active() {
558 assert_eq!(
559 extract_trace_id(&valid_header_from_another_trace()).as_deref(),
560 Some(OTHER_TRACE)
561 );
562 assert!(extract_trace_id(&header("not-a-valid-traceparent")).is_none());
563 assert!(extract_trace_id(&HeaderMap::new()).is_none());
564 }
565}