Skip to main content

dcontext_tracing/
layer.rs

1use std::marker::PhantomData;
2
3use tracing::Subscriber;
4use tracing_core::span;
5use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer};
6
7use crate::field_mapping::{
8    ExtractedFields, FieldExtractor, FieldMapping, TypedFieldSetter,
9};
10use crate::guard_stack;
11use crate::span_info::{SpanInfo, SPAN_INFO_KEY};
12use crate::FromFieldValue;
13
14/// A [`tracing_subscriber::Layer`] that automatically creates dcontext scopes
15/// when tracing spans are entered.
16///
17/// # Levels of Integration
18///
19/// 1. **Auto-scoping** (zero config): Every span enter creates a new dcontext
20///    scope that inherits the parent scope's values. The scope is reverted on
21///    span exit.
22///
23/// 2. **Field mapping**: Map tracing span fields to dcontext keys. When a span
24///    with the configured field is entered, the value is extracted and set in
25///    the new context scope.
26///
27/// 3. **Span info**: Optionally expose span metadata (name, target, level) as
28///    a [`SpanInfo`] context value.
29///
30/// # Example
31///
32/// ```ignore
33/// use tracing_subscriber::prelude::*;
34///
35/// let layer = dcontext_tracing::DcontextLayer::builder()
36///     .map_field::<RequestId>("request_id")
37///     .include_span_info()
38///     .build();
39///
40/// tracing_subscriber::registry()
41///     .with(layer)
42///     .init();
43/// ```
44pub struct DcontextLayer<S> {
45    field_mappings: Vec<FieldMapping>,
46    include_span_info: bool,
47    _subscriber: PhantomData<fn(S)>,
48}
49
50impl<S> DcontextLayer<S> {
51    /// Create a new `DcontextLayer` with default settings (auto-scoping only).
52    pub fn new() -> Self {
53        Self {
54            field_mappings: Vec::new(),
55            include_span_info: false,
56            _subscriber: PhantomData,
57        }
58    }
59
60    /// Create a [`DcontextLayerBuilder`] for configuring the layer.
61    pub fn builder() -> DcontextLayerBuilder<S> {
62        DcontextLayerBuilder {
63            field_mappings: Vec::new(),
64            include_span_info: false,
65            _subscriber: PhantomData,
66        }
67    }
68}
69
70impl<S> Default for DcontextLayer<S> {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76/// Builder for configuring a [`DcontextLayer`].
77pub struct DcontextLayerBuilder<S> {
78    field_mappings: Vec<FieldMapping>,
79    include_span_info: bool,
80    _subscriber: PhantomData<fn(S)>,
81}
82
83impl<S> DcontextLayerBuilder<S> {
84    /// Map a tracing span field to a dcontext key.
85    ///
86    /// When a span containing a field with the given name is entered,
87    /// the field value is extracted and set as a dcontext value of type `T`.
88    ///
89    /// The field name is used as both the tracing field name and the
90    /// dcontext key.
91    ///
92    /// # Type Requirements
93    ///
94    /// `T` must implement [`FromFieldValue`] for conversion from tracing
95    /// field values to the context type.
96    pub fn map_field<T: FromFieldValue>(mut self, field_name: &'static str) -> Self {
97        self.field_mappings.push(FieldMapping {
98            field_name,
99            context_key: field_name,
100            setter: Box::new(TypedFieldSetter::<T>::new()),
101        });
102        self
103    }
104
105    /// Map a tracing span field to a dcontext key with a different key name.
106    ///
107    /// Similar to [`map_field`](Self::map_field) but allows using a different
108    /// name for the dcontext key than the tracing field name.
109    pub fn map_field_as<T: FromFieldValue>(
110        mut self,
111        field_name: &'static str,
112        context_key: &'static str,
113    ) -> Self {
114        self.field_mappings.push(FieldMapping {
115            field_name,
116            context_key,
117            setter: Box::new(TypedFieldSetter::<T>::new()),
118        });
119        self
120    }
121
122    /// Include span metadata (name, target, level) as a [`SpanInfo`] context value.
123    ///
124    /// When enabled, each span enter will set a [`SpanInfo`] value under
125    /// the key `"dcontext.span"`.
126    pub fn include_span_info(mut self) -> Self {
127        self.include_span_info = true;
128        self
129    }
130
131    /// Build the configured [`DcontextLayer`].
132    pub fn build(self) -> DcontextLayer<S> {
133        DcontextLayer {
134            field_mappings: self.field_mappings,
135            include_span_info: self.include_span_info,
136            _subscriber: PhantomData,
137        }
138    }
139}
140
141impl<S> Layer<S> for DcontextLayer<S>
142where
143    S: Subscriber + for<'a> LookupSpan<'a>,
144{
145    fn on_new_span(
146        &self,
147        attrs: &span::Attributes<'_>,
148        id: &span::Id,
149        ctx: Context<'_, S>,
150    ) {
151        if self.field_mappings.is_empty() {
152            return;
153        }
154
155        let span = match ctx.span(id) {
156            Some(s) => s,
157            None => return,
158        };
159
160        // Extract the field values we're interested in
161        let field_names: Vec<&'static str> =
162            self.field_mappings.iter().map(|m| m.field_name).collect();
163        let mut extractor = FieldExtractor::new(&field_names);
164        attrs.record(&mut extractor);
165
166        if !extractor.extracted.is_empty() {
167            let mut extensions = span.extensions_mut();
168            extensions.insert(extractor.extracted);
169        }
170    }
171
172    fn on_record(
173        &self,
174        id: &span::Id,
175        values: &span::Record<'_>,
176        ctx: Context<'_, S>,
177    ) {
178        if self.field_mappings.is_empty() {
179            return;
180        }
181
182        let span = match ctx.span(id) {
183            Some(s) => s,
184            None => return,
185        };
186
187        let field_names: Vec<&'static str> =
188            self.field_mappings.iter().map(|m| m.field_name).collect();
189        let mut extractor = FieldExtractor::new(&field_names);
190        values.record(&mut extractor);
191
192        if !extractor.extracted.is_empty() {
193            let mut extensions = span.extensions_mut();
194            if let Some(existing) = extensions.get_mut::<ExtractedFields>() {
195                // Merge new values into existing extracted fields
196                existing.string_values.extend(extractor.extracted.string_values);
197                existing.u64_values.extend(extractor.extracted.u64_values);
198                existing.i64_values.extend(extractor.extracted.i64_values);
199                existing.bool_values.extend(extractor.extracted.bool_values);
200            } else {
201                extensions.insert(extractor.extracted);
202            }
203        }
204    }
205
206    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
207        // Use force_thread_local to ensure dcontext uses thread-local storage.
208        // This is necessary because on_enter/on_exit are synchronous callbacks
209        // that may be called inside a tokio runtime (e.g., via Instrument).
210        dcontext::force_thread_local(|| {
211            // Level 1: Create a new dcontext scope
212            let guard = dcontext::enter_scope();
213
214            // Level 2: Apply field mappings from extracted values
215            if !self.field_mappings.is_empty() {
216                if let Some(span) = ctx.span(id) {
217                    let extensions = span.extensions();
218                    if let Some(fields) = extensions.get::<ExtractedFields>() {
219                        for mapping in &self.field_mappings {
220                            // Try each value type in order of specificity
221                            if let Some(v) = fields.string_values.get(mapping.field_name) {
222                                mapping.setter.set_from_str(mapping.context_key, v);
223                            } else if let Some(&v) = fields.u64_values.get(mapping.field_name) {
224                                mapping.setter.set_from_u64(mapping.context_key, v);
225                            } else if let Some(&v) = fields.i64_values.get(mapping.field_name) {
226                                mapping.setter.set_from_i64(mapping.context_key, v);
227                            } else if let Some(&v) = fields.bool_values.get(mapping.field_name) {
228                                mapping.setter.set_from_bool(mapping.context_key, v);
229                            }
230                        }
231                    }
232                }
233            }
234
235            // Level 3: Set span info
236            if self.include_span_info {
237                if let Some(span) = ctx.span(id) {
238                    let metadata = span.metadata();
239                    let info = SpanInfo {
240                        name: metadata.name().to_string(),
241                        target: metadata.target().to_string(),
242                        level: metadata.level().to_string(),
243                    };
244                    dcontext::set_context(SPAN_INFO_KEY, info);
245                }
246            }
247
248            // Store the guard in the thread-local stack
249            guard_stack::push_guard(id, guard);
250        });
251    }
252
253    fn on_exit(&self, id: &span::Id, _ctx: Context<'_, S>) {
254        // Pop the guard inside force_thread_local so the drop (leave_scope)
255        // also uses thread-local storage.
256        dcontext::force_thread_local(|| {
257            guard_stack::pop_guard(id);
258        });
259    }
260
261    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
262        // Clean up any stale guards (defensive: handles missed on_exit)
263        dcontext::force_thread_local(|| {
264            guard_stack::pop_guard(&id);
265        });
266
267        // Clean up extracted fields from extensions
268        if let Some(span) = ctx.span(&id) {
269            let mut extensions = span.extensions_mut();
270            extensions.remove::<ExtractedFields>();
271        }
272    }
273}