Skip to content

Commit eb50246

Browse files
scottgerringbantonsson
authored andcommitted
fix: tracing context tests and context activation
Ensure that the we materialize the span and activate the context when accessing the span. This ensures the correct parent child relationships.
1 parent 6f8aecd commit eb50246

File tree

5 files changed

+231
-33
lines changed

5 files changed

+231
-33
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ Cargo.lock
5050

5151

5252
# End of https://www.gitignore.io/api/rust,macos,visualstudiocode
53+
.cursor

benches/trace.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,7 @@ where
264264
let span = ctx.span(&id).expect("Span not found, this is a bug");
265265
let mut extensions = span.extensions_mut();
266266

267-
if let Some(builder) = extensions.remove::<SpanBuilder>() {
268-
builder.with_end_time(SystemTime::now());
269-
}
267+
extensions.remove::<SpanBuilder>();
270268
}
271269
}
272270

src/layer.rs

Lines changed: 199 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub struct OpenTelemetryLayer<S, T> {
4747
with_threads: bool,
4848
with_level: bool,
4949
sem_conv_config: SemConvConfig,
50-
get_context: WithContext,
50+
with_context: WithContext,
5151
_registry: marker::PhantomData<S>,
5252
}
5353

@@ -82,26 +82,58 @@ where
8282
OpenTelemetryLayer::default()
8383
}
8484

85-
// this function "remembers" the types of the subscriber so that we
86-
// can downcast to something aware of them without knowing those
87-
// types at the callsite.
88-
//
89-
// See https://github.com/tokio-rs/tracing/blob/4dad420ee1d4607bad79270c1520673fa6266a3d/tracing-error/src/layer.rs
90-
pub(crate) struct WithContext(
85+
///
86+
/// This struct lets us call back into the layer from the [crate::OpenTelemetrySpanExt] methods,
87+
/// letting us access and mutate the underlying data on the layer side in the context of
88+
/// tokio-tracing's span operations.
89+
///
90+
/// The functions on this struct "remember" the types of the subscriber so that we
91+
/// can downcast to something aware of them without knowing those
92+
/// types at the callsite.
93+
///
94+
/// See https://github.com/tokio-rs/tracing/blob/4dad420ee1d4607bad79270c1520673fa6266a3d/tracing-error/src/layer.rs
95+
pub(crate) struct WithContext {
96+
///
97+
/// Provides access to the OtelData associated with the given span ID.
98+
///
9199
#[allow(clippy::type_complexity)]
92-
fn(&tracing::Dispatch, &span::Id, f: &mut dyn FnMut(&mut OtelData)),
93-
);
100+
pub(crate) with_context: fn(&tracing::Dispatch, &span::Id, f: &mut dyn FnMut(&mut OtelData)),
101+
102+
///
103+
/// Ensures the given SpanId has been activated - that is, created in the OTel side of things,
104+
/// and had its SpanBuilder consumed - and then provides access to the OtelData associated with it.
105+
///
106+
#[allow(clippy::type_complexity)]
107+
pub(crate) with_activated_context:
108+
fn(&tracing::Dispatch, &span::Id, f: &mut dyn FnMut(&mut OtelData)),
109+
}
94110

95111
impl WithContext {
96-
// This function allows a function to be called in the context of the
97-
// "remembered" subscriber.
112+
///
113+
/// Return the OtelData associated with the given spanId.
114+
///
98115
pub(crate) fn with_context(
99116
&self,
100117
dispatch: &tracing::Dispatch,
101118
id: &span::Id,
102119
mut f: impl FnMut(&mut OtelData),
103120
) {
104-
(self.0)(dispatch, id, &mut f)
121+
(self.with_context)(dispatch, id, &mut f)
122+
}
123+
124+
///
125+
/// If the span associated with the given SpanId has not yet been
126+
/// built, build it, consuming the span ID.
127+
///
128+
/// Optionally performs additional operations on the OtelData after building.
129+
///
130+
pub(crate) fn with_activated_context(
131+
&self,
132+
dispatch: &tracing::Dispatch,
133+
id: &span::Id,
134+
mut f: impl FnMut(&mut OtelData),
135+
) {
136+
(self.with_activated_context)(dispatch, id, &mut f)
105137
}
106138
}
107139

