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}