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