@@ -609,8 +641,10 @@ where
609641
error_events_to_exceptions: true,
610642
error_events_to_status: true,
611643
},
612-
613-
get_context: WithContext(Self::get_context),
644+
with_context: WithContext {
645+
with_context: Self::get_context,
646+
with_activated_context: Self::get_activated_context,
647+
},
614648
_registry: marker::PhantomData,
615649
}
616650
}
@@ -660,7 +694,10 @@ where
660694
with_threads: self.with_threads,
661695
with_level: self.with_level,
662696
sem_conv_config: self.sem_conv_config,
663-
get_context: WithContext(OpenTelemetryLayer::<S, Tracer>::get_context),
697+
with_context: WithContext {
698+
with_context: OpenTelemetryLayer::<S, Tracer>::get_context,
699+
with_activated_context: OpenTelemetryLayer::<S, Tracer>::get_activated_context,
700+
},
664701
_registry: self._registry,
665702
// cannot use ``..self` here due to different generics
666703
}
@@ -859,6 +896,18 @@ where
859896
}
860897
}
861898

899+
/// Provides access to the OpenTelemetry data (`OtelData`) stored in a tracing span.
900+
///
901+
/// This function retrieves the span from the subscriber's registry using the provided span ID,
902+
/// and then applies the callback function `f` to the span's `OtelData` if present.
903+
///
904+
/// # Parameters
905+
/// * `dispatch` - A reference to the tracing dispatch, used to access the subscriber
906+
/// * `id` - The ID of the span to look up
907+
/// * `f` - A callback function that receives a mutable reference to the span's `OtelData`
908+
/// This callback is used to manipulate or extract information from the OpenTelemetry context
909+
/// associated with the tracing span
910+
///
862911
fn get_context(dispatch: &tracing::Dispatch, id: &span::Id, f: &mut dyn FnMut(&mut OtelData)) {
863912
let subscriber = dispatch
864913
.downcast_ref::<S>()
@@ -873,6 +922,30 @@ where
873922
}
874923
}
875924

