Skip to main content

dcontext_tracing/
layer.rs

1use std::cell::Cell;
2use std::marker::PhantomData;
3
4use tracing::Subscriber;
5use tracing_core::span;
6use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer};
7
8use crate::field_mapping::{ExtractedFields, FieldExtractor};
9use crate::guard_stack;
10use crate::span_info::{SpanInfo, SPAN_INFO_KEY};
11use crate::tracing_field::get_tracing_fields;
12
13// Flag to prevent on_record from processing values recorded by our own layer.
14thread_local! {
15    static SELF_RECORDING: Cell<bool> = const { Cell::new(false) };
16}
17
18/// A [`tracing_subscriber::Layer`] that automatically creates dcontext scopes
19/// when tracing spans are entered.
20///
21/// # Levels of Integration
22///
23/// 1. **Auto-scoping** (zero config): Every span enter creates a new dcontext
24///    scope that inherits the parent scope's values. The scope is reverted on
25///    span exit.
26///
27/// 2. **Field extraction**: Extract tracing span fields into dcontext values.
28///    Configure via [`TracingField`](crate::TracingField) metadata at
29///    registration time.
30///
31/// 3. **Span info**: Optionally expose span metadata (name, target, level) as
32///    a [`SpanInfo`] context value.
33///
34/// # Example
35///
36/// ```ignore
37/// use tracing_subscriber::prelude::*;
38/// use dcontext_tracing::TracingField;
39///
40/// // Configure extraction + enrichment at registration time:
41/// builder.register_with::<String>("request_id", |opts| {
42///     opts.cached().with_metadata(
43///         TracingField::builder("rid")
44///             .extract_from_str(|s| Some(s.to_string()))
45///             .enrich_display::<String>()
46///             .build()
47///     )
48/// });
49///
50/// // The layer auto-discovers metadata — no field configuration needed:
51/// tracing_subscriber::registry()
52///     .with(dcontext_tracing::DcontextLayer::new())
53///     .init();
54/// ```
55pub struct DcontextLayer<S> {
56    include_span_info: bool,
57    _subscriber: PhantomData<fn(S)>,
58}
59
60impl<S> DcontextLayer<S> {
61    /// Create a new `DcontextLayer` with default settings (auto-scoping only).
62    ///
63    /// Field extraction is auto-discovered from [`TracingField`](crate::TracingField)
64    /// metadata in the registry. No explicit field configuration is needed.
65    pub fn new() -> Self {
66        Self {
67            include_span_info: false,
68            _subscriber: PhantomData,
69        }
70    }
71
72    /// Create a [`DcontextLayerBuilder`] for configuring the layer.
73    pub fn builder() -> DcontextLayerBuilder<S> {
74        DcontextLayerBuilder {
75            include_span_info: false,
76            _subscriber: PhantomData,
77        }
78    }
79}
80
81impl<S> Default for DcontextLayer<S> {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87/// Builder for configuring a [`DcontextLayer`].
88pub struct DcontextLayerBuilder<S> {
89    include_span_info: bool,
90    _subscriber: PhantomData<fn(S)>,
91}
92
93impl<S> DcontextLayerBuilder<S> {
94    /// Include span metadata (name, target, level) as a [`SpanInfo`] context value.
95    ///
96    /// When enabled, each span enter will set a [`SpanInfo`] value under
97    /// the key `"dcontext.span"`.
98    pub fn include_span_info(mut self) -> Self {
99        self.include_span_info = true;
100        self
101    }
102
103    /// Build the configured [`DcontextLayer`].
104    pub fn build(self) -> DcontextLayer<S> {
105        DcontextLayer {
106            include_span_info: self.include_span_info,
107            _subscriber: PhantomData,
108        }
109    }
110}
111
112impl<S> Layer<S> for DcontextLayer<S>
113where
114    S: Subscriber + for<'a> LookupSpan<'a>,
115{
116    fn on_new_span(
117        &self,
118        attrs: &span::Attributes<'_>,
119        id: &span::Id,
120        ctx: Context<'_, S>,
121    ) {
122        let metadata_fields = get_tracing_fields();
123
124        // Collect fields to extract AND record_field names to track user-set fields
125        let extract_names: Vec<&'static str> = metadata_fields
126            .iter()
127            .filter(|e| e.extract.is_some())
128            .map(|e| e.span_field)
129            .collect();
130
131        let record_fields: Vec<&'static str> = metadata_fields
132            .iter()
133            .filter(|e| e.span_fmt_fn.is_some())
134            .map(|e| e.record_field)
135            .collect();
136
137        if extract_names.is_empty() && record_fields.is_empty() {
138            return;
139        }
140
141        let span = match ctx.span(id) {
142            Some(s) => s,
143            None => return,
144        };
145
146        // Extract target fields from span attributes
147        let all_target: Vec<&'static str> = extract_names
148            .iter()
149            .chain(record_fields.iter())
150            .copied()
151            .collect();
152        let mut extractor = FieldExtractor::new(&all_target);
153        attrs.record(&mut extractor);
154
155        // Mark any record_field that was explicitly set by the user as user-set.
156        // These should NOT be overwritten by auto-record.
157        // Note: tracing's Value trait only supports these 4 primitive types
158        // (string, u64, i64, bool). All other types are formatted via Debug
159        // and stored as strings. So checking these 4 maps covers all possible values.
160        for &rf in &record_fields {
161            if extractor.extracted.string_values.contains_key(rf)
162                || extractor.extracted.u64_values.contains_key(rf)
163                || extractor.extracted.i64_values.contains_key(rf)
164                || extractor.extracted.bool_values.contains_key(rf)
165            {
166                extractor.extracted.mark_user_set(rf);
167            }
168        }
169
170        // Only keep extraction fields in extracted (remove record-only fields)
171        // Actually we keep all — the extraction logic only looks at extract_names
172        if !extractor.extracted.is_empty() || !extractor.extracted.user_set_fields.is_empty() {
173            let mut extensions = span.extensions_mut();
174            extensions.insert(extractor.extracted);
175        }
176    }
177
178    fn on_record(
179        &self,
180        id: &span::Id,
181        values: &span::Record<'_>,
182        ctx: Context<'_, S>,
183    ) {
184        // Skip processing if this on_record was triggered by our own auto-recording
185        if SELF_RECORDING.with(|f| f.get()) {
186            return;
187        }
188
189        let metadata_fields = get_tracing_fields();
190        let extract_names: Vec<&'static str> = metadata_fields
191            .iter()
192            .filter(|e| e.extract.is_some())
193            .map(|e| e.span_field)
194            .collect();
195
196        let record_fields: Vec<&'static str> = metadata_fields
197            .iter()
198            .filter(|e| e.span_fmt_fn.is_some())
199            .map(|e| e.record_field)
200            .collect();
201
202        if extract_names.is_empty() && record_fields.is_empty() {
203            return;
204        }
205
206        let span = match ctx.span(id) {
207            Some(s) => s,
208            None => return,
209        };
210
211        let all_target: Vec<&'static str> = extract_names
212            .iter()
213            .chain(record_fields.iter())
214            .copied()
215            .collect();
216        let mut extractor = FieldExtractor::new(&all_target);
217        values.record(&mut extractor);
218
219        if !extractor.extracted.is_empty() {
220            let mut extensions = span.extensions_mut();
221
222            // Mark user-set record fields
223            for &rf in &record_fields {
224                if extractor.extracted.string_values.contains_key(rf)
225                    || extractor.extracted.u64_values.contains_key(rf)
226                    || extractor.extracted.i64_values.contains_key(rf)
227                    || extractor.extracted.bool_values.contains_key(rf)
228                {
229                    extractor.extracted.mark_user_set(rf);
230                }
231            }
232
233            if let Some(existing) = extensions.get_mut::<ExtractedFields>() {
234                existing.string_values.extend(extractor.extracted.string_values);
235                existing.u64_values.extend(extractor.extracted.u64_values);
236                existing.i64_values.extend(extractor.extracted.i64_values);
237                existing.bool_values.extend(extractor.extracted.bool_values);
238                existing.user_set_fields.extend(extractor.extracted.user_set_fields);
239            } else {
240                extensions.insert(extractor.extracted);
241            }
242        }
243    }
244
245    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
246        dcontext::force_thread_local(|| {
247            // Level 1: Create a new named dcontext scope
248            let guard = if let Some(span) = ctx.span(id) {
249                let name = span.metadata().name();
250                dcontext::enter_named_scope(name)
251            } else {
252                dcontext::enter_scope()
253            };
254
255            // Level 2: Apply metadata-driven extraction
256            let metadata_fields = get_tracing_fields();
257            if metadata_fields.iter().any(|e| e.extract.is_some()) {
258                if let Some(span) = ctx.span(id) {
259                    let extensions = span.extensions();
260                    if let Some(fields) = extensions.get::<ExtractedFields>() {
261                        for entry in metadata_fields {
262                            if let Some(ref extract) = entry.extract {
263                                if let Some(v) = fields.string_values.get(entry.span_field) {
264                                    if let Some(ref f) = extract.from_str {
265                                        f(v, entry.context_key);
266                                    }
267                                } else if let Some(&v) = fields.u64_values.get(entry.span_field) {
268                                    if let Some(ref f) = extract.from_u64 {
269                                        f(v, entry.context_key);
270                                    }
271                                } else if let Some(&v) = fields.i64_values.get(entry.span_field) {
272                                    if let Some(ref f) = extract.from_i64 {
273                                        f(v, entry.context_key);
274                                    }
275                                } else if let Some(&v) = fields.bool_values.get(entry.span_field) {
276                                    if let Some(ref f) = extract.from_bool {
277                                        f(v, entry.context_key);
278                                    }
279                                }
280                            }
281                        }
282                    }
283                }
284            }
285
286            // Level 3: Set span info
287            if self.include_span_info {
288                if let Some(span) = ctx.span(id) {
289                    let metadata = span.metadata();
290                    let info = SpanInfo {
291                        name: metadata.name().to_string(),
292                        target: metadata.target().to_string(),
293                        level: metadata.level().to_string(),
294                    };
295                    dcontext::set_context(SPAN_INFO_KEY, info);
296                }
297            }
298
299            // Level 4: Record context values into span fields.
300            // Inside on_enter, Span::current() returns the span being entered.
301            // span.record() silently skips fields not declared on the span.
302            // We skip fields that were explicitly set by user code.
303            if metadata_fields.iter().any(|e| e.span_fmt_fn.is_some()) {
304                // Check which fields are user-set (should not be overwritten)
305                let user_set: std::collections::HashSet<&str> = ctx
306                    .span(id)
307                    .and_then(|span| {
308                        let extensions = span.extensions();
309                        extensions
310                            .get::<ExtractedFields>()
311                            .map(|ef| ef.user_set_fields.iter().copied().collect())
312                    })
313                    .unwrap_or_default();
314
315                let to_record: Vec<(&'static str, String)> = metadata_fields
316                    .iter()
317                    .filter_map(|entry| {
318                        let fmt_fn = entry.span_fmt_fn.as_ref()?;
319                        // Skip fields explicitly set by user code
320                        if user_set.contains(entry.record_field) {
321                            return None;
322                        }
323                        let formatted = dcontext::with_context_value(
324                            entry.context_key,
325                            |any_val| fmt_fn(any_val),
326                        )
327                        .flatten()?;
328                        Some((entry.record_field, formatted))
329                    })
330                    .collect();
331
332                if !to_record.is_empty() {
333                    let current = tracing::Span::current();
334                    // Set flag to prevent on_record from processing our own writes
335                    SELF_RECORDING.with(|f| f.set(true));
336                    for (field_name, value) in &to_record {
337                        current.record(*field_name, value.as_str());
338                    }
339                    SELF_RECORDING.with(|f| f.set(false));
340                }
341            }
342
343            guard_stack::push_guard(id, guard);
344        });
345    }
346
347    fn on_exit(&self, id: &span::Id, _ctx: Context<'_, S>) {
348        dcontext::force_thread_local(|| {
349            guard_stack::pop_guard(id);
350        });
351    }
352
353    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
354        dcontext::force_thread_local(|| {
355            guard_stack::pop_guard(&id);
356        });
357
358        if let Some(span) = ctx.span(&id) {
359            let mut extensions = span.extensions_mut();
360            extensions.remove::<ExtractedFields>();
361        }
362    }
363}