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::{ExtractedFields, FieldExtractor};
8use crate::guard_stack;
9use crate::span_info::{SpanInfo, SPAN_INFO_KEY};
10use crate::tracing_field::get_tracing_fields;
11
12/// A [`tracing_subscriber::Layer`] that automatically creates dcontext scopes
13/// when tracing spans are entered.
14///
15/// # Levels of Integration
16///
17/// 1. **Auto-scoping** (zero config): Every span enter creates a new dcontext
18///    scope that inherits the parent scope's values. The scope is reverted on
19///    span exit.
20///
21/// 2. **Field extraction**: Extract tracing span fields into dcontext values.
22///    Configure via [`TracingField`](crate::TracingField) metadata at
23///    registration time.
24///
25/// 3. **Span info**: Optionally expose span metadata (name, target, level) as
26///    a [`SpanInfo`] context value.
27///
28/// # Example
29///
30/// ```ignore
31/// use tracing_subscriber::prelude::*;
32/// use dcontext_tracing::TracingField;
33///
34/// // Configure extraction + enrichment at registration time:
35/// builder.register_with::<String>("request_id", |opts| {
36///     opts.cached().with_metadata(
37///         TracingField::builder("rid")
38///             .extract_from_str(|s| Some(s.to_string()))
39///             .enrich_display::<String>()
40///             .build()
41///     )
42/// });
43///
44/// // The layer auto-discovers metadata — no field configuration needed:
45/// tracing_subscriber::registry()
46///     .with(dcontext_tracing::DcontextLayer::new())
47///     .init();
48/// ```
49pub struct DcontextLayer<S> {
50    include_span_info: bool,
51    _subscriber: PhantomData<fn(S)>,
52}
53
54impl<S> DcontextLayer<S> {
55    /// Create a new `DcontextLayer` with default settings (auto-scoping only).
56    ///
57    /// Field extraction is auto-discovered from [`TracingField`](crate::TracingField)
58    /// metadata in the registry. No explicit field configuration is needed.
59    pub fn new() -> Self {
60        Self {
61            include_span_info: false,
62            _subscriber: PhantomData,
63        }
64    }
65
66    /// Create a [`DcontextLayerBuilder`] for configuring the layer.
67    pub fn builder() -> DcontextLayerBuilder<S> {
68        DcontextLayerBuilder {
69            include_span_info: false,
70            _subscriber: PhantomData,
71        }
72    }
73}
74
75impl<S> Default for DcontextLayer<S> {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81/// Builder for configuring a [`DcontextLayer`].
82pub struct DcontextLayerBuilder<S> {
83    include_span_info: bool,
84    _subscriber: PhantomData<fn(S)>,
85}
86
87impl<S> DcontextLayerBuilder<S> {
88    /// Include span metadata (name, target, level) as a [`SpanInfo`] context value.
89    ///
90    /// When enabled, each span enter will set a [`SpanInfo`] value under
91    /// the key `"dcontext.span"`.
92    pub fn include_span_info(mut self) -> Self {
93        self.include_span_info = true;
94        self
95    }
96
97    /// Build the configured [`DcontextLayer`].
98    pub fn build(self) -> DcontextLayer<S> {
99        DcontextLayer {
100            include_span_info: self.include_span_info,
101            _subscriber: PhantomData,
102        }
103    }
104}
105
106impl<S> Layer<S> for DcontextLayer<S>
107where
108    S: Subscriber + for<'a> LookupSpan<'a>,
109{
110    fn on_new_span(
111        &self,
112        attrs: &span::Attributes<'_>,
113        id: &span::Id,
114        ctx: Context<'_, S>,
115    ) {
116        let metadata_fields = get_tracing_fields();
117        let extract_names: Vec<&'static str> = metadata_fields
118            .iter()
119            .filter(|e| e.extract.is_some())
120            .map(|e| e.span_field)
121            .collect();
122
123        if extract_names.is_empty() {
124            return;
125        }
126
127        let span = match ctx.span(id) {
128            Some(s) => s,
129            None => return,
130        };
131
132        let mut extractor = FieldExtractor::new(&extract_names);
133        attrs.record(&mut extractor);
134
135        if !extractor.extracted.is_empty() {
136            let mut extensions = span.extensions_mut();
137            extensions.insert(extractor.extracted);
138        }
139    }
140
141    fn on_record(
142        &self,
143        id: &span::Id,
144        values: &span::Record<'_>,
145        ctx: Context<'_, S>,
146    ) {
147        let metadata_fields = get_tracing_fields();
148        let extract_names: Vec<&'static str> = metadata_fields
149            .iter()
150            .filter(|e| e.extract.is_some())
151            .map(|e| e.span_field)
152            .collect();
153
154        if extract_names.is_empty() {
155            return;
156        }
157
158        let span = match ctx.span(id) {
159            Some(s) => s,
160            None => return,
161        };
162
163        let mut extractor = FieldExtractor::new(&extract_names);
164        values.record(&mut extractor);
165
166        if !extractor.extracted.is_empty() {
167            let mut extensions = span.extensions_mut();
168            if let Some(existing) = extensions.get_mut::<ExtractedFields>() {
169                existing.string_values.extend(extractor.extracted.string_values);
170                existing.u64_values.extend(extractor.extracted.u64_values);
171                existing.i64_values.extend(extractor.extracted.i64_values);
172                existing.bool_values.extend(extractor.extracted.bool_values);
173            } else {
174                extensions.insert(extractor.extracted);
175            }
176        }
177    }
178
179    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
180        dcontext::force_thread_local(|| {
181            // Level 1: Create a new named dcontext scope
182            let guard = if let Some(span) = ctx.span(id) {
183                let name = span.metadata().name();
184                dcontext::enter_named_scope(name)
185            } else {
186                dcontext::enter_scope()
187            };
188
189            // Level 2: Apply metadata-driven extraction
190            let metadata_fields = get_tracing_fields();
191            if metadata_fields.iter().any(|e| e.extract.is_some()) {
192                if let Some(span) = ctx.span(id) {
193                    let extensions = span.extensions();
194                    if let Some(fields) = extensions.get::<ExtractedFields>() {
195                        for entry in metadata_fields {
196                            if let Some(ref extract) = entry.extract {
197                                if let Some(v) = fields.string_values.get(entry.span_field) {
198                                    if let Some(ref f) = extract.from_str {
199                                        f(v, entry.context_key);
200                                    }
201                                } else if let Some(&v) = fields.u64_values.get(entry.span_field) {
202                                    if let Some(ref f) = extract.from_u64 {
203                                        f(v, entry.context_key);
204                                    }
205                                } else if let Some(&v) = fields.i64_values.get(entry.span_field) {
206                                    if let Some(ref f) = extract.from_i64 {
207                                        f(v, entry.context_key);
208                                    }
209                                } else if let Some(&v) = fields.bool_values.get(entry.span_field) {
210                                    if let Some(ref f) = extract.from_bool {
211                                        f(v, entry.context_key);
212                                    }
213                                }
214                            }
215                        }
216                    }
217                }
218            }
219
220            // Level 3: Set span info
221            if self.include_span_info {
222                if let Some(span) = ctx.span(id) {
223                    let metadata = span.metadata();
224                    let info = SpanInfo {
225                        name: metadata.name().to_string(),
226                        target: metadata.target().to_string(),
227                        level: metadata.level().to_string(),
228                    };
229                    dcontext::set_context(SPAN_INFO_KEY, info);
230                }
231            }
232
233            guard_stack::push_guard(id, guard);
234        });
235    }
236
237    fn on_exit(&self, id: &span::Id, _ctx: Context<'_, S>) {
238        dcontext::force_thread_local(|| {
239            guard_stack::pop_guard(id);
240        });
241    }
242
243    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
244        dcontext::force_thread_local(|| {
245            guard_stack::pop_guard(&id);
246        });
247
248        if let Some(span) = ctx.span(&id) {
249            let mut extensions = span.extensions_mut();
250            extensions.remove::<ExtractedFields>();
251        }
252    }
253}