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// TODO Replace this with LazyLock once it is stable.
15static BAGGAGE_FIELDS: OnceLock<[String; 1]> = OnceLock::new();
16#[inline]
17fn baggage_fields() -> &'static [String; 1] {
18    BAGGAGE_FIELDS.get_or_init(|| [BAGGAGE_HEADER.to_owned()])
19}
20
21/// Propagates name-value pairs in [W3C Baggage] format.
22///
23/// Baggage is used to annotate telemetry, adding context and
24/// information to metrics, traces, and logs. It is an abstract data type
25/// represented by a set of name-value pairs describing user-defined properties.
26/// Each name in a [`Baggage`] is associated with exactly one value.
27/// `Baggage`s are serialized according to the editor's draft of
28/// the [W3C Baggage] specification.
29///
30/// # Examples
31///
32/// ```
33/// use opentelemetry::{baggage::{Baggage, BaggageExt}, propagation::TextMapPropagator};
34/// use opentelemetry_sdk::propagation::BaggagePropagator;
35/// use std::collections::HashMap;
36///
37/// // Example baggage value passed in externally via http headers
38/// let mut headers = HashMap::new();
39/// headers.insert("baggage".to_string(), "user_id=1".to_string());
40///
41/// let propagator = BaggagePropagator::new();
42/// // can extract from any type that impls `Extractor`, usually an HTTP header map
43/// let cx = propagator.extract(&headers);
44///
45/// // Iterate over extracted name-value pairs
46/// for (name, value) in cx.baggage() {
47///     // ...
48/// }
49///
50/// // Add new baggage
51/// let mut baggage = Baggage::new();
52/// let _ = baggage.insert("server_id", "42");
53///
54/// let cx_with_additions = cx.with_baggage(baggage);
55///
56/// // Inject baggage into http request
57/// propagator.inject_context(&cx_with_additions, &mut headers);
58///
59/// let header_value = headers.get("baggage").expect("header is injected");
60/// assert!(!header_value.contains("user_id=1"), "still contains previous name-value");
61/// assert!(header_value.contains("server_id=42"), "does not contain new name-value pair");
62/// ```
63///
64/// [W3C Baggage]: https://w3c.github.io/baggage
65/// [`Baggage`]: opentelemetry::baggage::Baggage
66#[derive(Debug, Default)]
67pub struct BaggagePropagator {
68    _private: (),
69}
70
71impl BaggagePropagator {
72    /// Construct a new baggage propagator.
73    pub fn new() -> Self {
74        BaggagePropagator { _private: () }
75    }
76}
77
78impl TextMapPropagator for BaggagePropagator {
79    /// Encodes the values of the `Context` and injects them into the provided `Injector`.
80    fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
81        let baggage = cx.baggage();
82        if !baggage.is_empty() {
83            let header_value = baggage
84                .iter()
85                .map(|(name, (value, metadata))| {
86                    let metadata_str = metadata.as_str().trim();
87                    let metadata_prefix = if metadata_str.is_empty() { "" } else { ";" };
88                    utf8_percent_encode(name.as_str().trim(), FRAGMENT)
89                        .chain(iter::once("="))
90                        .chain(utf8_percent_encode(value.as_str().trim(), FRAGMENT))
91                        .chain(iter::once(metadata_prefix))
92                        .chain(iter::once(metadata_str))
93                        .collect()
94                })
95                .collect::<Vec<String>>()
96                .join(",");
97            injector.set(BAGGAGE_HEADER, header_value);
98        }
99    }
100
101    /// Extracts a `Context` with baggage values from a `Extractor`.
102    fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context {
103        if let Some(header_value) = extractor.get(BAGGAGE_HEADER) {
104            let baggage = header_value.split(',').filter_map(|context_value| {
105                if let Some((name_and_value, props)) = context_value
106                    .split(';')
107                    .collect::<Vec<&str>>()
108                    .split_first()
109                {
110                    let mut iter = name_and_value.split('=');
111                    if let (Some(name), Some(value)) = (iter.next(), iter.next()) {
112                        let decode_name = percent_decode_str(name).decode_utf8();
113                        let decode_value = percent_decode_str(value).decode_utf8();
114
115                        if let (Ok(name), Ok(value)) = (decode_name, decode_value) {
116                            // Here we don't store the first ; into baggage since it should be treated
117                            // as separator rather part of metadata
118                            let decoded_props = props
119                                .iter()
120                                .flat_map(|prop| percent_decode_str(prop).decode_utf8())
121                                .map(|prop| prop.trim().to_string())
122                                .collect::<Vec<String>>()
123                                .join(";"); // join with ; because we deleted all ; when calling split above
124
125                            Some(KeyValueMetadata::new(
126                                name.trim().to_owned(),
127                                value.trim().to_string(),
128                                decoded_props.as_str(),
129                            ))
130                        } else {
131                            otel_warn!(
132                                name: "BaggagePropagator.Extract.InvalidUTF8",
133                                message = "Invalid UTF8 string in key values",
134                                baggage_header = header_value,
135                            );
136                            None
137                        }
138                    } else {
139                        otel_warn!(
140                            name: "BaggagePropagator.Extract.InvalidKeyValueFormat",
141                            message = "Invalid baggage key-value format",
142                            baggage_header = header_value,
143                        );
144                        None
145                    }
146                } else {
147                    otel_warn!(
148                        name: "BaggagePropagator.Extract.InvalidFormat",
149                        message = "Invalid baggage format",
150                        baggage_header = header_value);
151                    None
152                }
153            });
154            cx.with_baggage(baggage)
155        } else {
156            cx.clone()
157        }
158    }
159
160    fn fields(&self) -> FieldIter<'_> {
161        FieldIter::new(baggage_fields())
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use opentelemetry::{baggage::BaggageMetadata, Key, KeyValue, StringValue, Value};
169    use std::collections::HashMap;
170
171    #[rustfmt::skip]
172    fn valid_extract_data() -> Vec<(&'static str, HashMap<Key, StringValue>)> {
173        vec![
174            // "valid w3cHeader"
175            ("key1=val1,key2=val2", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
176            // "valid w3cHeader with spaces"
177            ("key1 =   val1,  key2 =val2   ", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
178            // "valid header with url-escaped comma"
179            ("key1=val1,key2=val2%2Cval3", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2,val3"))].into_iter().collect()),
180            // "valid header with an invalid header"
181            ("key1=val1,key2=val2,a,val3", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
182            // "valid header with no value"
183            ("key1=,key2=val2", vec![(Key::new("key1"), StringValue::from("")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
184        ]
185    }
186
187    #[rustfmt::skip]
188    #[allow(clippy::type_complexity)]
189    fn valid_extract_data_with_metadata() -> Vec<(&'static str, HashMap<Key, (StringValue, BaggageMetadata)>)> {
190        vec![
191            // "valid w3cHeader with properties"
192            ("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()),
193            // prop can don't need to be key value pair
194            ("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()),
195            ("key1=value1;property1;property2, key2 = value2, key3=value3; propertyKey=propertyValue",
196             vec![
197                 (Key::new("key1"), (StringValue::from("value1"), BaggageMetadata::from("property1;property2"))),
198                 (Key::new("key2"), (StringValue::from("value2"), BaggageMetadata::default())),
199                 (Key::new("key3"), (StringValue::from("value3"), BaggageMetadata::from("propertyKey=propertyValue"))),
200             ].into_iter().collect()),
201        ]
202    }
203
204    #[rustfmt::skip]
205    fn valid_inject_data() -> Vec<(Vec<KeyValue>, Vec<&'static str>)> {
206        vec![
207            // "two simple values"
208            (vec![KeyValue::new("key1", "val1"), KeyValue::new("key2", "val2")], vec!["key1=val1", "key2=val2"]),
209            // "two values with escaped chars"
210            (vec![KeyValue::new("key1", "val1,val2"), KeyValue::new("key2", "val3=4")], vec!["key1=val1%2Cval2", "key2=val3%3D4"]),
211            // "values of non-string non-array types"
212            (
213                vec![
214                    KeyValue::new("key1", true),
215                    KeyValue::new("key2", Value::I64(123)),
216                    KeyValue::new("key3", Value::F64(123.567)),
217                ],
218                vec![
219                    "key1=true",
220                    "key2=123",
221                    "key3=123.567",
222                ],
223            ),
224            // "values of array types"
225            (
226                vec![
227                    KeyValue::new("key1", Value::Array(vec![true, false].into())),
228                    KeyValue::new("key2", Value::Array(vec![123, 456].into())),
229                    KeyValue::new("key3", Value::Array(vec![StringValue::from("val1"), StringValue::from("val2")].into())),
230                ],
231                vec![
232                    "key1=[true%2Cfalse]",
233                    "key2=[123%2C456]",
234                    "key3=[%22val1%22%2C%22val2%22]",
235                ],
236            ),
237        ]
238    }
239
240    #[rustfmt::skip]
241    fn valid_inject_data_metadata() -> Vec<(Vec<KeyValueMetadata>, Vec<&'static str>)> {
242        vec![
243            (
244                vec![
245                    KeyValueMetadata::new("key1", "val1", "prop1"),
246                    KeyValue::new("key2", "val2").into(),
247                    KeyValueMetadata::new("key3", "val3", "anykey=anyvalue"),
248                ],
249                vec![
250                    "key1=val1;prop1",
251                    "key2=val2",
252                    "key3=val3;anykey=anyvalue",
253                ],
254            )
255        ]
256    }
257
258    #[test]
259    fn extract_baggage() {
260        let propagator = BaggagePropagator::new();
261
262        for (header_value, kvs) in valid_extract_data() {
263            let mut extractor: HashMap<String, String> = HashMap::new();
264            extractor.insert(BAGGAGE_HEADER.to_string(), header_value.to_string());
265            let context = propagator.extract(&extractor);
266            let baggage = context.baggage();
267
268            assert_eq!(kvs.len(), baggage.len());
269            for (key, (value, _metadata)) in baggage {
270                assert_eq!(Some(value), kvs.get(key))
271            }
272        }
273    }
274
275    #[test]
276    fn inject_baggage() {
277        let propagator = BaggagePropagator::new();
278
279        for (kvm, header_parts) in valid_inject_data() {
280            let mut injector = HashMap::new();
281            let cx = Context::current_with_baggage(kvm);
282            propagator.inject_context(&cx, &mut injector);
283            let header_value = injector.get(BAGGAGE_HEADER).unwrap();
284            assert_eq!(header_parts.join(",").len(), header_value.len(),);
285            for header_part in &header_parts {
286                assert!(header_value.contains(header_part),)
287            }
288        }
289    }
290
291    #[test]
292    fn extract_baggage_with_metadata() {
293        let propagator = BaggagePropagator::new();
294        for (header_value, kvm) in valid_extract_data_with_metadata() {
295            let mut extractor: HashMap<String, String> = HashMap::new();
296            extractor.insert(BAGGAGE_HEADER.to_string(), header_value.to_string());
297            let context = propagator.extract(&extractor);
298            let baggage = context.baggage();
299
300            assert_eq!(kvm.len(), baggage.len());
301            for (key, value_and_prop) in baggage {
302                assert_eq!(Some(value_and_prop), kvm.get(key))
303            }
304        }
305    }
306
307    #[test]
308    fn inject_baggage_with_metadata() {
309        let propagator = BaggagePropagator::new();
310
311        for (kvm, header_parts) in valid_inject_data_metadata() {
312            let mut injector = HashMap::new();
313            let cx = Context::current_with_baggage(kvm);
314            propagator.inject_context(&cx, &mut injector);
315            let header_value = injector.get(BAGGAGE_HEADER).unwrap();
316
317            assert_eq!(header_parts.join(",").len(), header_value.len());
318            for header_part in &header_parts {
319                assert!(header_value.contains(header_part),)
320            }
321        }
322    }
323}