Skip to main content

telemetry_rust/
propagation.rs

1//! Context propagation utilities for distributed tracing across service boundaries.
2
3use opentelemetry::{
4    Context,
5    propagation::{
6        Extractor, Injector, TextMapCompositePropagator, TextMapPropagator,
7        text_map_propagator::FieldIter,
8    },
9};
10#[cfg(feature = "xray")]
11use opentelemetry_aws::trace::XrayPropagator;
12use opentelemetry_sdk::{
13    error::OTelSdkError,
14    propagation::{BaggagePropagator, TraceContextPropagator},
15};
16#[cfg(feature = "zipkin")]
17#[allow(deprecated)]
18use opentelemetry_zipkin::{B3Encoding, Propagator as B3Propagator};
19use std::collections::BTreeSet;
20
21use crate::util;
22
23/// Type alias for a boxed text map propagator.
24///
25/// This type represents a thread-safe, heap-allocated text map propagator that can be
26/// used for OpenTelemetry context propagation across service boundaries.
27pub type Propagator = Box<dyn TextMapPropagator + Send + Sync>;
28
29/// A no-op propagator that performs no context injection or extraction.
30///
31/// This propagator can be used when context propagation is explicitly disabled
32/// or not needed. It implements the [`TextMapPropagator`] trait but performs
33/// no actual propagation operations.
34#[derive(Debug)]
35pub struct NonePropagator;
36
37impl TextMapPropagator for NonePropagator {
38    fn inject_context(&self, _: &Context, _: &mut dyn Injector) {}
39
40    fn extract_with_context(&self, cx: &Context, _: &dyn Extractor) -> Context {
41        cx.clone()
42    }
43
44    fn fields(&self) -> FieldIter<'_> {
45        FieldIter::new(&[])
46    }
47}
48
49/// A text map propagator that uses different propagators for injection and extraction.
50///
51/// This propagator allows for asymmetric context propagation where different
52/// propagation strategies can be used for outgoing requests (injection) versus
53/// incoming requests (extraction). This is useful when you need to maintain
54/// compatibility with multiple tracing systems or protocols.
55///
56/// # Use Cases
57///
58/// - Migrating between tracing systems while maintaining compatibility
59/// - Supporting multiple trace context formats in a single service
60/// - Using environment-specific propagation strategies
61#[derive(Debug)]
62pub struct TextMapSplitPropagator {
63    extract_propagator: Propagator,
64    inject_propagator: Propagator,
65    fields: Vec<String>,
66}
67
68impl TextMapSplitPropagator {
69    /// Creates a new split propagator with separate propagators for extraction and injection.
70    ///
71    /// # Arguments
72    ///
73    /// - `extract_propagator`: Propagator used for extracting context from incoming requests
74    /// - `inject_propagator`: Propagator used for injecting context into outgoing requests
75    ///
76    /// # Returns
77    ///
78    /// A new [`TextMapSplitPropagator`] instance
79    pub fn new(extract_propagator: Propagator, inject_propagator: Propagator) -> Self {
80        let mut fields = BTreeSet::from_iter(extract_propagator.fields());
81        fields.extend(inject_propagator.fields());
82        let fields = fields.into_iter().map(String::from).collect();
83
84        Self {
85            extract_propagator,
86            inject_propagator,
87            fields,
88        }
89    }
90
91    /// Creates a split propagator based on the `OTEL_PROPAGATORS` environment variable.
92    ///
93    /// This method reads the `OTEL_PROPAGATORS` environment variable to determine which
94    /// propagators to use. The first propagator in the list is used for injection,
95    /// while all propagators are composed together for extraction.
96    ///
97    /// # Environment Variable Format
98    ///
99    /// The `OTEL_PROPAGATORS` variable should contain a comma-separated list of propagator names:
100    /// - `tracecontext`: W3C Trace Context propagator
101    /// - `baggage`: W3C Baggage propagator
102    /// - `b3`: B3 single header propagator (requires "zipkin" feature)
103    /// - `b3multi`: B3 multiple header propagator (requires "zipkin" feature)
104    /// - `xray`: AWS X-Ray propagator (requires "xray" feature)
105    /// - `none`: No-op propagator
106    ///
107    /// # Returns
108    ///
109    /// A configured [`TextMapSplitPropagator`] on success, or an [`OTelSdkError`] if
110    /// the environment variable contains unsupported propagator names.
111    ///
112    /// # Examples
113    ///
114    /// ```bash
115    /// export OTEL_PROPAGATORS=tracecontext,baggage
116    /// ```
117    ///
118    /// ```rust
119    /// use telemetry_rust::propagation::TextMapSplitPropagator;
120    ///
121    /// let propagator = TextMapSplitPropagator::from_env()?;
122    /// # Ok::<(), opentelemetry_sdk::error::OTelSdkError>(())
123    /// ```
124    pub fn from_env() -> Result<Self, OTelSdkError> {
125        let value_from_env = match util::env_var("OTEL_PROPAGATORS") {
126            Some(value) => value,
127            None => {
128                return Ok(Self::default());
129            }
130        };
131        let propagators: Vec<String> = value_from_env
132            .split(',')
133            .map(|s| s.trim().to_lowercase())
134            .filter(|s| !s.is_empty())
135            .collect();
136        tracing::info!(target: "otel::setup", propagators = propagators.join(","));
137
138        let inject_propagator = match propagators.first() {
139            Some(s) => propagator_from_string(s)?,
140            None => Box::new(NonePropagator),
141        };
142        let propagators = propagators
143            .iter()
144            .rev()
145            .map(|s| propagator_from_string(s))
146            .collect::<Result<Vec<_>, _>>()?;
147        let extract_propagator = Box::new(TextMapCompositePropagator::new(propagators));
148
149        Ok(Self::new(extract_propagator, inject_propagator))
150    }
151}
152
153impl TextMapPropagator for TextMapSplitPropagator {
154    fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
155        self.inject_propagator.inject_context(cx, injector)
156    }
157
158    fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context {
159        self.extract_propagator.extract_with_context(cx, extractor)
160    }
161
162    fn fields(&self) -> FieldIter<'_> {
163        FieldIter::new(self.fields.as_slice())
164    }
165}
166
167impl Default for TextMapSplitPropagator {
168    fn default() -> Self {
169        let trace_context_propagator = Box::new(TraceContextPropagator::new());
170        #[cfg(feature = "zipkin")]
171        #[allow(deprecated)]
172        let b3_propagator = Box::new(B3Propagator::with_encoding(
173            B3Encoding::SingleAndMultiHeader,
174        ));
175        let composite_propagator = Box::new(TextMapCompositePropagator::new(vec![
176            trace_context_propagator.clone(),
177            #[cfg(feature = "zipkin")]
178            b3_propagator,
179        ]));
180
181        Self::new(composite_propagator, trace_context_propagator)
182    }
183}
184
185fn propagator_from_string(v: &str) -> Result<Propagator, OTelSdkError> {
186    match v.trim() {
187        "tracecontext" => Ok(Box::new(TraceContextPropagator::new())),
188        "baggage" => Ok(Box::new(BaggagePropagator::new())),
189        "none" => Ok(Box::new(NonePropagator)),
190        #[cfg(feature = "zipkin")]
191        #[allow(deprecated)]
192        "b3" => Ok(Box::new(B3Propagator::with_encoding(
193            B3Encoding::SingleHeader,
194        ))),
195        #[cfg(not(feature = "zipkin"))]
196        "b3" => Err(OTelSdkError::InternalFailure(
197            "unsupported propagator from env OTEL_PROPAGATORS: 'b3', try to enable compile feature 'zipkin'"
198                .to_owned(),
199        )),
200        #[cfg(feature = "zipkin")]
201        #[allow(deprecated)]
202        "b3multi" => Ok(Box::new(B3Propagator::with_encoding(
203            B3Encoding::MultipleHeader,
204        ))),
205        #[cfg(not(feature = "zipkin"))]
206        "b3multi" => Err(OTelSdkError::InternalFailure(
207            "unsupported propagator from env OTEL_PROPAGATORS: 'b3multi', try to enable compile feature 'zipkin'"
208                .to_owned(),
209        )),
210        #[cfg(feature = "xray")]
211        "xray" => Ok(Box::new(XrayPropagator::new())),
212        #[cfg(not(feature = "xray"))]
213        "xray" => Err(OTelSdkError::InternalFailure(
214            "unsupported propagator from env OTEL_PROPAGATORS: 'xray', try to enable compile feature 'xray'"
215                .to_owned(),
216        )),
217        unknown => Err(OTelSdkError::InternalFailure(format!(
218            "unsupported propagator from env OTEL_PROPAGATORS: {unknown:?}"
219        ))),
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use assert2::assert;
226
227    #[test]
228    fn init_tracing_failed_on_invalid_propagator() {
229        assert!(let Err(_) = super::propagator_from_string("xxxxxx"));
230    }
231}