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