opentelemetry_sdk/propagation/
baggage.rs

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