diff --git a/datadog-opentelemetry/examples/propagator/src/server.rs b/datadog-opentelemetry/examples/propagator/src/server.rs index 81656a22..2ed563d0 100644 --- a/datadog-opentelemetry/examples/propagator/src/server.rs +++ b/datadog-opentelemetry/examples/propagator/src/server.rs @@ -119,7 +119,10 @@ async fn send_request( ) -> std::result::Result<(), Box> { let client = Client::builder(TokioExecutor::new()).build_http(); - let cx = Context::current(); + let cx = Context::current().with_baggage(vec![ + KeyValue::new("request-id", "xyz-123"), + KeyValue::new("caller", "rust-propagator-example"), + ]); let mut req = hyper::Request::builder().uri(url); global::get_text_map_propagator(|propagator| { diff --git a/datadog-opentelemetry/src/core/configuration/configuration.rs b/datadog-opentelemetry/src/core/configuration/configuration.rs index 248faac1..808a97e2 100644 --- a/datadog-opentelemetry/src/core/configuration/configuration.rs +++ b/datadog-opentelemetry/src/core/configuration/configuration.rs @@ -773,6 +773,8 @@ pub enum TracePropagationStyle { Datadog, /// W3C Trace Context propagation format using `traceparent` and `tracestate` headers. TraceContext, + /// W3C Baggage propagation format using the `baggage` header. + Baggage, /// No propagation - trace context is not propagated. None, } @@ -804,6 +806,7 @@ impl FromStr for TracePropagationStyle { match s.trim().to_lowercase().as_str() { "datadog" => Ok(TracePropagationStyle::Datadog), "tracecontext" => Ok(TracePropagationStyle::TraceContext), + "baggage" => Ok(TracePropagationStyle::Baggage), "none" => Ok(TracePropagationStyle::None), _ => Err(format!("Unknown trace propagation style: '{s}'")), } @@ -815,6 +818,7 @@ impl Display for TracePropagationStyle { let style = match self { TracePropagationStyle::Datadog => "datadog", TracePropagationStyle::TraceContext => "tracecontext", + TracePropagationStyle::Baggage => "baggage", TracePropagationStyle::None => "none", }; write!(f, "{style}") @@ -1822,6 +1826,7 @@ fn default_config() -> Config { Some(vec![ TracePropagationStyle::Datadog, TracePropagationStyle::TraceContext, + TracePropagationStyle::Baggage, ]), ), trace_propagation_style_extract: ConfigItem::new( @@ -2726,6 +2731,7 @@ mod tests { Some(vec![ TracePropagationStyle::Datadog, TracePropagationStyle::TraceContext, + TracePropagationStyle::Baggage, ]) .as_deref() ); @@ -2737,6 +2743,47 @@ mod tests { assert!(config.trace_propagation_extract_first()); } + #[test] + fn test_propagation_style_baggage_parsed_from_env() { + // "baggage" is recognised as a valid style value (case-insensitive) in all three env vars. + let mut sources = CompositeSource::new(); + sources.add_source(HashMapSource::from_iter( + [ + ("DD_TRACE_PROPAGATION_STYLE", "datadog,tracecontext,baggage"), + ("DD_TRACE_PROPAGATION_STYLE_EXTRACT", "Baggage,datadog"), + ("DD_TRACE_PROPAGATION_STYLE_INJECT", "BAGGAGE,tracecontext"), + ], + ConfigSourceOrigin::EnvVar, + )); + let config = Config::builder_with_sources(&sources).build(); + + assert_eq!( + config.trace_propagation_style(), + Some(vec![ + TracePropagationStyle::Datadog, + TracePropagationStyle::TraceContext, + TracePropagationStyle::Baggage, + ]) + .as_deref() + ); + assert_eq!( + config.trace_propagation_style_extract(), + Some(vec![ + TracePropagationStyle::Baggage, + TracePropagationStyle::Datadog, + ]) + .as_deref() + ); + assert_eq!( + config.trace_propagation_style_inject(), + Some(vec![ + TracePropagationStyle::Baggage, + TracePropagationStyle::TraceContext, + ]) + .as_deref() + ); + } + #[test] fn test_stats_computation_enabled_config() { let mut sources = CompositeSource::new(); diff --git a/datadog-opentelemetry/src/propagation/baggage.rs b/datadog-opentelemetry/src/propagation/baggage.rs new file mode 100644 index 00000000..2ebf5e91 --- /dev/null +++ b/datadog-opentelemetry/src/propagation/baggage.rs @@ -0,0 +1,21 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! W3C Baggage propagation (`baggage` header). +//! +//! Actual extract/inject is performed by [`opentelemetry_sdk::propagation::BaggagePropagator`] +//! at the [`DatadogPropagator`](crate::text_map_propagator::DatadogPropagator) layer, which has +//! access to the OTel [`Context`](opentelemetry::Context) that carries baggage. This module +//! exposes the header key so the composite propagator can include it in its `fields()` list. + +use std::sync::LazyLock; + +/// The W3C `baggage` header name. +pub const BAGGAGE_KEY: &str = "baggage"; + +static BAGGAGE_HEADER_KEYS: LazyLock<[String; 1]> = LazyLock::new(|| [BAGGAGE_KEY.to_owned()]); + +/// Returns the header keys used by the W3C baggage propagator. +pub fn keys() -> &'static [String] { + BAGGAGE_HEADER_KEYS.as_slice() +} diff --git a/datadog-opentelemetry/src/propagation/mod.rs b/datadog-opentelemetry/src/propagation/mod.rs index 07a19ca2..1ee07e8e 100644 --- a/datadog-opentelemetry/src/propagation/mod.rs +++ b/datadog-opentelemetry/src/propagation/mod.rs @@ -14,6 +14,8 @@ use config::{get_extractors, get_injectors}; use datadog::DATADOG_LAST_PARENT_ID_KEY; use tracecontext::TRACESTATE_KEY; +/// W3C Baggage propagation (`baggage` header). +pub mod baggage; pub mod carrier; pub(crate) mod config; pub mod context; diff --git a/datadog-opentelemetry/src/propagation/trace_propagation_style.rs b/datadog-opentelemetry/src/propagation/trace_propagation_style.rs index 735424fa..9f7deb8b 100644 --- a/datadog-opentelemetry/src/propagation/trace_propagation_style.rs +++ b/datadog-opentelemetry/src/propagation/trace_propagation_style.rs @@ -5,6 +5,7 @@ use crate::core::configuration::TracePropagationStyle; use serde::{Deserialize, Deserializer}; use crate::propagation::{ + baggage, carrier::{Extractor, Injector}, context::{InjectSpanContext, SpanContext}, datadog, tracecontext, PropagationConfig, Propagator, @@ -17,7 +18,8 @@ impl Propagator for TracePropagationStyle { match self { Self::Datadog => datadog::extract(carrier, config), Self::TraceContext => tracecontext::extract(carrier), - _ => None, + // Baggage extraction operates on OTel Context and is handled by DatadogPropagator. + Self::Baggage | Self::None => None, } } @@ -25,7 +27,8 @@ impl Propagator for TracePropagationStyle { match self { Self::Datadog => datadog::inject(context, carrier, config), Self::TraceContext => tracecontext::inject(context, carrier), - _ => {} + // Baggage injection operates on OTel Context and is handled by DatadogPropagator. + Self::Baggage | Self::None => {} } } @@ -33,7 +36,8 @@ impl Propagator for TracePropagationStyle { match self { Self::Datadog => datadog::keys(), Self::TraceContext => tracecontext::keys(), - _ => &NONE_KEYS, + Self::Baggage => baggage::keys(), + Self::None => &NONE_KEYS, } } } diff --git a/datadog-opentelemetry/src/text_map_propagator.rs b/datadog-opentelemetry/src/text_map_propagator.rs index cfb4d278..90dc9635 100644 --- a/datadog-opentelemetry/src/text_map_propagator.rs +++ b/datadog-opentelemetry/src/text_map_propagator.rs @@ -8,13 +8,14 @@ use crate::{ core::{configuration::Config, sampling::priority}, propagation::{ context::{InjectSpanContext, InjectTraceState, Sampling, SpanContext, SpanLink}, - DatadogCompositePropagator, + DatadogCompositePropagator, TracePropagationStyle, }, }; use opentelemetry::{ propagation::{text_map_propagator::FieldIter, TextMapPropagator}, trace::TraceContextExt, }; +use opentelemetry_sdk::propagation::BaggagePropagator; use crate::TraceRegistry; @@ -64,14 +65,41 @@ pub struct DatadogPropagator { inner: DatadogCompositePropagator, registry: TraceRegistry, cfg: Arc, + baggage_propagator: BaggagePropagator, + baggage_extract: bool, + baggage_inject: bool, + fields: Vec, } impl DatadogPropagator { pub(crate) fn new(config: Arc, registry: TraceRegistry) -> Self { + let baggage_extract = config + .trace_propagation_style_extract() + .or_else(|| config.trace_propagation_style()) + .unwrap_or_default() + .contains(&TracePropagationStyle::Baggage); + let baggage_inject = config + .trace_propagation_style_inject() + .or_else(|| config.trace_propagation_style()) + .unwrap_or_default() + .contains(&TracePropagationStyle::Baggage); + let inner = DatadogCompositePropagator::new(config.clone()); + let mut fields = inner.keys().to_vec(); + if baggage_inject + && !fields + .iter() + .any(|k| k == crate::propagation::baggage::BAGGAGE_KEY) + { + fields.push(crate::propagation::baggage::BAGGAGE_KEY.to_owned()); + } DatadogPropagator { - inner: DatadogCompositePropagator::new(config.clone()), + inner, registry, cfg: config, + baggage_propagator: BaggagePropagator::new(), + baggage_extract, + baggage_inject, + fields, } } @@ -83,6 +111,10 @@ impl DatadogPropagator { cx: &opentelemetry::Context, mut injector: &mut dyn opentelemetry::propagation::Injector, ) { + if self.baggage_inject { + self.baggage_propagator.inject_context(cx, injector); + } + let span = cx.span(); let otel_span_context = span.span_context(); @@ -150,7 +182,8 @@ impl DatadogPropagator { cx: &opentelemetry::Context, extractor: &dyn opentelemetry::propagation::Extractor, ) -> opentelemetry::Context { - self.inner + let cx = self + .inner .extract(&extractor) .map(|dd_span_context| { let trace_flags = extract_trace_flags(&dd_span_context); @@ -167,7 +200,13 @@ impl DatadogPropagator { cx.with_remote_span_context(otel_span_context) .with_value(DatadogExtractData::from_span_context(dd_span_context)) }) - .unwrap_or_else(|| cx.clone()) + .unwrap_or_else(|| cx.clone()); + + if self.baggage_extract { + self.baggage_propagator.extract_with_context(&cx, extractor) + } else { + cx + } } } @@ -199,8 +238,8 @@ impl TextMapPropagator for DatadogPropagator { } fn fields(&self) -> opentelemetry::propagation::text_map_propagator::FieldIter<'_> { - let fields = if self.cfg.enabled() { - self.inner.keys() + let fields: &[String] = if self.cfg.enabled() { + &self.fields } else { &[] }; @@ -245,6 +284,7 @@ pub mod tests { }; use assert_unordered::assert_eq_unordered; use opentelemetry::{ + baggage::BaggageExt, propagation::{Extractor, TextMapPropagator}, trace::{Span, SpanContext as OtelSpanContext, Status, TraceContextExt, TraceState}, Context, KeyValue, SpanId, TraceFlags, TraceId, @@ -679,4 +719,304 @@ pub mod tests { } } } + + use crate::propagation::baggage::BAGGAGE_KEY; + + fn get_propagator_with_separate_styles( + extract: Vec, + inject: Vec, + ) -> DatadogPropagator { + let config = Arc::new( + Config::builder() + .set_trace_propagation_style_extract(extract) + .set_trace_propagation_style_inject(inject) + .build(), + ); + DatadogPropagator::new(config.clone(), TraceRegistry::new(config)) + } + + #[test] + fn baggage_extract_only_when_not_in_inject_styles() { + // Extract has baggage, inject does not. + let propagator = get_propagator_with_separate_styles( + vec![ + TracePropagationStyle::Baggage, + TracePropagationStyle::Datadog, + ], + vec![TracePropagationStyle::TraceContext], + ); + + // Extraction: baggage header present → lands in OTel Context. + let mut carrier = HashMap::new(); + carrier.insert(BAGGAGE_KEY.to_string(), "user=alice".to_string()); + let cx = propagator.extract(&carrier); + assert!( + cx.baggage().get("user").is_some(), + "baggage should be extracted when Baggage is in extract styles" + ); + + // Injection: same Context → baggage header must NOT be written. + let mut injector: HashMap = HashMap::new(); + propagator.inject_context(&cx, &mut injector); + assert!( + injector.get(BAGGAGE_KEY).is_none(), + "baggage header must not be injected when Baggage is absent from inject styles" + ); + } + + #[test] + fn baggage_inject_only_when_not_in_extract_styles() { + // Inject has baggage, extract does not. + let propagator = get_propagator_with_separate_styles( + vec![TracePropagationStyle::Datadog], + vec![ + TracePropagationStyle::Baggage, + TracePropagationStyle::TraceContext, + ], + ); + + // Extraction: baggage header present → must NOT land in OTel Context. + let mut carrier = HashMap::new(); + carrier.insert(BAGGAGE_KEY.to_string(), "user=alice".to_string()); + let cx = propagator.extract(&carrier); + assert!( + cx.baggage().get("user").is_none(), + "baggage must not be extracted when Baggage is absent from extract styles" + ); + + // Injection: context carrying baggage → header must be written. + let cx_with_baggage = Context::current_with_baggage(vec![KeyValue::new("server", "42")]); + let span_cx = OtelSpanContext::new( + TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736_u128), + SpanId::from(0x00f0_67aa_0ba9_02b7_u64), + TraceFlags::SAMPLED, + true, + TraceState::NONE, + ); + let cx_with_both = cx_with_baggage.with_remote_span_context(span_cx); + + let mut injector: HashMap = HashMap::new(); + propagator.inject_context(&cx_with_both, &mut injector); + let header = injector + .get(BAGGAGE_KEY) + .expect("baggage header must be injected"); + assert!( + header.contains("server=42"), + "injected baggage header should contain the baggage entry" + ); + } + + // ── Helper: build a propagator from a single combined style list ────────── + + fn get_propagator_with_combined_style(styles: Vec) -> DatadogPropagator { + let config = Arc::new( + Config::builder() + .set_trace_propagation_style(styles) + .build(), + ); + DatadogPropagator::new(config.clone(), TraceRegistry::new(config)) + } + + // ── Unit: composite propagator inclusion/exclusion ──────────────────────── + + #[test] + fn baggage_included_in_fields_when_configured() { + let propagator = get_propagator_with_combined_style(vec![TracePropagationStyle::Baggage]); + let fields: Vec<&str> = propagator.fields().collect(); + assert!( + fields.contains(&BAGGAGE_KEY), + "fields() must include 'baggage' when Baggage style is configured; got {fields:?}" + ); + } + + #[test] + fn baggage_excluded_from_fields_when_not_configured() { + let propagator = get_propagator_with_combined_style(vec![ + TracePropagationStyle::Datadog, + TracePropagationStyle::TraceContext, + ]); + let fields: Vec<&str> = propagator.fields().collect(); + assert!( + !fields.contains(&BAGGAGE_KEY), + "fields() must not include 'baggage' when Baggage style is absent; got {fields:?}" + ); + } + + #[test] + fn baggage_included_in_fields_when_only_in_inject_styles() { + let propagator = get_propagator_with_separate_styles( + vec![TracePropagationStyle::Datadog], + vec![ + TracePropagationStyle::Baggage, + TracePropagationStyle::TraceContext, + ], + ); + let fields: Vec<&str> = propagator.fields().collect(); + assert!( + fields.contains(&BAGGAGE_KEY), + "fields() must include 'baggage' when Baggage is only in inject styles; got {fields:?}" + ); + } + + #[test] + fn baggage_included_in_fields_when_only_in_extract_styles() { + let propagator = get_propagator_with_separate_styles( + vec![ + TracePropagationStyle::Baggage, + TracePropagationStyle::Datadog, + ], + vec![TracePropagationStyle::TraceContext], + ); + let fields: Vec<&str> = propagator.fields().collect(); + assert!( + fields.contains(&BAGGAGE_KEY), + "fields() must include 'baggage' when Baggage is only in extract styles; got {fields:?}" + ); + } + + // ── Integration: inject / extract / round-trip ──────────────────────────── + + #[test] + fn baggage_inject_encodes_multiple_entries() { + let propagator = get_propagator_with_combined_style(vec![TracePropagationStyle::Baggage]); + + let cx = Context::current_with_baggage(vec![ + KeyValue::new("user", "alice"), + KeyValue::new("tenant", "acme"), + ]); + + let mut carrier: HashMap = HashMap::new(); + propagator.inject_context(&cx, &mut carrier); + + let header = carrier + .get(BAGGAGE_KEY) + .expect("baggage header must be present"); + assert!( + header.contains("user=alice"), + "header must contain user=alice; got {header}" + ); + assert!( + header.contains("tenant=acme"), + "header must contain tenant=acme; got {header}" + ); + } + + #[test] + fn baggage_extract_populates_context() { + let propagator = get_propagator_with_combined_style(vec![TracePropagationStyle::Baggage]); + + let mut carrier: HashMap = HashMap::new(); + carrier.insert( + BAGGAGE_KEY.to_string(), + "user=alice,tenant=acme".to_string(), + ); + + let cx = propagator.extract(&carrier); + + let baggage = cx.baggage(); + assert_eq!( + baggage.get("user").map(|v| v.as_str()), + Some("alice"), + "extracted baggage must contain user=alice" + ); + assert_eq!( + baggage.get("tenant").map(|v| v.as_str()), + Some("acme"), + "extracted baggage must contain tenant=acme" + ); + } + + #[test] + fn baggage_round_trip() { + let propagator = get_propagator_with_combined_style(vec![TracePropagationStyle::Baggage]); + + // Inject + let original_cx = Context::current_with_baggage(vec![ + KeyValue::new("request-id", "xyz-123"), + KeyValue::new("region", "us-east-1"), + ]); + let mut carrier: HashMap = HashMap::new(); + propagator.inject_context(&original_cx, &mut carrier); + + // Extract into a fresh context + let extracted_cx = propagator.extract(&carrier); + let baggage = extracted_cx.baggage(); + + assert_eq!( + baggage.get("request-id").map(|v| v.as_str()), + Some("xyz-123"), + "round-trip must preserve request-id" + ); + assert_eq!( + baggage.get("region").map(|v| v.as_str()), + Some("us-east-1"), + "round-trip must preserve region" + ); + } + + // ── Env-var style scenarios ─────────────────────────────────────────────── + + #[test] + fn baggage_not_injected_when_style_is_datadog_tracecontext() { + // DD_TRACE_PROPAGATION_STYLE=datadog,tracecontext — no Baggage variant. + let propagator = get_propagator_with_combined_style(vec![ + TracePropagationStyle::Datadog, + TracePropagationStyle::TraceContext, + ]); + + let cx = Context::current_with_baggage(vec![KeyValue::new("user", "alice")]); + let mut carrier: HashMap = HashMap::new(); + propagator.inject_context(&cx, &mut carrier); + + assert!( + carrier.get(BAGGAGE_KEY).is_none(), + "baggage header must NOT be present when Baggage is not in propagation style" + ); + } + + #[test] + fn baggage_only_style_injects_only_baggage_header() { + // DD_TRACE_PROPAGATION_STYLE=baggage — no Datadog or TraceContext headers. + let propagator = get_propagator_with_combined_style(vec![TracePropagationStyle::Baggage]); + + let cx = Context::current_with_baggage(vec![KeyValue::new("user", "alice")]); + let mut carrier: HashMap = HashMap::new(); + propagator.inject_context(&cx, &mut carrier); + + assert!( + carrier.get(BAGGAGE_KEY).is_some(), + "baggage header must be present" + ); + assert!( + carrier.get("traceparent").is_none(), + "traceparent must NOT be present when TraceContext is not in propagation style" + ); + assert!( + carrier.get("x-datadog-trace-id").is_none(), + "x-datadog-trace-id must NOT be present when Datadog is not in propagation style" + ); + } + + #[test] + fn baggage_injected_by_default_config() { + // Default config (no explicit style set) includes Baggage. + let config = Arc::new(Config::builder().build()); + let propagator = DatadogPropagator::new(config.clone(), TraceRegistry::new(config)); + + // Verify via fields() + let fields: Vec<&str> = propagator.fields().collect(); + assert!( + fields.contains(&BAGGAGE_KEY), + "fields() must include 'baggage' with default config; got {fields:?}" + ); + + // Verify via actual injection + let cx = Context::current_with_baggage(vec![KeyValue::new("env", "prod")]); + let mut carrier: HashMap = HashMap::new(); + propagator.inject_context(&cx, &mut carrier); + assert!( + carrier.get(BAGGAGE_KEY).is_some(), + "baggage header must be injected with default config" + ); + } }