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