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