Skip to content

Commit ab629c3

Browse files
authored
adding support for setting the status of the span through SpanExt (#176)
## Motivation I have found that when working with `tracing` spans created by 3rd party libraries, it is incredibly difficult to set the OpenTelemetry Status of the span, which results in always having an unset span in my visualization tool. <img width="623" alt="image" src="https://github.com/user-attachments/assets/7acaf443-3d7b-40b4-80c3-3c2d588679d1"> In this case, I am working with the span created by the `aws_lambda_runtime` [crate](https://github.com/awslabs/aws-lambda-rust-runtime/blob/main/lambda-runtime/src/layers/otel.rs#L75). Unfortunately, since the span in the `aws_lambda_runtime` crate does not include the `otel.status_code` attribute upfront the `OpenTelemetryLayer::SpanAttributeVisitor::record_str` and `OpenTelemetryLayer::SpanAttributeVisitor::record_debug` never gets triggered when calling `tracing::Span::current().record("otel.status_code", "ok");` This behavior of the `record` function ignoring new fields is documented [here](https://docs.rs/tracing/latest/tracing/struct.Span.html#method.record) ## Solution My solution to this is add a new member `OpenTelemetrySpanExt::set_status` that takes an `opentelemetry::trace::Status` enum that gets written to the underlying `SpanBuilder`.
1 parent 515fe00 commit ab629c3

File tree

2 files changed

+107
-1
lines changed

2 files changed

+107
-1
lines changed

src/span_ext.rs

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::layer::WithContext;
2-
use opentelemetry::{trace::SpanContext, Context, Key, KeyValue, Value};
2+
use opentelemetry::{trace::SpanContext, trace::Status, Context, Key, KeyValue, Value};
33

44
/// Utility functions to allow tracing [`Span`]s to accept and return
55
/// [OpenTelemetry] [`Context`]s.
@@ -133,6 +133,25 @@ pub trait OpenTelemetrySpanExt {
133133
/// app_root.set_attribute("http.request.header.x_forwarded_for", "example");
134134
/// ```
135135
fn set_attribute(&self, key: impl Into<Key>, value: impl Into<Value>);
136+
137+
/// Sets an OpenTelemetry status for this span.
138+
/// This is useful for setting the status of a span that was created by a library that does not declare
139+
/// the otel.status_code field of the span in advance.
140+
///
141+
/// # Examples
142+
///
143+
/// ```rust
144+
/// use opentelemetry::trace::Status;
145+
/// use tracing_opentelemetry::OpenTelemetrySpanExt;
146+
/// use tracing::Span;
147+
///
148+
/// /// // Generate a tracing span as usual
149+
/// let app_root = tracing::span!(tracing::Level::INFO, "app_start");
150+
///
151+
/// // Set the Status of the span to `Status::Ok`.
152+
/// app_root.set_status(Status::Ok);
153+
/// ```
154+
fn set_status(&self, status: Status);
136155
}
137156

138157
impl OpenTelemetrySpanExt for tracing::Span {
@@ -207,4 +226,15 @@ impl OpenTelemetrySpanExt for tracing::Span {
207226
}
208227
});
209228
}
229+
230+
fn set_status(&self, status: Status) {
231+
self.with_subscriber(move |(id, subscriber)| {
232+
let mut status = Some(status);
233+
if let Some(get_context) = subscriber.downcast_ref::<WithContext>() {
234+
get_context.with_context(subscriber, id, move |builder, _| {
235+
builder.builder.status = status.take().unwrap();
236+
});
237+
}
238+
});
239+
}
210240
}

tests/span_ext.rs

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use futures_util::future::BoxFuture;
2+
use opentelemetry::trace::{Status, TracerProvider as _};
3+
use opentelemetry_sdk::{
4+
export::trace::{ExportResult, SpanData, SpanExporter},
5+
trace::{Tracer, TracerProvider},
6+
};
7+
use std::sync::{Arc, Mutex};
8+
use tracing::level_filters::LevelFilter;
9+
use tracing::Subscriber;
10+
use tracing_opentelemetry::{layer, OpenTelemetrySpanExt};
11+
use tracing_subscriber::prelude::*;
12+
13+
#[derive(Clone, Default, Debug)]
14+
struct TestExporter(Arc<Mutex<Vec<SpanData>>>);
15+
16+
impl SpanExporter for TestExporter {
17+
fn export(&mut self, mut batch: Vec<SpanData>) -> BoxFuture<'static, ExportResult> {
18+
let spans = self.0.clone();
19+
Box::pin(async move {
20+
if let Ok(mut inner) = spans.lock() {
21+
inner.append(&mut batch);
22+
}
23+
Ok(())
24+
})
25+
}
26+
}
27+
28+
fn test_tracer() -> (Tracer, TracerProvider, TestExporter, impl Subscriber) {
29+
let exporter = TestExporter::default();
30+
let provider = TracerProvider::builder()
31+
.with_simple_exporter(exporter.clone())
32+
.build();
33+
let tracer = provider.tracer("test");
34+
35+
let subscriber = tracing_subscriber::registry()
36+
.with(
37+
layer()
38+
.with_tracer(tracer.clone())
39+
.with_filter(LevelFilter::DEBUG),
40+
)
41+
.with(tracing_subscriber::fmt::layer().with_filter(LevelFilter::TRACE));
42+
43+
(tracer, provider, exporter, subscriber)
44+
}
45+
46+
#[test]
47+
fn set_status_ok() {
48+
let root_span = set_status_helper(Status::Ok);
49+
assert_eq!(Status::Ok, root_span.status);
50+
}
51+
52+
#[test]
53+
fn set_status_error() {
54+
let expected_error = Status::Error {
55+
description: std::borrow::Cow::Borrowed("Elon put in too much fuel in his rocket!"),
56+
};
57+
let root_span = set_status_helper(expected_error.clone());
58+
assert_eq!(expected_error, root_span.status);
59+
}
60+
61+
fn set_status_helper(status: Status) -> SpanData {
62+
let (_tracer, provider, exporter, subscriber) = test_tracer();
63+
64+
tracing::subscriber::with_default(subscriber, || {
65+
let root = tracing::debug_span!("root").entered();
66+
67+
root.set_status(status);
68+
});
69+
70+
drop(provider); // flush all spans
71+
let spans = exporter.0.lock().unwrap();
72+
73+
assert_eq!(spans.len(), 1);
74+
75+
spans.iter().find(|s| s.name == "root").unwrap().clone()
76+
}

0 commit comments

Comments
 (0)