tracing_datadog/context.rs
1//! Functionality for working with distributed trace context.
2
3use crate::span::Span;
4use tracing_core::{Dispatch, span::Id};
5
6/// The trace context for distributed tracing. This is a subset of the W3C trace context
7/// which allows stitching together traces with spans from different services.
8///
9/// It maps to [`tracing::Span`] via [`TracingContextExt`], and to other types via
10/// [`TraceContextExt`].
11#[derive(Copy, Clone, Default)]
12pub struct DatadogContext {
13 pub trace_id: u128,
14 pub parent_id: u64,
15}
16
17impl DatadogContext {
18 /// Returns `true` if the context is empty, i.e. if it does not contain a trace ID or
19 /// a parent ID.
20 pub(crate) fn is_empty(&self) -> bool {
21 self.trace_id == 0 || self.parent_id == 0
22 }
23}
24
25/// Extension trait for extracting/injecting [`DatadogContext`] using a [`Strategy`].
26///
27/// This trait allows handling of trace context in arbitrary container types. To implement it for
28/// additional types, you need to implement [`Strategy`] for them.
29///
30/// You should not need to override this trait's default implementations.
31///
32/// See this example:
33///
34/// ```
35/// use tracing_datadog::context::{DatadogContext, TraceContextExt, Strategy};
36///
37/// struct MyType(String);
38///
39/// impl TraceContextExt for MyType {}
40///
41/// struct MyTypeStrategy;
42///
43/// impl Strategy<MyType> for MyTypeStrategy {
44/// fn inject(my_type: &mut MyType, context: DatadogContext) {
45/// my_type.0 = format!("{}:{}", context.trace_id, context.parent_id);
46/// }
47///
48/// fn extract(my_type: &MyType) -> DatadogContext {
49/// let Some((trace_id, span_id)) = my_type.0.split_once(':') else {
50/// return DatadogContext::default();
51/// };
52///
53/// DatadogContext {
54/// trace_id: trace_id.parse().unwrap_or_default(),
55/// parent_id: span_id.parse().unwrap_or_default(),
56/// }
57/// }
58/// }
59///
60/// // Round-trip through MyType.
61/// let before = DatadogContext { trace_id: 123, parent_id: 456 };
62/// let mut my_type = MyType(String::new());
63///
64/// my_type.inject_trace_context::<MyTypeStrategy>(before);
65/// let after = my_type.extract_trace_context::<MyTypeStrategy>();
66///
67/// assert_eq!(before.trace_id, after.trace_id);
68/// assert_eq!(before.parent_id, after.parent_id);
69/// ```
70///
71/// See [`W3CTraceContextHeaders`](crate::http::W3CTraceContextHeaders) for a real example
72/// implementation.
73pub trait TraceContextExt {
74 fn inject_trace_context<S>(&mut self, context: DatadogContext)
75 where
76 S: Strategy<Self>,
77 {
78 S::inject(self, context)
79 }
80
81 fn extract_trace_context<S>(&self) -> DatadogContext
82 where
83 S: Strategy<Self>,
84 {
85 S::extract(self)
86 }
87}
88
89/// Strategy for extracting/injecting [`DatadogContext`] from/to a container type.
90///
91/// See [`TraceContextExt`] for an example of the intended use.
92pub trait Strategy<T: ?Sized> {
93 /// Injects a trace context into `T`.
94 ///
95 /// If the provided context is empty, `T` is unchanged.
96 fn inject(container: &mut T, context: DatadogContext);
97
98 /// Extracts a trace context from `T`.
99 ///
100 /// If `T` does not contain a valid trace context, the resulting context will be empty.
101 fn extract(container: &T) -> DatadogContext;
102}
103
104/// This function "remembers" the types of the subscriber so that we can downcast to something
105/// aware of them without knowing those types at the call site. Adapted from tracing-error.
106#[derive(Debug)]
107pub(crate) struct WithContext(
108 #[allow(clippy::type_complexity)] pub(crate) fn(&Dispatch, &Id, f: &mut dyn FnMut(&mut Span)),
109);
110
111impl WithContext {
112 pub(crate) fn with_context(
113 &self,
114 dispatch: &Dispatch,
115 id: &Id,
116 mut f: &mut dyn FnMut(&mut Span),
117 ) {
118 self.0(dispatch, id, &mut f);
119 }
120}
121
122// Technically, this duplicates TraceContextExt, but it has a nicer API because it doesn't require
123// strategies or a mutable reference to inject context.
124
125/// Extension trait for [`tracing::Span`] that allows extracting/injecting [`DatadogContext`].
126///
127/// For other types see [`TraceContextExt`].
128pub trait TracingContextExt {
129 /// Sets the distributed trace context on the tracing span.
130 fn set_context(&self, context: DatadogContext);
131
132 /// Gets the distributed trace context from the tracing span.
133 fn get_context(&self) -> DatadogContext;
134}
135
136impl TracingContextExt for tracing::Span {
137 fn set_context(&self, context: DatadogContext) {
138 // Avoid setting a null context.
139 if context.is_empty() {
140 return;
141 }
142
143 self.with_subscriber(move |(id, subscriber)| {
144 let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
145 return;
146 };
147 get_context.with_context(subscriber, id, &mut |dd_span| {
148 dd_span.trace_id = context.trace_id;
149 dd_span.parent_id = context.parent_id;
150 })
151 });
152 }
153
154 fn get_context(&self) -> DatadogContext {
155 let mut ctx = None;
156
157 self.with_subscriber(|(id, subscriber)| {
158 let Some(get_context) = subscriber.downcast_ref::<WithContext>() else {
159 return;
160 };
161 get_context.with_context(subscriber, id, &mut |dd_span| {
162 ctx = Some(DatadogContext {
163 trace_id: dd_span.trace_id,
164 parent_id: dd_span.span_id,
165 })
166 });
167 });
168
169 ctx.unwrap_or_default()
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::DatadogTraceLayer;
177 use rand::random_range;
178 use tracing::info_span;
179 use tracing_subscriber::layer::SubscriberExt;
180
181 #[test]
182 fn span_context_round_trip() {
183 tracing::subscriber::with_default(
184 tracing_subscriber::registry().with(
185 DatadogTraceLayer::builder()
186 .service("test-service")
187 .env("test")
188 .version("test-version")
189 .agent_address("localhost:8126")
190 .build()
191 .unwrap(),
192 ),
193 || {
194 let context = DatadogContext {
195 // Need to limit the size here as we only track 64-bit trace IDs.
196 trace_id: random_range(1..=u128::MAX),
197 parent_id: random_range(1..=u64::MAX),
198 };
199
200 let span = info_span!("test");
201
202 span.set_context(context);
203 let result = span.get_context();
204
205 assert_eq!(context.trace_id, result.trace_id);
206 // NB Parent ID is asymmetrical, this span's ID becomes the next span's parent ID.
207 assert_eq!(span.id().unwrap().into_u64(), result.parent_id);
208 },
209 );
210 }
211
212 #[test]
213 fn empty_span_context_does_not_erase_trace_id() {
214 tracing::subscriber::with_default(
215 tracing_subscriber::registry().with(
216 DatadogTraceLayer::builder()
217 .service("test-service")
218 .env("test")
219 .version("test-version")
220 .agent_address("localhost:8126")
221 .build()
222 .unwrap(),
223 ),
224 || {
225 let context = DatadogContext::default();
226
227 let span = info_span!("test");
228
229 span.set_context(context);
230 let result = span.get_context();
231
232 assert_ne!(result.trace_id, 0);
233 },
234 );
235 }
236}