Skip to main content

opentelemetry_sdk/propagation/
trace_context.rs

1//! # W3C Trace Context Propagator
2//!
3
4use opentelemetry::{
5    propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator},
6    trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState},
7    Context,
8};
9use std::str::FromStr;
10use std::sync::OnceLock;
11
12const SUPPORTED_VERSION: u8 = 0;
13const MAX_VERSION: u8 = 254;
14const TRACEPARENT_HEADER: &str = "traceparent";
15const TRACESTATE_HEADER: &str = "tracestate";
16
17// TODO Replace this with LazyLock once it is stable.
18static TRACE_CONTEXT_HEADER_FIELDS: OnceLock<[String; 2]> = OnceLock::new();
19
20fn trace_context_header_fields() -> &'static [String; 2] {
21    TRACE_CONTEXT_HEADER_FIELDS
22        .get_or_init(|| [TRACEPARENT_HEADER.to_owned(), TRACESTATE_HEADER.to_owned()])
23}
24
25/// Propagates `SpanContext`s in [W3C TraceContext] format under `traceparent` and `tracestate` header.
26///
27/// The `traceparent` header represents the incoming request in a
28/// tracing system in a common format, understood by all vendors.
29/// Here’s an example of a `traceparent` header.
30///
31/// `traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01`
32///
33/// The `traceparent` HTTP header field identifies the incoming request in a
34/// tracing system. It has four fields:
35///
36///    - version
37///    - trace-id
38///    - parent-id
39///    - trace-flags
40///
41/// The `tracestate` header provides additional vendor-specific trace
42/// identification information across different distributed tracing systems.
43/// Here's an example of a `tracestate` header
44///
45/// `tracestate: vendorname1=opaqueValue1,vendorname2=opaqueValue2`
46///
47/// See the [w3c trace-context docs] for more details.
48///
49/// [w3c trace-context docs]: https://w3c.github.io/trace-context/
50/// [W3C TraceContext]: https://www.w3.org/TR/trace-context/
51#[derive(Clone, Debug, Default)]
52pub struct TraceContextPropagator {
53    _private: (),
54}
55
56impl TraceContextPropagator {
57    /// Create a new `TraceContextPropagator`.
58    pub fn new() -> Self {
59        TraceContextPropagator { _private: () }
60    }
61
62    /// Extract span context from w3c trace-context header.
63    fn extract_span_context(&self, extractor: &dyn Extractor) -> Result<SpanContext, ()> {
64        let header_value = extractor.get(TRACEPARENT_HEADER).unwrap_or("").trim();
65        let parts = header_value.split_terminator('-').collect::<Vec<&str>>();
66        // Ensure parts are not out of range.
67        if parts.len() < 4 {
68            return Err(());
69        }
70
71        // Ensure version is within range, for version 0 there must be 4 parts.
72        let version = u8::from_str_radix(parts[0], 16).map_err(|_| ())?;
73        if version > MAX_VERSION || version == 0 && parts.len() != 4 {
74            return Err(());
75        }
76
77        // Ensure trace id is lowercase
78        if parts[1].chars().any(|c| c.is_ascii_uppercase()) {
79            return Err(());
80        }
81
82        // Parse trace id section
83        let trace_id = TraceId::from_hex(parts[1]).map_err(|_| ())?;
84
85        // Ensure span id is lowercase
86        if parts[2].chars().any(|c| c.is_ascii_uppercase()) {
87            return Err(());
88        }
89
90        // Parse span id section
91        let span_id = SpanId::from_hex(parts[2]).map_err(|_| ())?;
92
93        // Ensure trace flags are lowercase
94        if parts[3].chars().any(|c| c.is_ascii_uppercase()) {
95            return Err(());
96        }
97
98        // Parse trace flags section
99        let opts = u8::from_str_radix(parts[3], 16).map_err(|_| ())?;
100
101        // Build trace flags clearing all flags other than the trace-context
102        // supported sampling bit. Unknown flags are accepted but zeroed out,
103        // as required by https://www.w3.org/TR/trace-context/#other-flags
104        let trace_flags = TraceFlags::new(opts) & TraceFlags::SAMPLED;
105
106        let trace_state = match extractor.get(TRACESTATE_HEADER) {
107            Some(trace_state_str) => {
108                TraceState::from_str(trace_state_str).unwrap_or_else(|_| TraceState::default())
109            }
110            None => TraceState::default(),
111        };
112
113        // create context
114        let span_context = SpanContext::new(trace_id, span_id, trace_flags, true, trace_state);
115
116        // Ensure span is valid
117        if !span_context.is_valid() {
118            return Err(());
119        }
120
121        Ok(span_context)
122    }
123}
124
125impl TextMapPropagator for TraceContextPropagator {
126    /// Properly encodes the values of the `SpanContext` and injects them
127    /// into the `Injector`.
128    fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
129        let span = cx.span();
130        let span_context = span.span_context();
131        if span_context.is_valid() {
132            let header_value = format!(
133                "{:02x}-{}-{}-{:02x}",
134                SUPPORTED_VERSION,
135                span_context.trace_id(),
136                span_context.span_id(),
137                span_context.trace_flags() & TraceFlags::SAMPLED
138            );
139            injector.set(TRACEPARENT_HEADER, header_value);
140            injector.set(TRACESTATE_HEADER, span_context.trace_state().header());
141        }
142    }
143
144    /// Retrieves encoded `SpanContext`s using the `Extractor`. It decodes
145    /// the `SpanContext` and returns it. If no `SpanContext` was retrieved
146    /// OR if the retrieved SpanContext is invalid then an empty `SpanContext`
147    /// is returned.
148    fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context {
149        self.extract_span_context(extractor)
150            .map(|sc| cx.with_remote_span_context(sc))
151            .unwrap_or_else(|_| cx.clone())
152    }
153
154    fn fields(&self) -> FieldIter<'_> {
155        FieldIter::new(trace_context_header_fields())
156    }
157}
158
159#[cfg(all(test, feature = "testing", feature = "trace"))]
160mod tests {
161    use super::*;
162    use crate::testing::trace::TestSpan;
163    use std::collections::HashMap;
164
165    #[rustfmt::skip]
166    fn extract_data() -> Vec<(&'static str, &'static str, SpanContext)> {
167        vec![
168            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::default(), true, TraceState::from_str("foo=bar").unwrap())),
169            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())),
170            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-03", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())),
171            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())),
172            ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())),
173            ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())),
174            ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-08", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::default(), true, TraceState::from_str("foo=bar").unwrap())),
175            ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09-XYZxsf09", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())),
176            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())),
177            ("01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09-", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())),
178        ]
179    }
180
181    #[rustfmt::skip]
182    fn extract_data_invalid() -> Vec<(&'static str, &'static str)> {
183        vec![
184            ("0000-00000000000000000000000000000000-0000000000000000-01", "wrong version length"),
185            ("00-ab00000000000000000000000000000000-cd00000000000000-01", "wrong trace ID length"),
186            ("00-ab000000000000000000000000000000-cd0000000000000000-01", "wrong span ID length"),
187            ("00-ab000000000000000000000000000000-cd00000000000000-0100", "wrong trace flag length"),
188            ("qw-00000000000000000000000000000000-0000000000000000-01",   "bogus version"),
189            ("00-qw000000000000000000000000000000-cd00000000000000-01",   "bogus trace ID"),
190            ("00-ab000000000000000000000000000000-qw00000000000000-01",   "bogus span ID"),
191            ("00-ab000000000000000000000000000000-cd00000000000000-qw",   "bogus trace flag"),
192            ("A0-00000000000000000000000000000000-0000000000000000-01",   "upper case version"),
193            ("00-AB000000000000000000000000000000-cd00000000000000-01",   "upper case trace ID"),
194            ("00-ab000000000000000000000000000000-CD00000000000000-01",   "upper case span ID"),
195            ("00-ab000000000000000000000000000000-cd00000000000000-A1",   "upper case trace flag"),
196            ("00-00000000000000000000000000000000-0000000000000000-01",   "zero trace ID and span ID"),
197            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7",      "missing options"),
198            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-",     "empty options"),
199        ]
200    }
201
202    #[rustfmt::skip]
203    fn inject_data() -> Vec<(&'static str, &'static str, SpanContext)> {
204        vec![
205            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())),
206            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::default(), true, TraceState::from_str("foo=bar").unwrap())),
207            ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::new(0xff), true, TraceState::from_str("foo=bar").unwrap())),
208            ("", "", SpanContext::empty_context()),
209        ]
210    }
211
212    #[test]
213    fn extract_w3c() {
214        let propagator = TraceContextPropagator::new();
215
216        for (trace_parent, trace_state, expected_context) in extract_data() {
217            let mut extractor = HashMap::new();
218            extractor.insert(TRACEPARENT_HEADER.to_string(), trace_parent.to_string());
219            extractor.insert(TRACESTATE_HEADER.to_string(), trace_state.to_string());
220
221            assert_eq!(
222                propagator.extract(&extractor).span().span_context(),
223                &expected_context
224            )
225        }
226    }
227
228    #[test]
229    fn extract_w3c_tracestate() {
230        let propagator = TraceContextPropagator::new();
231        let state = "foo=bar".to_string();
232        let parent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00".to_string();
233
234        let mut extractor = HashMap::new();
235        extractor.insert(TRACEPARENT_HEADER.to_string(), parent);
236        extractor.insert(TRACESTATE_HEADER.to_string(), state.clone());
237
238        assert_eq!(
239            propagator
240                .extract(&extractor)
241                .span()
242                .span_context()
243                .trace_state()
244                .header(),
245            state
246        )
247    }
248
249    #[test]
250    fn extract_w3c_reject_invalid() {
251        let propagator = TraceContextPropagator::new();
252
253        for (invalid_header, reason) in extract_data_invalid() {
254            let mut extractor = HashMap::new();
255            extractor.insert(TRACEPARENT_HEADER.to_string(), invalid_header.to_string());
256
257            assert_eq!(
258                propagator.extract(&extractor).span().span_context(),
259                &SpanContext::empty_context(),
260                "{reason}"
261            )
262        }
263    }
264
265    #[test]
266    fn inject_w3c() {
267        let propagator = TraceContextPropagator::new();
268
269        for (expected_trace_parent, expected_trace_state, context) in inject_data() {
270            let mut injector = HashMap::new();
271            propagator.inject_context(
272                &Context::current_with_span(TestSpan(context)),
273                &mut injector,
274            );
275
276            assert_eq!(
277                Extractor::get(&injector, TRACEPARENT_HEADER).unwrap_or(""),
278                expected_trace_parent
279            );
280
281            assert_eq!(
282                Extractor::get(&injector, TRACESTATE_HEADER).unwrap_or(""),
283                expected_trace_state
284            );
285        }
286    }
287
288    #[test]
289    fn inject_w3c_tracestate() {
290        let propagator = TraceContextPropagator::new();
291        let state = "foo=bar";
292
293        let mut injector: HashMap<String, String> = HashMap::new();
294        injector.set(TRACESTATE_HEADER, state.to_string());
295
296        Context::map_current(|cx| propagator.inject_context(cx, &mut injector));
297
298        assert_eq!(Extractor::get(&injector, TRACESTATE_HEADER), Some(state))
299    }
300}