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}