Skip to content

Commit 79fbec7

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 130e718 commit 79fbec7

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
@@ -46,7 +46,7 @@ pub struct OpenTelemetryLayer<S, T> {
4646
with_threads: bool,
4747
with_level: bool,
4848
sem_conv_config: SemConvConfig,
49-
get_context: WithContext,
49+
with_context: WithContext,
5050
_registry: marker::PhantomData<S>,
5151
}
5252

@@ -81,26 +81,58 @@ where
8181
OpenTelemetryLayer::default()
8282
}
8383

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

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

@@ -608,8 +640,10 @@ where
608640
error_events_to_exceptions: true,
609641
error_events_to_status: true,
610642
},
611-
612-
get_context: WithContext(Self::get_context),
643+
with_context: WithContext {
644+
with_context: Self::get_context,
645+
with_activated_context: Self::get_activated_context,
646+
},
613647
_registry: marker::PhantomData,
614648
}
615649
}
@@ -659,7 +693,10 @@ where
659693
with_threads: self.with_threads,
660694
with_level: self.with_level,
661695
sem_conv_config: self.sem_conv_config,
662-
get_context: WithContext(OpenTelemetryLayer::<S, Tracer>::get_context),
696+
with_context: WithContext {
697+
with_context: OpenTelemetryLayer::<S, Tracer>::get_context,
698+
with_activated_context: OpenTelemetryLayer::<S, Tracer>::get_activated_context,
699+
},
663700
_registry: self._registry,
664701
// cannot use ``..self` here due to different generics
665702
}
@@ -858,6 +895,18 @@ where
858895
}
859896
}
860897

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

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

962+
///
963+
/// Builds the OTel span associated with given OTel context, consuming the SpanBuilder within
964+
/// the context in the process.
965+
///
889966
fn start_cx(&self, otel_data: &mut OtelData) {
890967
if let Some(builder) = otel_data.builder.take() {
891968
let span = builder.start_with_context(&self.tracer, &otel_data.parent_cx);
@@ -1225,6 +1302,7 @@ where
12251302
/// [`Span`]: opentelemetry::trace::Span
12261303
fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
12271304
let span = ctx.span(&id).expect("Span not found, this is a bug");
1305+
// Now get mutable extensions for removal
12281306
let (otel_data, timings) = {
12291307
let mut extensions = span.extensions_mut();
12301308
let timings = if self.tracked_inactivity {
@@ -1254,10 +1332,10 @@ where
12541332
let busy_ns = Key::new("busy_ns");
12551333
let idle_ns = Key::new("idle_ns");
12561334

1257-
let mut attributes = Vec::with_capacity(2);
1258-
attributes.push(KeyValue::new(busy_ns, timings.busy));
1259-
attributes.push(KeyValue::new(idle_ns, timings.idle));
1260-
span.set_attributes(attributes);
1335+
span.set_attributes(vec![
1336+
KeyValue::new(busy_ns, timings.busy),
1337+
KeyValue::new(idle_ns, timings.idle),
1338+
]);
12611339
}
12621340

12631341
if let Some(end_time) = end_time {
@@ -1274,7 +1352,7 @@ where
12741352
match id {
12751353
id if id == TypeId::of::<Self>() => Some(self as *const _ as *const ()),
12761354
id if id == TypeId::of::<WithContext>() => {
1277-
Some(&self.get_context as *const _ as *const ())
1355+
Some(&self.with_context as *const _ as *const ())
12781356
}
12791357
_ => None,
12801358
}
@@ -1310,10 +1388,13 @@ fn thread_id_integer(id: thread::ThreadId) -> u64 {
13101388

13111389
#[cfg(test)]
13121390
mod tests {
1391+
use crate::OpenTelemetrySpanExt;
1392+
13131393
use super::*;
13141394
use opentelemetry::trace::{SpanContext, TraceFlags, TracerProvider};
13151395
use opentelemetry_sdk::trace::SpanExporter;
13161396
use std::{collections::HashMap, error::Error, fmt::Display, time::SystemTime};
1397+
use tracing::trace_span;
13171398
use tracing_subscriber::prelude::*;
13181399

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

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)