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