tracing_opentelemetry/
span_ext.rs

1use crate::OtelData;
2use crate::{layer::WithContext, OtelDataState};
3use opentelemetry::{
4    time,
5    trace::{SpanContext, Status, TraceContextExt},
6    Context, Key, KeyValue, Value,
7};
8use std::{borrow::Cow, fmt, time::SystemTime};
9
10/// Utility functions to allow tracing [`Span`]s to accept and return
11/// [OpenTelemetry] [`Context`]s.
12///
13/// [`Span`]: tracing::Span
14/// [OpenTelemetry]: https://opentelemetry.io
15/// [`Context`]: opentelemetry::Context
16pub trait OpenTelemetrySpanExt {
17    /// Associates `self` with a given OpenTelemetry trace, using the provided
18    /// parent [`Context`].
19    ///
20    /// This method exists primarily to make it possible to inject a _distributed_ incoming
21    /// context, e.g. span IDs, etc.
22    ///
23    /// A span's parent should only be set _once_, for the purpose described above.
24    /// Additionally, once a span has been fully built - and the SpanBuilder has been
25    /// consumed - the parent _cannot_ be mutated.
26    ///
27    /// This method provides error handling for cases where the span context
28    /// cannot be set, such as when the OpenTelemetry layer is not present
29    /// or when the span has already been started.
30    ///
31    /// [`Context`]: opentelemetry::Context
32    ///
33    /// # Examples
34    ///
35    /// ```rust
36    /// use opentelemetry::{propagation::TextMapPropagator, trace::TraceContextExt};
37    /// use opentelemetry_sdk::propagation::TraceContextPropagator;
38    /// use tracing_opentelemetry::OpenTelemetrySpanExt;
39    /// use std::collections::HashMap;
40    /// use tracing::Span;
41    ///
42    /// // Example carrier, could be a framework header map that impls otel's `Extractor`.
43    /// let mut carrier = HashMap::new();
44    ///
45    /// // Propagator can be swapped with b3 propagator, jaeger propagator, etc.
46    /// let propagator = TraceContextPropagator::new();
47    ///
48    /// // Extract otel parent context via the chosen propagator
49    /// let parent_context = propagator.extract(&carrier);
50    ///
51    /// // Generate a tracing span as usual
52    /// let app_root = tracing::span!(tracing::Level::INFO, "app_start");
53    ///
54    /// // Assign parent trace from external context
55    /// let _ = app_root.set_parent(parent_context.clone());
56    ///
57    /// // Or if the current span has been created elsewhere:
58    /// let _ = Span::current().set_parent(parent_context);
59    /// ```
60    fn set_parent(&self, cx: Context) -> Result<(), SetParentError>;
61
62    /// Associates `self` with a given OpenTelemetry trace, using the provided
63    /// followed span [`SpanContext`].
64    ///
65    /// [`SpanContext`]: opentelemetry::trace::SpanContext
66    ///
67    /// # Examples
68    ///
69    /// ```rust
70    /// use opentelemetry::{propagation::TextMapPropagator, trace::TraceContextExt};
71    /// use opentelemetry_sdk::propagation::TraceContextPropagator;
72    /// use tracing_opentelemetry::OpenTelemetrySpanExt;
73    /// use std::collections::HashMap;
74    /// use tracing::Span;
75    ///
76    /// // Example carrier, could be a framework header map that impls otel's `Extractor`.
77    /// let mut carrier = HashMap::new();
78    ///
79    /// // Propagator can be swapped with b3 propagator, jaeger propagator, etc.
80    /// let propagator = TraceContextPropagator::new();
81    ///
82    /// // Extract otel context of linked span via the chosen propagator
83    /// let linked_span_otel_context = propagator.extract(&carrier);
84    ///
85    /// // Extract the linked span context from the otel context
86    /// let linked_span_context = linked_span_otel_context.span().span_context().clone();
87    ///
88    /// // Generate a tracing span as usual
89    /// let app_root = tracing::span!(tracing::Level::INFO, "app_start");
90    ///
91    /// // Assign linked trace from external context
92    /// app_root.add_link(linked_span_context);
93    ///
94    /// // Or if the current span has been created elsewhere:
95    /// let linked_span_context = linked_span_otel_context.span().span_context().clone();
96    /// Span::current().add_link(linked_span_context);
97    /// ```
98    fn add_link(&self, cx: SpanContext);
99
100    /// Associates `self` with a given OpenTelemetry trace, using the provided
101    /// followed span [`SpanContext`] and attributes.
102    ///
103    /// [`SpanContext`]: opentelemetry::trace::SpanContext
104    fn add_link_with_attributes(&self, cx: SpanContext, attributes: Vec<KeyValue>);
105
106    /// Extracts an OpenTelemetry [`Context`] from `self`.
107    ///
108    /// [`Context`]: opentelemetry::Context
109    ///
110    /// # Examples
111    ///
112    /// ```rust
113    /// use opentelemetry::Context;
114    /// use tracing_opentelemetry::OpenTelemetrySpanExt;
115    /// use tracing::Span;
116    ///
117    /// fn make_request(cx: Context) {
118    ///     // perform external request after injecting context
119    ///     // e.g. if the request's headers impl `opentelemetry::propagation::Injector`
120    ///     // then `propagator.inject_context(cx, request.headers_mut())`
121    /// }
122    ///
123    /// // Generate a tracing span as usual
124    /// let app_root = tracing::span!(tracing::Level::INFO, "app_start");
125    ///
126    /// // To include tracing context in client requests from _this_ app,
127    /// // extract the current OpenTelemetry context.
128    /// make_request(app_root.context());
129    ///
130    /// // Or if the current span has been created elsewhere:
131    /// make_request(Span::current().context())
132    /// ```
133    fn context(&self) -> Context;
134
135    /// Sets an OpenTelemetry attribute directly for this span, bypassing `tracing`.
136    /// If fields set here conflict with `tracing` fields, the `tracing` fields will supersede fields set with `set_attribute`.
137    /// This allows for more than 32 fields.
138    ///
139    /// # Examples
140    ///
141    /// ```rust
142    /// use opentelemetry::Context;
143    /// use tracing_opentelemetry::OpenTelemetrySpanExt;
144    /// use tracing::Span;
145    ///
146    /// // Generate a tracing span as usual
147    /// let app_root = tracing::span!(tracing::Level::INFO, "app_start");
148    ///
149    /// // Set the `http.request.header.x_forwarded_for` attribute to `example`.
150    /// app_root.set_attribute("http.request.header.x_forwarded_for", "example");
151    /// ```
152    fn set_attribute(&self, key: impl Into<Key>, value: impl Into<Value>);
153
154    /// Sets an OpenTelemetry status for this span.
155    /// This is useful for setting the status of a span that was created by a library that does not declare
156    /// the otel.status_code field of the span in advance.
157    ///
158    /// # Examples
159    ///
160    /// ```rust
161    /// use opentelemetry::trace::Status;
162    /// use tracing_opentelemetry::OpenTelemetrySpanExt;
163    /// use tracing::Span;
164    ///
165    /// /// // Generate a tracing span as usual
166    /// let app_root = tracing::span!(tracing::Level::INFO, "app_start");
167    ///
168    /// // Set the Status of the span to `Status::Ok`.
169    /// app_root.set_status(Status::Ok);
170    /// ```            
171    fn set_status(&self, status: Status);
172
173    /// Adds an OpenTelemetry event directly to this span, bypassing `tracing::event!`.
174    /// This allows for adding events with dynamic attribute keys, similar to `set_attribute` for span attributes.
175    /// Events are added with the current timestamp.
176    ///
177    /// # Examples
178    ///
179    /// ```rust
180    /// use opentelemetry::{KeyValue};
181    /// use tracing_opentelemetry::OpenTelemetrySpanExt;
182    /// use tracing::Span;
183    ///
184    /// let app_root = tracing::span!(tracing::Level::INFO, "processing_request");
185    ///
186    /// let dynamic_attrs = vec![
187    ///     KeyValue::new("job_id", "job-123"),
188    ///     KeyValue::new("user.id", "user-xyz"),
189    /// ];
190    ///
191    /// // Add event using the extension method
192    /// app_root.add_event("job_started".to_string(), dynamic_attrs);
193    ///
194    /// // ... perform work ...
195    ///
196    /// app_root.add_event("job_completed", vec![KeyValue::new("status", "success")]);
197    /// ```
198    fn add_event(&self, name: impl Into<Cow<'static, str>>, attributes: Vec<KeyValue>);
199
200    /// Adds an OpenTelemetry event with a specific timestamp directly to this span.
201    /// Similar to `add_event`, but allows overriding the event timestamp.
202    ///
203    /// # Examples
204    ///
205    /// ```rust
206    /// use opentelemetry::{KeyValue};
207    /// use tracing_opentelemetry::OpenTelemetrySpanExt;
208    /// use tracing::Span;
209    /// use std::time::{Duration, SystemTime};
210    /// use std::borrow::Cow;
211    ///
212    /// let app_root = tracing::span!(tracing::Level::INFO, "historical_event_processing");
213    ///
214    /// let event_time = SystemTime::now() - Duration::from_secs(60);
215    /// let event_attrs = vec![KeyValue::new("record_id", "rec-456")];
216    /// let event_name: Cow<'static, str> = "event_from_past".into();
217    ///
218    /// app_root.add_event_with_timestamp(event_name, event_time, event_attrs);
219    /// ```
220    fn add_event_with_timestamp(
221        &self,
222        name: impl Into<Cow<'static, str>>,
223        timestamp: SystemTime,
224        attributes: Vec<KeyValue>,
225    );
226}
227
228/// An error returned if [`OpenTelemetrySpanExt::set_parent`] could not set the parent.
229#[derive(Debug)]
230pub enum SetParentError {
231    /// The layer could not be found and therefore the action could not be carried out. This can
232    /// happen with some advanced layers that do not handle downcasting well, for example
233    /// [`tracing_subscriber::reload::Layer`].
234    LayerNotFound,
235
236    /// The span has been already started.
237    ///
238    /// Someone already called a context-starting method such as [`OpenTelemetrySpanExt::context`]
239    /// or the span has been entered and automatic context starting was not configured out.
240    AlreadyStarted,
241
242    /// The span is filtered out by tracing filters.
243    ///
244    /// If the filtered out span had children, they will not be connected to the parent span either.
245    SpanDisabled,
246}
247
248impl fmt::Display for SetParentError {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            SetParentError::LayerNotFound => {
252                write!(f, "OpenTelemetry layer not found")
253            }
254            SetParentError::AlreadyStarted => {
255                write!(f, "Span has already been started, cannot set parent")
256            }
257            SetParentError::SpanDisabled => {
258                write!(f, "Span disabled")
259            }
260        }
261    }
262}
263
264impl std::error::Error for SetParentError {}
265
266impl OpenTelemetrySpanExt for tracing::Span {
267    fn set_parent(&self, cx: Context) -> Result<(), SetParentError> {
268        let mut cx = Some(cx);
269
270        self.with_subscriber(move |(id, subscriber)| {
271            let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
272                return Err(SetParentError::LayerNotFound);
273            };
274
275            let mut result = Ok(());
276            let result_ref = &mut result;
277            // Set the parent OTel for the current span
278            get_context.with_context(subscriber, id, move |data| {
279                let Some(new_cx) = cx.take() else {
280                    *result_ref = Err(SetParentError::AlreadyStarted);
281                    return;
282                };
283                // Create a new context with the new parent but preserve our span.
284                // NOTE - if the span has been created - if we have _already_
285                // consumed our SpanBuilder_ - we can no longer mutate our parent!
286                // This is an intentional design decision.
287                match &mut data.state {
288                    OtelDataState::Builder { parent_cx, .. } => {
289                        // If we still have a builder, update the data so it uses the
290                        // new parent context when it's eventually built
291                        *parent_cx = new_cx;
292                    }
293                    OtelDataState::Context { .. } => {
294                        *result_ref = Err(SetParentError::AlreadyStarted);
295                    }
296                }
297            });
298            result
299        })
300        .unwrap_or(Err(SetParentError::SpanDisabled))
301    }
302
303    fn add_link(&self, cx: SpanContext) {
304        self.add_link_with_attributes(cx, Vec::new())
305    }
306
307    fn add_link_with_attributes(&self, cx: SpanContext, attributes: Vec<KeyValue>) {
308        if cx.is_valid() {
309            let mut cx = Some(cx);
310            let mut att = Some(attributes);
311            self.with_subscriber(move |(id, subscriber)| {
312                let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
313                    return;
314                };
315                get_context.with_context(subscriber, id, move |data| {
316                    let Some(cx) = cx.take() else {
317                        return;
318                    };
319                    let attr = att.take().unwrap_or_default();
320                    let follows_link = opentelemetry::trace::Link::new(cx, attr, 0);
321                    match &mut data.state {
322                        OtelDataState::Builder { builder, .. } => {
323                            // If we still have a builder, update the data so it uses the
324                            // new link when it's eventually built
325                            builder
326                                .links
327                                .get_or_insert_with(|| Vec::with_capacity(1))
328                                .push(follows_link);
329                        }
330                        OtelDataState::Context { current_cx } => {
331                            // If we have a context, add the link to the span in the context
332                            current_cx
333                                .span()
334                                .add_link(follows_link.span_context, follows_link.attributes);
335                        }
336                    }
337                });
338            });
339        }
340    }
341
342    fn context(&self) -> Context {
343        let mut cx = None;
344        self.with_subscriber(|(id, subscriber)| {
345            let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
346                return;
347            };
348            // If our span hasn't been built, we should build it and get the context in one call
349            get_context.with_activated_context(subscriber, id, |data: &mut OtelData| {
350                if let OtelDataState::Context { current_cx } = &data.state {
351                    cx = Some(current_cx.clone());
352                }
353            });
354        });
355
356        cx.unwrap_or_default()
357    }
358
359    fn set_attribute(&self, key: impl Into<Key>, value: impl Into<Value>) {
360        self.with_subscriber(move |(id, subscriber)| {
361            let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
362                return;
363            };
364            let mut key_value = Some(KeyValue::new(key.into(), value.into()));
365            get_context.with_context(subscriber, id, move |data| {
366                match &mut data.state {
367                    OtelDataState::Builder { builder, .. } => {
368                        if builder.attributes.is_none() {
369                            builder.attributes = Some(Default::default());
370                        }
371                        builder
372                            .attributes
373                            .as_mut()
374                            .unwrap()
375                            .push(key_value.take().unwrap());
376                    }
377                    OtelDataState::Context { current_cx } => {
378                        let span = current_cx.span();
379                        span.set_attribute(key_value.take().unwrap());
380                    }
381                };
382            });
383        });
384    }
385
386    fn set_status(&self, status: Status) {
387        self.with_subscriber(move |(id, subscriber)| {
388            let mut status = Some(status);
389            let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
390                return;
391            };
392            get_context.with_context(subscriber, id, move |data| match &mut data.state {
393                OtelDataState::Builder { status: s, .. } => {
394                    *s = status.take().unwrap();
395                }
396                OtelDataState::Context { current_cx } => {
397                    let span = current_cx.span();
398                    span.set_status(status.take().unwrap());
399                }
400            });
401        });
402    }
403
404    fn add_event(&self, name: impl Into<Cow<'static, str>>, attributes: Vec<KeyValue>) {
405        self.add_event_with_timestamp(name, time::now(), attributes);
406    }
407
408    fn add_event_with_timestamp(
409        &self,
410        name: impl Into<Cow<'static, str>>,
411        timestamp: SystemTime,
412        attributes: Vec<KeyValue>,
413    ) {
414        self.with_subscriber(move |(id, subscriber)| {
415            let mut event = Some(opentelemetry::trace::Event::new(
416                name, timestamp, attributes, 0,
417            ));
418            let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
419                return;
420            };
421            get_context.with_context(subscriber, id, move |data| {
422                let Some(event) = event.take() else {
423                    return;
424                };
425                match &mut data.state {
426                    OtelDataState::Builder { builder, .. } => {
427                        builder
428                            .events
429                            .get_or_insert_with(|| Vec::with_capacity(1))
430                            .push(event);
431                    }
432                    OtelDataState::Context { current_cx } => {
433                        let span = current_cx.span();
434                        span.add_event_with_timestamp(
435                            event.name,
436                            event.timestamp,
437                            event.attributes,
438                        );
439                    }
440                }
441            });
442        });
443    }
444}