Skip to main content

opentelemetry/
baggage.rs

1//! Primitives for sending name/value data across system boundaries.
2//!
3//! Baggage is used to annotate telemetry, adding context and information to
4//! metrics, traces, and logs. It is a set of name/value pairs describing
5//! user-defined properties. Each name in Baggage is associated with exactly one
6//! value.
7//!
8//! Main types in this module are:
9//!
10//! * [`Baggage`]: A set of name/value pairs describing user-defined properties.
11//! * [`BaggageExt`]: Extensions for managing `Baggage` in a [`Context`].
12//!
13//! Baggage can be sent between systems using a baggage propagator in
14//! accordance with the [W3C Baggage] specification.
15//!
16//! Note: Baggage is not automatically added to any telemetry. Users have to
17//! explicitly add baggage entries to telemetry items.
18//!
19//!
20//! [W3C Baggage]: https://w3c.github.io/baggage
21use crate::{Context, Key, KeyValue, StringValue};
22use std::collections::hash_map::Entry;
23use std::collections::{hash_map, HashMap};
24use std::fmt;
25use std::sync::OnceLock;
26
27static DEFAULT_BAGGAGE: OnceLock<Baggage> = OnceLock::new();
28
29const MAX_KEY_VALUE_PAIRS: usize = 64;
30const MAX_LEN_OF_ALL_PAIRS: usize = 8192;
31
32// https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
33const INVALID_ASCII_KEY_CHARS: [u8; 17] = *b"(),/:;<=>?@[\\]{}\"";
34
35/// Returns the default baggage, ensuring it is initialized only once.
36#[inline]
37fn get_default_baggage() -> &'static Baggage {
38    DEFAULT_BAGGAGE.get_or_init(Baggage::default)
39}
40
41/// A set of name/value pairs describing user-defined properties.
42///
43/// ### Baggage Names
44///
45/// * ASCII strings according to the token format, defined in [RFC2616, Section 2.2]
46///
47/// ### Baggage Values
48///
49/// * URL encoded UTF-8 strings.
50///
51/// ### Baggage Value Metadata
52///
53/// Additional metadata can be added to values in the form of a property set,
54/// represented as semi-colon `;` delimited list of names and/or name/value pairs,
55/// e.g. `;k1=v1;k2;k3=v3`.
56///
57/// ### Limits
58///
59/// * Maximum number of name/value pairs: `64`.
60/// * Maximum total length of all name/value pairs: `8192`.
61///
62/// <https://www.w3.org/TR/baggage/#limits>
63#[derive(Debug, Default)]
64pub struct Baggage {
65    inner: HashMap<Key, (StringValue, BaggageMetadata)>,
66    kv_content_len: usize, // the length of key-value-metadata string in `inner`
67}
68
69impl Baggage {
70    /// Creates an empty `Baggage`.
71    pub fn new() -> Self {
72        Baggage {
73            inner: HashMap::default(),
74            kv_content_len: 0,
75        }
76    }
77
78    /// Returns a reference to the value associated with a given name
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use opentelemetry::{baggage::Baggage, StringValue};
84    ///
85    /// let mut baggage = Baggage::new();
86    /// let _ = baggage.insert("my-name", "my-value");
87    ///
88    /// assert_eq!(baggage.get("my-name"), Some(&StringValue::from("my-value")))
89    /// ```
90    pub fn get<K: AsRef<str>>(&self, key: K) -> Option<&StringValue> {
91        self.inner.get(key.as_ref()).map(|(value, _metadata)| value)
92    }
93
94    /// Returns a reference to the value and metadata associated with a given name
95    ///
96    /// # Examples
97    /// ```
98    /// use opentelemetry::{baggage::{Baggage, BaggageMetadata}, StringValue};
99    ///
100    /// let mut baggage = Baggage::new();
101    /// let _ = baggage.insert("my-name", "my-value");
102    ///
103    /// // By default, the metadata is empty
104    /// assert_eq!(baggage.get_with_metadata("my-name"), Some(&(StringValue::from("my-value"), BaggageMetadata::from(""))))
105    /// ```
106    pub fn get_with_metadata<K: AsRef<str>>(
107        &self,
108        key: K,
109    ) -> Option<&(StringValue, BaggageMetadata)> {
110        self.inner.get(key.as_ref())
111    }
112
113    /// Inserts a name/value pair into the baggage.
114    ///
115    /// If the name was not present, [`None`] is returned. If the name was present,
116    /// the value is updated, and the old value is returned.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use opentelemetry::{baggage::Baggage, StringValue};
122    ///
123    /// let mut baggage = Baggage::new();
124    /// let _ = baggage.insert("my-name", "my-value");
125    ///
126    /// assert_eq!(baggage.get("my-name"), Some(&StringValue::from("my-value")))
127    /// ```
128    pub fn insert<K, V>(&mut self, key: K, value: V) -> Option<StringValue>
129    where
130        K: Into<Key>,
131        V: Into<StringValue>,
132    {
133        self.insert_with_metadata(key, value, BaggageMetadata::default())
134            .map(|pair| pair.0)
135    }
136
137    /// Inserts a name/value(+metadata) pair into the baggage.
138    ///
139    /// Same with `insert`, if the name was not present, [`None`] will be returned.
140    /// If the name is present, the old value and metadata will be returned.
141    ///
142    /// Also checks for [limits](https://w3c.github.io/baggage/#limits).
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use opentelemetry::{baggage::{Baggage, BaggageMetadata}, StringValue};
148    ///
149    /// let mut baggage = Baggage::new();
150    /// let _ = baggage.insert_with_metadata("my-name", "my-value", "test");
151    ///
152    /// assert_eq!(baggage.get_with_metadata("my-name"), Some(&(StringValue::from("my-value"), BaggageMetadata::from("test"))))
153    /// ```
154    pub fn insert_with_metadata<K, V, S>(
155        &mut self,
156        key: K,
157        value: V,
158        metadata: S,
159    ) -> Option<(StringValue, BaggageMetadata)>
160    where
161        K: Into<Key>,
162        V: Into<StringValue>,
163        S: Into<BaggageMetadata>,
164    {
165        let (key, value, metadata) = (key.into(), value.into(), metadata.into());
166        let entries_count = self.inner.len();
167        match self.inner.entry(key) {
168            Entry::Occupied(mut occupied_entry) => {
169                let key_str = occupied_entry.key().as_str();
170                let entry_content_len =
171                    key_value_metadata_bytes_size(key_str, value.as_str(), metadata.as_str());
172                let prev_content_len = key_value_metadata_bytes_size(
173                    key_str,
174                    occupied_entry.get().0.as_str(),
175                    occupied_entry.get().1.as_str(),
176                );
177                let new_content_len = self.kv_content_len + entry_content_len - prev_content_len;
178                if new_content_len > MAX_LEN_OF_ALL_PAIRS {
179                    return None;
180                }
181                self.kv_content_len = new_content_len;
182                Some(occupied_entry.insert((value, metadata)))
183            }
184            Entry::Vacant(vacant_entry) => {
185                let key_str = vacant_entry.key().as_str();
186                if !Self::is_key_valid(key_str.as_bytes()) {
187                    return None;
188                }
189                if entries_count == MAX_KEY_VALUE_PAIRS {
190                    return None;
191                }
192                let entry_content_len =
193                    key_value_metadata_bytes_size(key_str, value.as_str(), metadata.as_str());
194                let new_content_len = self.kv_content_len + entry_content_len;
195                if new_content_len > MAX_LEN_OF_ALL_PAIRS {
196                    return None;
197                }
198                self.kv_content_len = new_content_len;
199                vacant_entry.insert((value, metadata));
200                None
201            }
202        }
203    }
204
205    /// Removes a name from the baggage, returning the value
206    /// corresponding to the name if the pair was previously in the map.
207    pub fn remove<K: AsRef<str>>(&mut self, key: K) -> Option<(StringValue, BaggageMetadata)> {
208        self.inner.remove(key.as_ref())
209    }
210
211    /// Returns the number of attributes for this baggage
212    pub fn len(&self) -> usize {
213        self.inner.len()
214    }
215
216    /// Returns `true` if the baggage contains no items.
217    pub fn is_empty(&self) -> bool {
218        self.inner.is_empty()
219    }
220
221    /// Gets an iterator over the baggage items, in any order.
222    pub fn iter(&self) -> Iter<'_> {
223        self.into_iter()
224    }
225
226    fn is_key_valid(key: &[u8]) -> bool {
227        !key.is_empty()
228            && key
229                .iter()
230                .all(|b| b.is_ascii_graphic() && !INVALID_ASCII_KEY_CHARS.contains(b))
231    }
232}
233
234/// Get the number of bytes for one key-value pair
235fn key_value_metadata_bytes_size(key: &str, value: &str, metadata: &str) -> usize {
236    key.len() + value.len() + metadata.len()
237}
238
239/// An iterator over the entries of a [`Baggage`].
240#[derive(Debug)]
241pub struct Iter<'a>(hash_map::Iter<'a, Key, (StringValue, BaggageMetadata)>);
242
243impl<'a> Iterator for Iter<'a> {
244    type Item = (&'a Key, &'a (StringValue, BaggageMetadata));
245
246    fn next(&mut self) -> Option<Self::Item> {
247        self.0.next()
248    }
249}
250
251impl<'a> IntoIterator for &'a Baggage {
252    type Item = (&'a Key, &'a (StringValue, BaggageMetadata));
253    type IntoIter = Iter<'a>;
254
255    fn into_iter(self) -> Self::IntoIter {
256        Iter(self.inner.iter())
257    }
258}
259
260impl FromIterator<(Key, (StringValue, BaggageMetadata))> for Baggage {
261    fn from_iter<I: IntoIterator<Item = (Key, (StringValue, BaggageMetadata))>>(iter: I) -> Self {
262        let mut baggage = Baggage::default();
263        for (key, (value, metadata)) in iter.into_iter() {
264            baggage.insert_with_metadata(key, value, metadata);
265        }
266        baggage
267    }
268}
269
270impl FromIterator<KeyValue> for Baggage {
271    fn from_iter<I: IntoIterator<Item = KeyValue>>(iter: I) -> Self {
272        let mut baggage = Baggage::default();
273        for kv in iter.into_iter() {
274            baggage.insert(kv.key, kv.value);
275        }
276        baggage
277    }
278}
279
280impl FromIterator<KeyValueMetadata> for Baggage {
281    fn from_iter<I: IntoIterator<Item = KeyValueMetadata>>(iter: I) -> Self {
282        let mut baggage = Baggage::default();
283        for kvm in iter.into_iter() {
284            baggage.insert_with_metadata(kvm.key, kvm.value, kvm.metadata);
285        }
286        baggage
287    }
288}
289
290impl<I> From<I> for Baggage
291where
292    I: IntoIterator,
293    I::Item: Into<KeyValueMetadata>,
294{
295    fn from(value: I) -> Self {
296        value.into_iter().map(Into::into).collect()
297    }
298}
299
300fn encode(s: &str) -> String {
301    let mut encoded_string = String::with_capacity(s.len());
302
303    for byte in s.as_bytes() {
304        match *byte {
305            b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_' | b'~' => {
306                encoded_string.push(*byte as char)
307            }
308            b' ' => encoded_string.push_str("%20"),
309            _ => encoded_string.push_str(&format!("%{byte:02X}")),
310        }
311    }
312    encoded_string
313}
314
315impl fmt::Display for Baggage {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        for (i, (k, v)) in self.into_iter().enumerate() {
318            write!(f, "{}={}", k, encode(v.0.as_str()))?;
319            if !v.1.as_str().is_empty() {
320                write!(f, ";{}", v.1)?;
321            }
322
323            if i < self.len() - 1 {
324                write!(f, ",")?;
325            }
326        }
327
328        Ok(())
329    }
330}
331
332/// Methods for sorting and retrieving baggage data in a context.
333pub trait BaggageExt {
334    /// Returns a clone of the given context with the included name/value pairs.
335    ///
336    /// # Examples
337    ///
338    /// ```
339    /// use opentelemetry::{baggage::{Baggage, BaggageExt}, Context, KeyValue, StringValue};
340    ///
341    /// // Explicit `Baggage` creation
342    /// let mut baggage = Baggage::new();
343    /// let _ = baggage.insert("my-name", "my-value");
344    ///
345    /// let cx = Context::map_current(|cx| {
346    ///     cx.with_baggage(baggage)
347    /// });
348    ///
349    /// // Passing an iterator
350    /// let cx = Context::map_current(|cx| {
351    ///     cx.with_baggage([KeyValue::new("my-name", "my-value")])
352    /// });
353    ///
354    /// assert_eq!(
355    ///     cx.baggage().get("my-name"),
356    ///     Some(&StringValue::from("my-value")),
357    /// )
358    /// ```
359    fn with_baggage<T: Into<Baggage>>(&self, baggage: T) -> Self;
360
361    /// Returns a clone of the current context with the included name/value pairs.
362    ///
363    /// # Examples
364    ///
365    /// ```
366    /// use opentelemetry::{baggage::{Baggage, BaggageExt}, Context, StringValue};
367    ///
368    /// let mut baggage = Baggage::new();
369    /// let _ = baggage.insert("my-name", "my-value");
370    ///
371    /// let cx = Context::current_with_baggage(baggage);
372    ///
373    /// assert_eq!(
374    ///     cx.baggage().get("my-name"),
375    ///     Some(&StringValue::from("my-value")),
376    /// )
377    /// ```
378    fn current_with_baggage<T: Into<Baggage>>(baggage: T) -> Self;
379
380    /// Returns a clone of the given context with no baggage.
381    ///
382    /// # Examples
383    ///
384    /// ```
385    /// use opentelemetry::{baggage::BaggageExt, Context};
386    ///
387    /// let cx = Context::map_current(|cx| cx.with_cleared_baggage());
388    ///
389    /// assert_eq!(cx.baggage().len(), 0);
390    /// ```
391    fn with_cleared_baggage(&self) -> Self;
392
393    /// Returns a reference to this context's baggage, or the default
394    /// empty baggage if none has been set.
395    fn baggage(&self) -> &Baggage;
396}
397
398/// Solely used to store `Baggage` in the `Context` without allowing direct access
399#[derive(Debug)]
400struct BaggageContextValue(Baggage);
401
402impl BaggageExt for Context {
403    fn with_baggage<T: Into<Baggage>>(&self, baggage: T) -> Self {
404        self.with_value(BaggageContextValue(baggage.into()))
405    }
406
407    fn current_with_baggage<T: Into<Baggage>>(baggage: T) -> Self {
408        Context::map_current(|cx| cx.with_baggage(baggage))
409    }
410
411    fn with_cleared_baggage(&self) -> Self {
412        self.with_baggage(Baggage::new())
413    }
414
415    fn baggage(&self) -> &Baggage {
416        self.get::<BaggageContextValue>()
417            .map_or(get_default_baggage(), |b| &b.0)
418    }
419}
420
421/// An optional property set that can be added to [`Baggage`] values.
422///
423/// `BaggageMetadata` can be added to values in the form of a property set,
424/// represented as semi-colon `;` delimited list of names and/or name/value
425/// pairs, e.g. `;k1=v1;k2;k3=v3`.
426#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Default)]
427pub struct BaggageMetadata(String);
428
429impl BaggageMetadata {
430    /// Return underlying string
431    pub fn as_str(&self) -> &str {
432        self.0.as_str()
433    }
434}
435
436impl From<String> for BaggageMetadata {
437    fn from(s: String) -> BaggageMetadata {
438        BaggageMetadata(s.trim().to_string())
439    }
440}
441
442impl From<&str> for BaggageMetadata {
443    fn from(s: &str) -> Self {
444        BaggageMetadata(s.trim().to_string())
445    }
446}
447
448impl fmt::Display for BaggageMetadata {
449    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450        Ok(write!(f, "{}", self.as_str())?)
451    }
452}
453
454/// [`Baggage`] name/value pairs with their associated metadata.
455#[derive(Clone, Debug, PartialEq)]
456pub struct KeyValueMetadata {
457    /// Dimension or event key
458    pub(crate) key: Key,
459    /// Dimension or event value
460    pub(crate) value: StringValue,
461    /// Metadata associate with this key value pair
462    pub(crate) metadata: BaggageMetadata,
463}
464
465impl KeyValueMetadata {
466    /// Create a new `KeyValue` pair with metadata
467    pub fn new<K, V, S>(key: K, value: V, metadata: S) -> Self
468    where
469        K: Into<Key>,
470        V: Into<StringValue>,
471        S: Into<BaggageMetadata>,
472    {
473        KeyValueMetadata {
474            key: key.into(),
475            value: value.into(),
476            metadata: metadata.into(),
477        }
478    }
479}
480
481impl From<KeyValue> for KeyValueMetadata {
482    fn from(kv: KeyValue) -> Self {
483        KeyValueMetadata {
484            key: kv.key,
485            value: kv.value.into(),
486            metadata: BaggageMetadata::default(),
487        }
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use crate::StringValue;
494
495    use super::*;
496
497    #[test]
498    fn insert_non_ascii_key() {
499        let mut baggage = Baggage::new();
500        baggage.insert("🚫", "not ascii key");
501        assert_eq!(baggage.len(), 0, "did not insert invalid key");
502    }
503
504    #[test]
505    fn test_ascii_values() {
506        let string1 = "test_ 123";
507        let string2 = "Hello123";
508        let string3 = "This & That = More";
509        let string4 = "Unicode: 😊";
510        let string5 = "Non-ASCII: áéíóú";
511        let string6 = "Unsafe: ~!@#$%^&*()_+{}[];:'\\\"<>?,./";
512        let string7: &str = "🚀Unicode:";
513        let string8 = "ΑΒΓ";
514
515        assert_eq!(encode(string1), "test_%20123");
516        assert_eq!(encode(string2), "Hello123");
517        assert_eq!(encode(string3), "This%20%26%20That%20%3D%20More");
518        assert_eq!(encode(string4), "Unicode%3A%20%F0%9F%98%8A");
519        assert_eq!(
520            encode(string5),
521            "Non-ASCII%3A%20%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA"
522        );
523        assert_eq!(encode(string6), "Unsafe%3A%20~%21%40%23%24%25%5E%26%2A%28%29_%2B%7B%7D%5B%5D%3B%3A%27%5C%22%3C%3E%3F%2C.%2F");
524        assert_eq!(encode(string7), "%F0%9F%9A%80Unicode%3A");
525        assert_eq!(encode(string8), "%CE%91%CE%92%CE%93");
526    }
527
528    #[test]
529    fn insert_too_much_baggage() {
530        // too many key pairs
531        let over_limit = MAX_KEY_VALUE_PAIRS + 1;
532        let mut data = Vec::with_capacity(over_limit);
533        for i in 0..over_limit {
534            data.push(KeyValue::new(format!("key{i}"), format!("key{i}")))
535        }
536        let baggage = data.into_iter().collect::<Baggage>();
537        assert_eq!(baggage.len(), MAX_KEY_VALUE_PAIRS)
538    }
539
540    #[test]
541    fn insert_pairs_length_exceed() {
542        let mut data = vec![];
543        for letter in vec!['a', 'b', 'c', 'd'].into_iter() {
544            data.push(KeyValue::new(
545                (0..MAX_LEN_OF_ALL_PAIRS / 3)
546                    .map(|_| letter)
547                    .collect::<String>(),
548                "",
549            ));
550        }
551        let baggage = data.into_iter().collect::<Baggage>();
552        assert_eq!(baggage.len(), 3)
553    }
554
555    #[test]
556    fn serialize_baggage_as_string() {
557        // Empty baggage
558        let b = Baggage::default();
559        assert_eq!("", b.to_string());
560
561        // "single member empty value no properties"
562        let mut b = Baggage::default();
563        b.insert("foo", StringValue::from(""));
564        assert_eq!("foo=", b.to_string());
565
566        // "single member no properties"
567        let mut b = Baggage::default();
568        b.insert("foo", StringValue::from("1"));
569        assert_eq!("foo=1", b.to_string());
570
571        // "URL encoded value"
572        let mut b = Baggage::default();
573        b.insert("foo", StringValue::from("1=1"));
574        assert_eq!("foo=1%3D1", b.to_string());
575
576        // "single member empty value with properties"
577        let mut b = Baggage::default();
578        b.insert_with_metadata(
579            "foo",
580            StringValue::from(""),
581            BaggageMetadata::from("red;state=on"),
582        );
583        assert_eq!("foo=;red;state=on", b.to_string());
584
585        // "single member with properties"
586        let mut b = Baggage::default();
587        b.insert_with_metadata("foo", StringValue::from("1"), "red;state=on;z=z=z");
588        assert_eq!("foo=1;red;state=on;z=z=z", b.to_string());
589
590        // "two members with properties"
591        let mut b = Baggage::default();
592        b.insert_with_metadata("foo", StringValue::from("1"), "red;state=on");
593        b.insert_with_metadata("bar", StringValue::from("2"), "yellow");
594        assert!(b.to_string().contains("bar=2;yellow"));
595        assert!(b.to_string().contains("foo=1;red;state=on"));
596    }
597
598    #[test]
599    fn replace_existing_key() {
600        let half_minus2: StringValue = (0..MAX_LEN_OF_ALL_PAIRS / 2 - 2)
601            .map(|_| 'x')
602            .collect::<String>()
603            .into();
604
605        let mut b = Baggage::default();
606        b.insert("a", half_minus2.clone()); // +1 for key
607        b.insert("b", half_minus2); // +1 for key
608        b.insert("c", StringValue::from(".")); // total of 2 bytes
609        assert!(b.get("a").is_some());
610        assert!(b.get("b").is_some());
611        assert!(b.get("c").is_some());
612        assert!(b.insert("c", StringValue::from("..")).is_none()); // exceeds MAX_LEN_OF_ALL_PAIRS
613        assert_eq!(b.insert("c", StringValue::from("!")).unwrap(), ".".into()); // replaces existing
614    }
615
616    #[test]
617    fn test_crud_operations() {
618        let mut baggage = Baggage::default();
619        assert!(baggage.is_empty());
620
621        // create
622        baggage.insert("foo", "1");
623        assert_eq!(baggage.len(), 1);
624
625        // get
626        assert_eq!(baggage.get("foo"), Some(&StringValue::from("1")));
627
628        // update
629        baggage.insert("foo", "2");
630        assert_eq!(baggage.get("foo"), Some(&StringValue::from("2")));
631
632        // delete
633        baggage.remove("foo");
634        assert!(baggage.is_empty());
635    }
636
637    #[test]
638    fn test_insert_invalid_key() {
639        let mut baggage = Baggage::default();
640
641        // empty
642        baggage.insert("", "1");
643        assert!(baggage.is_empty());
644
645        // non-ascii
646        baggage.insert("Grüße", "1");
647        assert!(baggage.is_empty());
648
649        // invalid ascii chars
650        baggage.insert("(example)", "1");
651        assert!(baggage.is_empty());
652    }
653
654    #[test]
655    fn test_context_clear_baggage() {
656        let ctx = Context::new();
657        let ctx = ctx.with_baggage([KeyValue::new("foo", 1)]);
658        let _guard = ctx.attach();
659
660        {
661            let ctx = Context::current();
662            let baggage = ctx.baggage();
663            // At this point baggage should still contain the inital value.
664            assert_eq!(baggage.len(), 1);
665
666            // Baggage gets cleared.
667            let ctx = ctx.with_cleared_baggage();
668            let _guard = ctx.attach();
669            {
670                let ctx = Context::current();
671                let baggage = ctx.baggage();
672                // Baggage should contain no entries.
673                assert_eq!(baggage.len(), 0);
674            }
675        }
676    }
677}