1use opentelemetry::{
2 baggage::{BaggageExt, KeyValueMetadata},
3 otel_warn,
4 propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator},
5 Context,
6};
7use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS};
8use std::iter;
9use std::sync::OnceLock;
10
11static BAGGAGE_HEADER: &str = "baggage";
12const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b';').add(b',').add(b'=');
13
14const MAX_BAGGAGE_LENGTH: usize = 8192;
17const MAX_BAGGAGE_ITEMS: usize = 64;
18
19static BAGGAGE_FIELDS: OnceLock<[String; 1]> = OnceLock::new();
21#[inline]
22fn baggage_fields() -> &'static [String; 1] {
23 BAGGAGE_FIELDS.get_or_init(|| [BAGGAGE_HEADER.to_owned()])
24}
25
26#[derive(Debug, Default)]
72pub struct BaggagePropagator {
73 _private: (),
74}
75
76impl BaggagePropagator {
77 pub fn new() -> Self {
79 BaggagePropagator { _private: () }
80 }
81}
82
83impl TextMapPropagator for BaggagePropagator {
84 fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
86 let baggage = cx.baggage();
87 if !baggage.is_empty() {
88 let header_value = baggage
89 .iter()
90 .map(|(name, (value, metadata))| {
91 let metadata_str = metadata.as_str().trim();
92 let metadata_prefix = if metadata_str.is_empty() { "" } else { ";" };
93 utf8_percent_encode(name.as_str().trim(), FRAGMENT)
94 .chain(iter::once("="))
95 .chain(utf8_percent_encode(value.as_str().trim(), FRAGMENT))
96 .chain(iter::once(metadata_prefix))
97 .chain(iter::once(metadata_str))
98 .collect()
99 })
100 .collect::<Vec<String>>()
101 .join(",");
102 injector.set(BAGGAGE_HEADER, header_value);
103 }
104 }
105
106 fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context {
108 if let Some(header_value) = extractor.get(BAGGAGE_HEADER) {
109 if header_value.len() > MAX_BAGGAGE_LENGTH {
114 otel_warn!(
115 name: "BaggagePropagator.Extract.HeaderTooLarge",
116 message = "Baggage header exceeds W3C maximum length and was dropped",
117 header_bytes = header_value.len() as i64,
118 limit_bytes = MAX_BAGGAGE_LENGTH as i64,
119 );
120 return cx.clone();
121 }
122
123 let baggage =
124 header_value
125 .split(',')
126 .take(MAX_BAGGAGE_ITEMS)
127 .filter_map(|context_value| {
128 if let Some((name_and_value, props)) = context_value
129 .split(';')
130 .collect::<Vec<&str>>()
131 .split_first()
132 {
133 let mut iter = name_and_value.split('=');
134 if let (Some(name), Some(value)) = (iter.next(), iter.next()) {
135 let decode_name = percent_decode_str(name).decode_utf8();
136 let decode_value = percent_decode_str(value).decode_utf8();
137
138 if let (Ok(name), Ok(value)) = (decode_name, decode_value) {
139 let decoded_props = props
142 .iter()
143 .flat_map(|prop| percent_decode_str(prop).decode_utf8())
144 .map(|prop| prop.trim().to_string())
145 .collect::<Vec<String>>()
146 .join(";"); Some(KeyValueMetadata::new(
149 name.trim().to_owned(),
150 value.trim().to_string(),
151 decoded_props.as_str(),
152 ))
153 } else {
154 otel_warn!(
155 name: "BaggagePropagator.Extract.InvalidUTF8",
156 message = "Invalid UTF8 string in key values",
157 baggage_header = header_value,
158 );
159 None
160 }
161 } else {
162 otel_warn!(
163 name: "BaggagePropagator.Extract.InvalidKeyValueFormat",
164 message = "Invalid baggage key-value format",
165 baggage_header = header_value,
166 );
167 None
168 }
169 } else {
170 otel_warn!(
171 name: "BaggagePropagator.Extract.InvalidFormat",
172 message = "Invalid baggage format",
173 baggage_header = header_value);
174 None
175 }
176 });
177 cx.with_baggage(baggage)
178 } else {
179 cx.clone()
180 }
181 }
182
183 fn fields(&self) -> FieldIter<'_> {
184 FieldIter::new(baggage_fields())
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use opentelemetry::{baggage::BaggageMetadata, Key, KeyValue, StringValue, Value};
192 use std::collections::HashMap;
193
194 #[rustfmt::skip]
195 fn valid_extract_data() -> Vec<(&'static str, HashMap<Key, StringValue>)> {
196 vec![
197 ("key1=val1,key2=val2", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
199 ("key1 = val1, key2 =val2 ", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
201 ("key1=val1,key2=val2%2Cval3", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2,val3"))].into_iter().collect()),
203 ("key1=val1,key2=val2,a,val3", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
205 ("key1=,key2=val2", vec![(Key::new("key1"), StringValue::from("")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()),
207 ]
208 }
209
210 #[rustfmt::skip]
211 #[allow(clippy::type_complexity)]
212 fn valid_extract_data_with_metadata() -> Vec<(&'static str, HashMap<Key, (StringValue, BaggageMetadata)>)> {
213 vec![
214 ("key1=val1,key2=val2;prop=1", vec![(Key::new("key1"), (StringValue::from("val1"), BaggageMetadata::default())), (Key::new("key2"), (StringValue::from("val2"), BaggageMetadata::from("prop=1")))].into_iter().collect()),
216 ("key1=val1,key2=val2;prop1", vec![(Key::new("key1"), (StringValue::from("val1"), BaggageMetadata::default())), (Key::new("key2"), (StringValue::from("val2"), BaggageMetadata::from("prop1")))].into_iter().collect()),
218 ("key1=value1;property1;property2, key2 = value2, key3=value3; propertyKey=propertyValue",
219 vec![
220 (Key::new("key1"), (StringValue::from("value1"), BaggageMetadata::from("property1;property2"))),
221 (Key::new("key2"), (StringValue::from("value2"), BaggageMetadata::default())),
222 (Key::new("key3"), (StringValue::from("value3"), BaggageMetadata::from("propertyKey=propertyValue"))),
223 ].into_iter().collect()),
224 ]
225 }
226
227 #[rustfmt::skip]
228 fn valid_inject_data() -> Vec<(Vec<KeyValue>, Vec<&'static str>)> {
229 vec![
230 (vec![KeyValue::new("key1", "val1"), KeyValue::new("key2", "val2")], vec!["key1=val1", "key2=val2"]),
232 (vec![KeyValue::new("key1", "val1,val2"), KeyValue::new("key2", "val3=4")], vec!["key1=val1%2Cval2", "key2=val3%3D4"]),
234 (
236 vec![
237 KeyValue::new("key1", true),
238 KeyValue::new("key2", Value::I64(123)),
239 KeyValue::new("key3", Value::F64(123.567)),
240 ],
241 vec![
242 "key1=true",
243 "key2=123",
244 "key3=123.567",
245 ],
246 ),
247 (
249 vec![
250 KeyValue::new("key1", Value::Array(vec![true, false].into())),
251 KeyValue::new("key2", Value::Array(vec![123, 456].into())),
252 KeyValue::new("key3", Value::Array(vec![StringValue::from("val1"), StringValue::from("val2")].into())),
253 ],
254 vec![
255 "key1=[true%2Cfalse]",
256 "key2=[123%2C456]",
257 "key3=[%22val1%22%2C%22val2%22]",
258 ],
259 ),
260 ]
261 }
262
263 #[rustfmt::skip]
264 fn valid_inject_data_metadata() -> Vec<(Vec<KeyValueMetadata>, Vec<&'static str>)> {
265 vec![
266 (
267 vec![
268 KeyValueMetadata::new("key1", "val1", "prop1"),
269 KeyValue::new("key2", "val2").into(),
270 KeyValueMetadata::new("key3", "val3", "anykey=anyvalue"),
271 ],
272 vec![
273 "key1=val1;prop1",
274 "key2=val2",
275 "key3=val3;anykey=anyvalue",
276 ],
277 )
278 ]
279 }
280
281 #[test]
282 fn extract_baggage() {
283 let propagator = BaggagePropagator::new();
284
285 for (header_value, kvs) in valid_extract_data() {
286 let mut extractor: HashMap<String, String> = HashMap::new();
287 extractor.insert(BAGGAGE_HEADER.to_string(), header_value.to_string());
288 let context = propagator.extract(&extractor);
289 let baggage = context.baggage();
290
291 assert_eq!(kvs.len(), baggage.len());
292 for (key, (value, _metadata)) in baggage {
293 assert_eq!(Some(value), kvs.get(key))
294 }
295 }
296 }
297
298 #[test]
299 fn inject_baggage() {
300 let propagator = BaggagePropagator::new();
301
302 for (kvm, header_parts) in valid_inject_data() {
303 let mut injector = HashMap::new();
304 let cx = Context::current_with_baggage(kvm);
305 propagator.inject_context(&cx, &mut injector);
306 let header_value = injector.get(BAGGAGE_HEADER).unwrap();
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
314 #[test]
315 fn extract_baggage_with_metadata() {
316 let propagator = BaggagePropagator::new();
317 for (header_value, kvm) in valid_extract_data_with_metadata() {
318 let mut extractor: HashMap<String, String> = HashMap::new();
319 extractor.insert(BAGGAGE_HEADER.to_string(), header_value.to_string());
320 let context = propagator.extract(&extractor);
321 let baggage = context.baggage();
322
323 assert_eq!(kvm.len(), baggage.len());
324 for (key, value_and_prop) in baggage {
325 assert_eq!(Some(value_and_prop), kvm.get(key))
326 }
327 }
328 }
329
330 #[test]
331 fn inject_baggage_with_metadata() {
332 let propagator = BaggagePropagator::new();
333
334 for (kvm, header_parts) in valid_inject_data_metadata() {
335 let mut injector = HashMap::new();
336 let cx = Context::current_with_baggage(kvm);
337 propagator.inject_context(&cx, &mut injector);
338 let header_value = injector.get(BAGGAGE_HEADER).unwrap();
339
340 assert_eq!(header_parts.join(",").len(), header_value.len());
341 for header_part in &header_parts {
342 assert!(header_value.contains(header_part),)
343 }
344 }
345 }
346
347 #[test]
348 fn extract_drops_header_exceeding_max_length() {
349 let propagator = BaggagePropagator::new();
350
351 let mut header = String::with_capacity(MAX_BAGGAGE_LENGTH + 1024);
354 let mut i = 0u32;
355 while header.len() < MAX_BAGGAGE_LENGTH + 256 {
356 if !header.is_empty() {
357 header.push(',');
358 }
359 header.push_str(&format!("k{i}=v{i}"));
360 i += 1;
361 }
362 assert!(header.len() > MAX_BAGGAGE_LENGTH);
363
364 let mut extractor: HashMap<String, String> = HashMap::new();
365 extractor.insert(BAGGAGE_HEADER.to_string(), header);
366
367 let context = propagator.extract(&extractor);
368 assert_eq!(context.baggage().len(), 0);
369 }
370
371 #[test]
372 fn extract_caps_entry_count_at_max_baggage_items() {
373 let propagator = BaggagePropagator::new();
374
375 let mut header = String::new();
378 let entries = MAX_BAGGAGE_ITEMS + 32;
379 for i in 0..entries {
380 if i > 0 {
381 header.push(',');
382 }
383 header.push_str(&format!("k{i:03}=v"));
384 }
385 assert!(header.len() <= MAX_BAGGAGE_LENGTH);
386
387 let mut extractor: HashMap<String, String> = HashMap::new();
388 extractor.insert(BAGGAGE_HEADER.to_string(), header);
389
390 let context = propagator.extract(&extractor);
391 assert_eq!(context.baggage().len(), MAX_BAGGAGE_ITEMS);
395 }
396}