Skip to main content

opentelemetry_sdk/propagation/
baggage.rs

1use opentelemetry::{
2    baggage::{BaggageExt, KeyValueMetadata},
3    otel_warn,
4    propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator},
5    Context,
6};
7use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS};
8use std::iter;
9use std::sync::OnceLock;
10
11static BAGGAGE_HEADER: &str = "baggage";
12const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b';').add(b',').add(b'=');
13
14// W3C Baggage specification limits.
15// See https://www.w3.org/TR/baggage/#limits
16const MAX_BAGGAGE_LENGTH: usize = 8192;
17const MAX_BAGGAGE_ITEMS: usize = 64;
18
19// TODO Replace this with LazyLock once it is stable.
20static BAGGAGE_FIELDS: OnceLock<[String; 1]> = OnceLock::new();
21#[inline]
22fn baggage_fields() -> &'static [String; 1] {
23    BAGGAGE_FIELDS.get_or_init(|| [BAGGAGE_HEADER.to_owned()])
24}
25
26/// Propagates name-value pairs in [W3C Baggage] format.
27///
28/// Baggage is used to annotate telemetry, adding context and
29/// information to metrics, traces, and logs. It is an abstract data type
30/// represented by a set of name-value pairs describing user-defined properties.
31/// Each name in a [`Baggage`] is associated with exactly one value.
32/// `Baggage`s are serialized according to the editor's draft of
33/// the [W3C Baggage] specification.
34///
35/// # Examples
36///
37/// ```
38/// use opentelemetry::{baggage::{Baggage, BaggageExt}, propagation::TextMapPropagator};
39/// use opentelemetry_sdk::propagation::BaggagePropagator;
40/// use std::collections::HashMap;
41///
42/// // Example baggage value passed in externally via http headers
43/// let mut headers = HashMap::new();
44/// headers.insert("baggage".to_string(), "user_id=1".to_string());
45///
46/// let propagator = BaggagePropagator::new();
47/// // can extract from any type that impls `Extractor`, usually an HTTP header map
48/// let cx = propagator.extract(&headers);
49///
50/// // Iterate over extracted name-value pairs
51/// for (name, value) in cx.baggage() {
52///     // ...
53/// }
54///
55/// // Add new baggage
56/// let mut baggage = Baggage::new();
57/// let _ = baggage.insert("server_id", "42");
58///
59/// let cx_with_additions = cx.with_baggage(baggage);
60///
61/// // Inject baggage into http request
62/// propagator.inject_context(&cx_with_additions, &mut headers);
63///
64/// let header_value = headers.get("baggage").expect("header is injected");
65/// assert!(!header_value.contains("user_id=1"), "still contains previous name-value");
66/// assert!(header_value.contains("server_id=42"), "does not contain new name-value pair");
67/// ```
68///
69/// [W3C Baggage]: https://w3c.github.io/baggage
70/// [`Baggage`]: opentelemetry::baggage::Baggage
71#[derive(Debug, Default)]
72pub struct BaggagePropagator {
73    _private: (),
74}
75
76impl BaggagePropagator {
77    /// Construct a new baggage propagator.
78    pub fn new() -> Self {
79        BaggagePropagator { _private: () }
80    }
81}
82
83impl TextMapPropagator for BaggagePropagator {
84    /// Encodes the values of the `Context` and injects them into the provided `Injector`.
85    fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
86        let baggage = cx.baggage();
87        if !baggage.is_empty() {
88            let header_value = baggage
89                .iter()
90                .map(|(name, (value, metadata))| {
91                    let metadata_str = metadata.as_str().trim();
92                    let metadata_prefix = if metadata_str.is_empty() { "" } else { ";" };
93                    utf8_percent_encode(name.as_str().trim(), FRAGMENT)
94                        .chain(iter::once("="))
95                        .chain(utf8_percent_encode(value.as_str().trim(), FRAGMENT))
96                        .chain(iter::once(metadata_prefix))
97                        .chain(iter::once(metadata_str))
98                        .collect()
99                })
100                .collect::<Vec<String>>()
101                .join(",");
102            injector.set(BAGGAGE_HEADER, header_value);
103        }
104    }
105
106    /// Extracts a `Context` with baggage values from a `Extractor`.
107    fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context {
108        if let Some(header_value) = extractor.get(BAGGAGE_HEADER) {
109            // Enforce the W3C Baggage maximum header length up-front so a
110            // single oversize header cannot drive per-entry allocation work
111            // before the entries are dropped on insert. See
112            // https://www.w3.org/TR/baggage/#limits.
113            if header_value.len() > MAX_BAGGAGE_LENGTH {
114                otel_warn!(
115                    name: "BaggagePropagator.Extract.HeaderTooLarge",
116                    message = "Baggage header exceeds W3C maximum length and was dropped",
117                    header_bytes = header_value.len() as i64,
118                    limit_bytes = MAX_BAGGAGE_LENGTH as i64,
119                );
120                return cx.clone();
121            }
122
123            let baggage =
124                header_value
125                    .split(',')
126                    .take(MAX_BAGGAGE_ITEMS)
127                    .filter_map(|context_value| {
128                        if let Some((name_and_value, props)) = context_value
129                            .split(';')
130                            .collect::<Vec<&str>>()
131                            .split_first()
132                        {
133                            let mut iter = name_and_value.split('=');
134                            if let (Some(name), Some(value)) = (iter.next(), iter.next()) {
135                                let decode_name = percent_decode_str(name).decode_utf8();
136                                let decode_value = percent_decode_str(value).decode_utf8();
137
138                                if let (Ok(name), Ok(value)) = (decode_name, decode_value) {
139                                    // Here we don't store the first ; into baggage since it should be treated
140                                    // as separator rather part of metadata
141                                    let decoded_props = props
142                                        .iter()
143                                        .flat_map(|prop| percent_decode_str(prop).decode_utf8())
144                                        .map(|prop| prop.trim().to_string())
145                                        .collect::<Vec<String>>()
146                                        .join(";"); // join with ; because we deleted all ; when calling split above
147
148                                    Some(KeyValueMetadata::new(
149                                        name.trim().to_owned(),
150                                        value.trim().to_string(),
151                                        decoded_props.as_str(),
152                                    ))
153                                } else {
154                                    otel_warn!(
155                                        name: "BaggagePropagator.Extract.InvalidUTF8",
156                                        message = "Invalid UTF8 string in key values",
157                                        baggage_header = header_value,
158                                    );
159                                    None
160                                }
161                            } else {
162                                otel_warn!(
163                                    name: "BaggagePropagator.Extract.InvalidKeyValueFormat",
164                                    message = "Invalid baggage key-value format",
165                                    baggage_header = header_value,
166                                );
167                                None
168                            }
169                        } else {
170                            otel_warn!(
171                        name: "BaggagePropagator.Extract.InvalidFormat",
172                        message = "Invalid baggage format",
173                        baggage_header = header_value);
174                            None
175                        }
176                    });
177            cx.with_baggage(baggage)
178        } else {
179            cx.clone()
180        }
181    }
182
183    fn fields(&self) -> FieldIter<'_> {
184        FieldIter::new(baggage_fields())
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use opentelemetry::{baggage::BaggageMetadata, Key, KeyValue, StringValue, Value};
192    use std::collections::HashMap;
193
194    #[rustfmt::skip]
195    fn valid_extract_data() -> Vec<(&'static str, HashMap<Key, StringValue>)> {
196        vec![
197            // "valid w3cHeader"
198            ("key1=val1,key2=val2", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
199            // "valid w3cHeader with spaces"
200            ("key1 =   val1,  key2 =val2   ", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
201            // "valid header with url-escaped comma"
202            ("key1=val1,key2=val2%2Cval3", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2,val3"))].into_iter().collect()),
203            // "valid header with an invalid header"
204            ("key1=val1,key2=val2,a,val3", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
205            // "valid header with no value"
206            ("key1=,key2=val2", vec![(Key::new("key1"), StringValue::from("")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
207        ]
208    }
209
210    #[rustfmt::skip]
211    #[allow(clippy::type_complexity)]
212    fn valid_extract_data_with_metadata() -> Vec<(&'static str, HashMap<Key, (StringValue, BaggageMetadata)>)> {
213        vec![
214            // "valid w3cHeader with properties"
215            ("key1=val1,key2=val2;prop=1", vec![(Key::new("key1"), (StringValue::from("val1"), BaggageMetadata::default())), (Key::new("key2"), (StringValue::from("val2"), BaggageMetadata::from("prop=1")))].into_iter().collect()),
216            // prop can don't need to be key value pair
217            ("key1=val1,key2=val2;prop1", vec![(Key::new("key1"), (StringValue::from("val1"), BaggageMetadata::default())), (Key::new("key2"), (StringValue::from("val2"), BaggageMetadata::from("prop1")))].into_iter().collect()),
218            ("key1=value1;property1;property2, key2 = value2, key3=value3; propertyKey=propertyValue",
219             vec![
220                 (Key::new("key1"), (StringValue::from("value1"), BaggageMetadata::from("property1;property2"))),
221                 (Key::new("key2"), (StringValue::from("value2"), BaggageMetadata::default())),
222                 (Key::new("key3"), (StringValue::from("value3"), BaggageMetadata::from("propertyKey=propertyValue"))),
223             ].into_iter().collect()),
224        ]
225    }
226
227    #[rustfmt::skip]
228    fn valid_inject_data() -> Vec<(Vec<KeyValue>, Vec<&'static str>)> {
229        vec![
230            // "two simple values"
231            (vec![KeyValue::new("key1", "val1"), KeyValue::new("key2", "val2")], vec!["key1=val1", "key2=val2"]),
232            // "two values with escaped chars"
233            (vec![KeyValue::new("key1", "val1,val2"), KeyValue::new("key2", "val3=4")], vec!["key1=val1%2Cval2", "key2=val3%3D4"]),
234            // "values of non-string non-array types"
235            (
236                vec![
237                    KeyValue::new("key1", true),
238                    KeyValue::new("key2", Value::I64(123)),
239                    KeyValue::new("key3", Value::F64(123.567)),
240                ],
241                vec![
242                    "key1=true",
243                    "key2=123",
244                    "key3=123.567",
245                ],
246            ),
247            // "values of array types"
248            (
249                vec![
250                    KeyValue::new("key1", Value::Array(vec![true, false].into())),
251                    KeyValue::new("key2", Value::Array(vec![123, 456].into())),
252                    KeyValue::new("key3", Value::Array(vec![StringValue::from("val1"), StringValue::from("val2")].into())),
253                ],
254                vec![
255                    "key1=[true%2Cfalse]",
256                    "key2=[123%2C456]",
257                    "key3=[%22val1%22%2C%22val2%22]",
258                ],
259            ),
260        ]
261    }
262
263    #[rustfmt::skip]
264    fn valid_inject_data_metadata() -> Vec<(Vec<KeyValueMetadata>, Vec<&'static str>)> {
265        vec![
266            (
267                vec![
268                    KeyValueMetadata::new("key1", "val1", "prop1"),
269                    KeyValue::new("key2", "val2").into(),
270                    KeyValueMetadata::new("key3", "val3", "anykey=anyvalue"),
271                ],
272                vec![
273                    "key1=val1;prop1",
274                    "key2=val2",
275                    "key3=val3;anykey=anyvalue",
276                ],
277            )
278        ]
279    }
280
281    #[test]
282    fn extract_baggage() {
283        let propagator = BaggagePropagator::new();
284
285        for (header_value, kvs) in valid_extract_data() {
286            let mut extractor: HashMap<String, String> = HashMap::new();
287            extractor.insert(BAGGAGE_HEADER.to_string(), header_value.to_string());
288            let context = propagator.extract(&extractor);
289            let baggage = context.baggage();
290
291            assert_eq!(kvs.len(), baggage.len());
292            for (key, (value, _metadata)) in baggage {
293                assert_eq!(Some(value), kvs.get(key))
294            }
295        }
296    }
297
298    #[test]
299    fn inject_baggage() {
300        let propagator = BaggagePropagator::new();
301
302        for (kvm, header_parts) in valid_inject_data() {
303            let mut injector = HashMap::new();
304            let cx = Context::current_with_baggage(kvm);
305            propagator.inject_context(&cx, &mut injector);
306            let header_value = injector.get(BAGGAGE_HEADER).unwrap();
307            assert_eq!(header_parts.join(",").len(), header_value.len(),);
308            for header_part in &header_parts {
309                assert!(header_value.contains(header_part),)
310            }
311        }
312    }
313
314    #[test]
315    fn extract_baggage_with_metadata() {
316        let propagator = BaggagePropagator::new();
317        for (header_value, kvm) in valid_extract_data_with_metadata() {
318            let mut extractor: HashMap<String, String> = HashMap::new();
319            extractor.insert(BAGGAGE_HEADER.to_string(), header_value.to_string());
320            let context = propagator.extract(&extractor);
321            let baggage = context.baggage();
322
323            assert_eq!(kvm.len(), baggage.len());
324            for (key, value_and_prop) in baggage {
325                assert_eq!(Some(value_and_prop), kvm.get(key))
326            }
327        }
328    }
329
330    #[test]
331    fn inject_baggage_with_metadata() {
332        let propagator = BaggagePropagator::new();
333
334        for (kvm, header_parts) in valid_inject_data_metadata() {
335            let mut injector = HashMap::new();
336            let cx = Context::current_with_baggage(kvm);
337            propagator.inject_context(&cx, &mut injector);
338            let header_value = injector.get(BAGGAGE_HEADER).unwrap();
339
340            assert_eq!(header_parts.join(",").len(), header_value.len());
341            for header_part in &header_parts {
342                assert!(header_value.contains(header_part),)
343            }
344        }
345    }
346
347    #[test]
348    fn extract_drops_header_exceeding_max_length() {
349        let propagator = BaggagePropagator::new();
350
351        // Build a syntactically valid header longer than the W3C 8192-byte
352        // limit. The header must be rejected outright; no entries are added.
353        let mut header = String::with_capacity(MAX_BAGGAGE_LENGTH + 1024);
354        let mut i = 0u32;
355        while header.len() < MAX_BAGGAGE_LENGTH + 256 {
356            if !header.is_empty() {
357                header.push(',');
358            }
359            header.push_str(&format!("k{i}=v{i}"));
360            i += 1;
361        }
362        assert!(header.len() > MAX_BAGGAGE_LENGTH);
363
364        let mut extractor: HashMap<String, String> = HashMap::new();
365        extractor.insert(BAGGAGE_HEADER.to_string(), header);
366
367        let context = propagator.extract(&extractor);
368        assert_eq!(context.baggage().len(), 0);
369    }
370
371    #[test]
372    fn extract_caps_entry_count_at_max_baggage_items() {
373        let propagator = BaggagePropagator::new();
374
375        // Build a header whose total size stays under MAX_BAGGAGE_LENGTH but
376        // contains more than MAX_BAGGAGE_ITEMS entries.
377        let mut header = String::new();
378        let entries = MAX_BAGGAGE_ITEMS + 32;
379        for i in 0..entries {
380            if i > 0 {
381                header.push(',');
382            }
383            header.push_str(&format!("k{i:03}=v"));
384        }
385        assert!(header.len() <= MAX_BAGGAGE_LENGTH);
386
387        let mut extractor: HashMap<String, String> = HashMap::new();
388        extractor.insert(BAGGAGE_HEADER.to_string(), header);
389
390        let context = propagator.extract(&extractor);
391        // The storage type also caps at MAX_KEY_VALUE_PAIRS=64; the propagator
392        // additionally stops parsing at MAX_BAGGAGE_ITEMS so allocator work
393        // does not grow with attacker-supplied entry counts.
394        assert_eq!(context.baggage().len(), MAX_BAGGAGE_ITEMS);
395    }
396}