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}