opentelemetry_spanprocessor_any/sdk/propagation/
baggage.rs

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