925+
fn get_activated_context(
926+
dispatch: &tracing::Dispatch,
927+
id: &span::Id,
928+
f: &mut dyn FnMut(&mut OtelData),
929+
) {
930+
let subscriber = dispatch
931+
.downcast_ref::<S>()
932+
.expect("subscriber should downcast to expected type; this is a bug!");
933+
let span = subscriber
934+
.span(id)
935+
.expect("registry should have a span for the current ID");
936+
937+
let layer = dispatch
938+
.downcast_ref::<OpenTelemetryLayer<S, T>>()
939+
.expect("layer should downcast to expected type; this is a bug!");
940+
941+
let mut extensions = span.extensions_mut();
942+
if let Some(otel_data) = extensions.get_mut::<OtelData>() {
943+
// Activate the context
944+
layer.start_cx(otel_data);
945+
f(otel_data);
946+
}
947+
}
948+
876949
fn extra_span_attrs(&self) -> usize {
877950
let mut extra_attrs = 0;
878951
if self.location {
@@ -887,6 +960,10 @@ where
887960
extra_attrs
888961
}
889962

963+
///
964+
/// Builds the OTel span associated with given OTel context, consuming the SpanBuilder within
965+
/// the context in the process.
966+
///
890967
fn start_cx(&self, otel_data: &mut OtelData) {
891968
if let Some(builder) = otel_data.builder.take() {
892969
let span = builder.start_with_context(&self.tracer, &otel_data.parent_cx);
@@ -1220,6 +1297,7 @@ where
12201297
/// [`Span`]: opentelemetry::trace::Span
12211298
fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
12221299
let span = ctx.span(&id).expect("Span not found, this is a bug");
1300+
// Now get mutable extensions for removal
12231301
let (otel_data, timings) = {
12241302
let mut extensions = span.extensions_mut();
12251303
let timings = if self.tracked_inactivity {
@@ -1249,10 +1327,10 @@ where
12491327
let busy_ns = Key::new("busy_ns");
12501328
let idle_ns = Key::new("idle_ns");
12511329

1252-
let mut attributes = Vec::with_capacity(2);
1253-
attributes.push(KeyValue::new(busy_ns, timings.busy));
1254-
attributes.push(KeyValue::new(idle_ns, timings.idle));
1255-
span.set_attributes(attributes);
1330+
span.set_attributes(vec![
1331+
KeyValue::new(busy_ns, timings.busy),
1332+
KeyValue::new(idle_ns, timings.idle),
1333+
]);
12561334
}
12571335

12581336
if let Some(end_time) = end_time {
@@ -1269,7 +1347,7 @@ where
12691347
match id {
12701348
id if id == TypeId::of::<Self>() => Some(self as *const _ as *const ()),
12711349
id if id == TypeId::of::<WithContext>() => {
1272-
Some(&self.get_context as *const _ as *const ())
1350+
Some(&self.with_context as *const _ as *const ())
12731351
}
12741352
_ => None,
12751353
}
@@ -1303,10 +1381,13 @@ fn thread_id_integer(id: thread::ThreadId) -> u64 {
13031381

13041382
#[cfg(test)]
13051383
mod tests {
1384+
use crate::OpenTelemetrySpanExt;
1385+
13061386
use super::*;
13071387
use opentelemetry::trace::{SpanContext, TraceFlags, TracerProvider};
13081388
use opentelemetry_sdk::trace::SpanExporter;
13091389
use std::{collections::HashMap, error::Error, fmt::Display, time::SystemTime};
1390+
use tracing::trace_span;
13101391
use tracing_subscriber::prelude::*;
13111392

13121393
#[derive(Debug, Clone)]
@@ -1911,4 +1992,102 @@ mod tests {
19111992
// The second tokio child span should have the otel span as parent
19121993
assert_eq!(child2.parent_span_id, otel.span_context.span_id());
19131994
}
1995+
1996+
#[test]
1997+
fn parent_context() {
1998+
let mut tracer = TestTracer::default();
1999+
let subscriber = tracing_subscriber::registry().with(layer().with_tracer(tracer.clone()));
2000+
2001+
tracing::subscriber::with_default(subscriber, || {
2002+
let root = trace_span!("root");
2003+
2004+
let child1 = trace_span!("child-1");
2005+
let root_context = root.context(); // SpanData (None)
2006+
child1.set_parent(root_context); // Clone context, but SpanData(None)
2007+
2008+
let _enter_root = root.enter();
2009+
drop(_enter_root);
2010+
2011+
let child2 = trace_span!("child-2");
2012+
child2.set_parent(root.context());
2013+
});
2014+
2015+
// Let's check the spans
2016+
let spans = tracer.spans();
2017+
let parent = spans.iter().find(|span| span.name == "root").unwrap();
2018+
let child1 = spans.iter().find(|span| span.name == "child-1").unwrap();
2019+
let child2 = spans.iter().find(|span| span.name == "child-2").unwrap();
2020+
assert_eq!(parent.parent_span_id, otel::SpanId::INVALID);
2021+
assert_eq!(child1.parent_span_id, parent.span_context.span_id());
2022+
assert_eq!(child2.parent_span_id, parent.span_context.span_id());
2023+
}
2024+
2025+
#[test]
2026+
fn record_after() {
2027+
let mut tracer = TestTracer::default();
2028+
let subscriber = tracing_subscriber::registry().with(layer().with_tracer(tracer.clone()));
2029+
2030+
tracing::subscriber::with_default(subscriber, || {
2031+
let root = trace_span!("root", before = "before", after = "before");
2032+
2033+
// Record a value before the span is entered
2034+
root.record("before", "after");
2035+
2036+
// Enter and exit the span
2037+
let _enter_root = root.enter();
2038+
drop(_enter_root);
2039+
2040+
// Record a value after the span is exited
2041+
root.record("after", "after");
2042+
});
2043+
2044+
// Let's check the spans. Both values should've been
2045+
// updated to 'after'.
2046+
let spans = tracer.spans();
2047+
let parent = spans.iter().find(|span| span.name == "root").unwrap();
2048+
assert_eq!(parent.parent_span_id, otel::SpanId::INVALID);
2049+
assert!(parent
2050+
.attributes
2051+
.iter()
2052+
.filter(|kv| kv.key.as_str() == "before")
2053+
.any(|kv| kv.value.as_str() == "after"));
2054+
2055+
assert!(parent
2056+
.attributes
2057+
.iter()
2058+
.filter(|kv| kv.key.as_str() == "after")
2059+
.any(|kv| kv.value.as_str() == "after"));
2060+
}
2061+
2062+
#[test]
2063+
fn parent_context_2() {
2064+
let mut tracer = TestTracer::default();
2065+
let subscriber = tracing_subscriber::registry().with(layer().with_tracer(tracer.clone()));
2066+
2067+
tracing::subscriber::with_default(subscriber, || {
2068+
let root = trace_span!("root");
2069+
_ = root.enter();
2070+
2071+
let child1 = trace_span!("child-1");
2072+
child1.set_parent(root.context());
2073+
2074+
trace_span!(parent: &child1, "child-2");
2075+
child1.set_parent(root.context()); // <-- this is what causes the issue
2076+
2077+
trace_span!(parent: &child1, "child-3");
2078+
});
2079+
2080+
// Let's check the spans
2081+
let spans = tracer.spans();
2082+
let root = spans.iter().find(|span| span.name == "root").unwrap();
2083+
let child1 = spans.iter().find(|span| span.name == "child-1").unwrap();
2084+
let child2 = spans.iter().find(|span| span.name == "child-2").unwrap();
2085+
let child3 = spans.iter().find(|span| span.name == "child-3").unwrap();
2086+
assert_eq!(root.parent_span_id, otel::SpanId::INVALID);
2087+
assert_eq!(child1.parent_span_id, root.span_context.span_id());
2088+
assert_eq!(child2.parent_span_id, child1.span_context.span_id());
2089+
2090+
// This is surprising, the parent should be `child1`, but is 'root'.
2091+
assert_eq!(child3.parent_span_id, child1.span_context.span_id());
2092+
}
19142093
}

src/span_ext.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::layer::WithContext;
2+
use crate::OtelData;
23
use opentelemetry::{
34
time,
45
trace::{SpanContext, Status, TraceContextExt},
@@ -214,21 +215,36 @@ pub trait OpenTelemetrySpanExt {
214215
}
215216

216217
impl OpenTelemetrySpanExt for tracing::Span {
217-
// TODO:ban should this really operate on a SpanContext instead of a Context?
218+
///
219+
/// Allows us to set the parent context of this span. This method exists primarily to allow
220+
/// us to pull in distributed_ incoming context - e.g. span IDs, etc - that have been read
221+
/// into an existing context.
222+
///
223+
/// A span's parent should only be set _once_, for the purpose described above.
224+
/// Additionally, once a span has been fully built - and the SpanBuilder has been consumed -
225+
/// the parent _cannot_ be mutated.
226+
///
218227
fn set_parent(&self, cx: Context) {
219228
let mut cx = Some(cx);
220229
self.with_subscriber(move |(id, subscriber)| {
221230
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
222231
return;
223232
};
233+
// Set the parent OTel for the current span
224234
get_context.with_context(subscriber, id, move |data| {
225-
let Some(cx) = cx.take() else {
235+
let Some(new_cx) = cx.take() else {
226236
return;
227237
};
228-
data.parent_cx = cx;
229-
data.builder
230-
.as_mut()
231-
.map(|builder| builder.sampling_result = None);
238+
// Create a new context with the new parent but preserve our span.
239+
// NOTE - if the span has been created - if we have _already_
240+
// consumed our SpanBuilder_ - we can no longer mutate our parent!
241+
// This is an intentional design decision.
242+
if let Some(builder) = &mut data.builder {
243+
// If we still have a builder, update it to use the new parent context
244+
// when it's eventually built
245+
data.parent_cx = new_cx;
246+
builder.sampling_result = None;
247+
}
232248
});
233249
});
234250
}
@@ -269,10 +285,10 @@ impl OpenTelemetrySpanExt for tracing::Span {
269285
let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
270286
return;
271287
};
272-
get_context.with_context(subscriber, id, |data| {
273-
// TODO:ban create the span if it doesn't exist
288+
// If our span hasn't been built, we should build it and get the context in one call
289+
get_context.with_activated_context(subscriber, id, |data: &mut OtelData| {
274290
cx = Some(data.parent_cx.clone());
275-
})
291+
});
276292
});
277293

278294
cx.unwrap_or_default()

0 commit comments

Comments
 (0)