Skip to main content

dcontext_tracing/
sync_layer.rs

1//! Sync tracing layer that writes to the thread-local context store.
2//!
3//! `SyncDcontextLayer` pushes/pops scopes on the **thread-local** store.
4//! It always succeeds (thread-local is always available).
5//!
6//! This layer supports four levels of integration:
7//!
8//! 1. **Auto-scoping** (zero config): Every span enter creates a new dcontext
9//!    scope that inherits the parent scope's values. Reverted on span exit.
10//!
11//! 2. **Field extraction**: Extract tracing span fields into dcontext values.
12//!    Configure via [`TracingField`](crate::TracingField) metadata at
13//!    registration time.
14//!
15//! 3. **Span info**: Optionally expose span metadata (name, target, level) as
16//!    a [`SpanInfo`](crate::SpanInfo) context value.
17//!
18//! 4. **Span recording**: Auto-record context values into pre-declared Empty
19//!    span fields.
20
21use std::collections::HashSet;
22use std::marker::PhantomData;
23
24use tracing::Subscriber;
25use tracing_core::span;
26use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer};
27
28use crate::field_mapping::ExtractedFields;
29use crate::guard_stack;
30use crate::layer_common;
31use crate::span_info::{SpanInfo, SPAN_INFO_KEY};
32use crate::tracing_field::get_tracing_fields;
33
34/// Marker stored in span extensions to record whether async context was
35/// available when the span was first entered. This ensures consistent
36/// skip behavior across enter/exit/close even if the execution context changes.
37struct SyncSkipMarker {
38    /// If true, async context was available on first enter — sync layer should skip.
39    skip: bool,
40}
41
42/// A tracing layer that manages dcontext scopes on the **thread-local** store.
43///
44/// On span enter: pushes a named scope via `sync_ctx::push_scope(span_name)`
45/// On span exit: pops the scope via guard drop
46///
47/// Always succeeds — thread-local storage is always available.
48///
49/// # Example
50///
51/// ```rust,ignore
52/// use tracing_subscriber::prelude::*;
53///
54/// tracing_subscriber::registry()
55///     .with(dcontext_tracing::SyncDcontextLayer::new())
56///     .init();
57/// ```
58pub struct SyncDcontextLayer<S> {
59    include_span_info: bool,
60    /// When true, skip all work if an async task-local context is available.
61    async_aware: bool,
62    _subscriber: PhantomData<fn(S)>,
63}
64
65impl<S> SyncDcontextLayer<S> {
66    /// Create a new `SyncDcontextLayer` with default settings (auto-scoping only).
67    pub fn new() -> Self {
68        Self {
69            include_span_info: false,
70            async_aware: false,
71            _subscriber: PhantomData,
72        }
73    }
74
75    /// Create a [`SyncDcontextLayerBuilder`] for configuring the layer.
76    pub fn builder() -> SyncDcontextLayerBuilder<S> {
77        SyncDcontextLayerBuilder {
78            include_span_info: false,
79            async_aware: false,
80            _subscriber: PhantomData,
81        }
82    }
83
84    /// Returns true if async context is available and this layer should skip.
85    fn should_skip_for_span<S2: Subscriber + for<'a> LookupSpan<'a>>(
86        &self,
87        id: &span::Id,
88        ctx: &Context<'_, S2>,
89    ) -> bool {
90        if !self.async_aware {
91            return false;
92        }
93
94        let span = match ctx.span(id) {
95            Some(s) => s,
96            None => return false,
97        };
98
99        let extensions = span.extensions();
100        if let Some(marker) = extensions.get::<SyncSkipMarker>() {
101            return marker.skip;
102        }
103        drop(extensions);
104
105        let skip = dcontext::async_ctx::current_depth().is_some();
106        let mut extensions = span.extensions_mut();
107        extensions.insert(SyncSkipMarker { skip });
108        skip
109    }
110}
111
112impl<S> Default for SyncDcontextLayer<S> {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118/// Builder for configuring a [`SyncDcontextLayer`].
119pub struct SyncDcontextLayerBuilder<S> {
120    include_span_info: bool,
121    async_aware: bool,
122    _subscriber: PhantomData<fn(S)>,
123}
124
125impl<S> SyncDcontextLayerBuilder<S> {
126    /// Include span metadata (name, target, level) as a [`SpanInfo`](crate::SpanInfo) context value.
127    pub fn include_span_info(mut self) -> Self {
128        self.include_span_info = true;
129        self
130    }
131
132    /// Make this layer async-aware: skip when task-local context is available.
133    pub fn async_aware(mut self) -> Self {
134        self.async_aware = true;
135        self
136    }
137
138    /// Build the configured [`SyncDcontextLayer`].
139    pub fn build(self) -> SyncDcontextLayer<S> {
140        SyncDcontextLayer {
141            include_span_info: self.include_span_info,
142            async_aware: self.async_aware,
143            _subscriber: PhantomData,
144        }
145    }
146}
147
148// ── Store-specific helpers (sync_ctx) ──────────────────────────
149
150use tracing_subscriber::registry::SpanRef;
151
152/// Apply field extraction from span extensions into the thread-local context.
153fn apply_field_extraction<S>(span: &SpanRef<'_, S>)
154where
155    S: for<'a> LookupSpan<'a>,
156{
157    let metadata_fields = get_tracing_fields();
158    if !metadata_fields.iter().any(|e| e.extract.is_some()) {
159        return;
160    }
161
162    let extensions = span.extensions();
163    let fields = match extensions.get::<ExtractedFields>() {
164        Some(f) => f,
165        None => return,
166    };
167
168    for entry in metadata_fields {
169        if let Some(ref extract) = entry.extract {
170            let value = if let Some(v) = fields.string_values.get(entry.span_field) {
171                extract.from_str.as_ref().and_then(|f| f(v))
172            } else if let Some(&v) = fields.u64_values.get(entry.span_field) {
173                extract.from_u64.as_ref().and_then(|f| f(v))
174            } else if let Some(&v) = fields.i64_values.get(entry.span_field) {
175                extract.from_i64.as_ref().and_then(|f| f(v))
176            } else if let Some(&v) = fields.bool_values.get(entry.span_field) {
177                extract.from_bool.as_ref().and_then(|f| f(v))
178            } else {
179                None
180            };
181
182            if let Some(val) = value {
183                dcontext::sync_ctx::set_raw_value(entry.context_key, val);
184            }
185        }
186    }
187}
188
189/// Set span info in the thread-local context.
190fn set_span_info<S>(span: &SpanRef<'_, S>)
191where
192    S: for<'a> LookupSpan<'a>,
193{
194    let metadata = span.metadata();
195    let info = SpanInfo {
196        name: metadata.name().to_string(),
197        target: metadata.target().to_string(),
198        level: metadata.level().to_string(),
199    };
200    dcontext::sync_ctx::set_context(SPAN_INFO_KEY, info);
201}
202
203/// Record context values from thread-local store into span fields.
204fn record_context_to_span<S>(span: &SpanRef<'_, S>)
205where
206    S: for<'a> LookupSpan<'a>,
207{
208    let metadata_fields = get_tracing_fields();
209    if !metadata_fields.iter().any(|e| e.span_fmt_fn.is_some()) {
210        return;
211    }
212
213    let user_set: HashSet<&str> = {
214        let extensions = span.extensions();
215        extensions
216            .get::<ExtractedFields>()
217            .map(|ef| ef.user_set_fields.iter().copied().collect())
218            .unwrap_or_default()
219    };
220
221    let to_record: Vec<(&'static str, String)> = metadata_fields
222        .iter()
223        .filter_map(|entry| {
224            let fmt_fn = entry.span_fmt_fn.as_ref()?;
225            if user_set.contains(entry.record_field) {
226                return None;
227            }
228            let formatted = dcontext::sync_ctx::with_context_value(entry.context_key, |any_val| {
229                fmt_fn(any_val)
230            })
231            .flatten()?;
232            Some((entry.record_field, formatted))
233        })
234        .collect();
235
236    if !to_record.is_empty() {
237        let current = tracing::Span::current();
238        layer_common::SELF_RECORDING.with(|f| f.set(true));
239        for (field_name, value) in &to_record {
240            current.record(*field_name, value.as_str());
241        }
242        layer_common::SELF_RECORDING.with(|f| f.set(false));
243    }
244}
245
246// ── Layer implementation ───────────────────────────────────────
247
248impl<S> Layer<S> for SyncDcontextLayer<S>
249where
250    S: Subscriber + for<'a> LookupSpan<'a>,
251{
252    fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
253        if let Some(span) = ctx.span(id) {
254            layer_common::extract_span_fields(attrs, &span);
255        }
256    }
257
258    fn on_record(&self, id: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
259        if let Some(span) = ctx.span(id) {
260            layer_common::merge_recorded_fields(values, &span);
261        }
262    }
263
264    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
265        if self.should_skip_for_span(id, &ctx) {
266            return;
267        }
268
269        // Level 1: Create a new named dcontext scope
270        let guard = if let Some(span) = ctx.span(id) {
271            let name = span.metadata().name();
272            dcontext::sync_ctx::push_scope(name)
273        } else {
274            dcontext::sync_ctx::push_scope("")
275        };
276
277        // Level 2: Apply field extraction
278        if let Some(span) = ctx.span(id) {
279            apply_field_extraction(&span);
280        }
281
282        // Level 3: Set span info
283        if self.include_span_info {
284            if let Some(span) = ctx.span(id) {
285                set_span_info(&span);
286            }
287        }
288
289        // Level 4: Record context values into span fields
290        if let Some(span) = ctx.span(id) {
291            record_context_to_span(&span);
292        }
293
294        guard_stack::push_guard(id, guard);
295    }
296
297    fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) {
298        if let Some(span) = ctx.span(id) {
299            let extensions = span.extensions();
300            if let Some(marker) = extensions.get::<SyncSkipMarker>() {
301                if marker.skip {
302                    return;
303                }
304            }
305        }
306
307        guard_stack::pop_guard(id);
308    }
309
310    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
311        if let Some(span) = ctx.span(&id) {
312            let extensions = span.extensions();
313            if let Some(marker) = extensions.get::<SyncSkipMarker>() {
314                if marker.skip {
315                    return;
316                }
317            }
318        }
319
320        guard_stack::pop_guard(&id);
321
322        if let Some(span) = ctx.span(&id) {
323            let mut extensions = span.extensions_mut();
324            extensions.remove::<ExtractedFields>();
325            extensions.remove::<SyncSkipMarker>();
326        }
327    }
328}