Skip to main content

dcontext_tracing/
async_layer.rs

1//! Async-aware tracing layer that writes to the task-local context store.
2//!
3//! `AsyncDcontextLayer` pushes/pops scopes on the **task-local** store.
4//! Unlike sync tracing, async spans enter/exit on every poll boundary
5//! (due to `Instrumented<F>`). This layer handles that correctly by:
6//!
7//! - Pushing a scope on **first enter** of a span (tracked via span extensions)
8//! - NOT popping on exit (scope persists across yields)
9//! - Popping on **close** (span fully completes)
10//!
11//! This ensures scopes follow the logical async lifetime, not individual polls.
12//! State is stored in span extensions (not thread-local), so it correctly
13//! handles task migration across threads in multi-threaded runtimes.
14//!
15//! This layer supports four levels of integration:
16//!
17//! 1. **Auto-scoping**: Every span creates a task-local dcontext scope.
18//!
19//! 2. **Field extraction**: Extract tracing span fields into task-local context.
20//!    Configure via [`TracingField`](crate::TracingField) metadata.
21//!
22//! 3. **Span info**: Optionally expose span metadata as a
23//!    [`SpanInfo`](crate::SpanInfo) context value.
24//!
25//! 4. **Span recording**: Auto-record context values into pre-declared Empty
26//!    span fields.
27
28use std::collections::HashSet;
29use std::marker::PhantomData;
30
31use tracing::Subscriber;
32use tracing_core::span;
33use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer};
34
35use crate::field_mapping::ExtractedFields;
36use crate::layer_common;
37use crate::span_info::{SpanInfo, SPAN_INFO_KEY};
38use crate::tracing_field::get_tracing_fields;
39
40/// Per-span state stored in span extensions.
41/// Tracks whether this span has pushed a scope and at what depth.
42struct AsyncScopeState {
43    /// The depth returned by push_scope — serves as a unique scope ID
44    /// for verifying the correct scope is being popped on close.
45    depth: usize,
46}
47
48/// A tracing layer that manages dcontext scopes on the **task-local** store.
49///
50/// Designed for async code: scopes persist across yields and are only
51/// cleaned up when the span closes.
52///
53/// On first span enter: pushes a named scope via `async_ctx::push_scope(span_name)`
54/// On span close: pops the scope
55///
56/// If not in an async task (`TASK_CONTEXT.try_with` fails), silently no-ops.
57///
58/// # Example
59///
60/// ```rust,ignore
61/// use tracing_subscriber::prelude::*;
62///
63/// tracing_subscriber::registry()
64///     .with(dcontext_tracing::AsyncDcontextLayer::new())
65///     .init();
66/// ```
67pub struct AsyncDcontextLayer<S> {
68    include_span_info: bool,
69    _subscriber: PhantomData<fn(S)>,
70}
71
72impl<S> AsyncDcontextLayer<S> {
73    /// Create a new `AsyncDcontextLayer` with default settings (auto-scoping only).
74    ///
75    /// Field extraction is auto-discovered from [`TracingField`](crate::TracingField)
76    /// metadata in the registry. No explicit field configuration is needed.
77    pub fn new() -> Self {
78        Self {
79            include_span_info: false,
80            _subscriber: PhantomData,
81        }
82    }
83
84    /// Create an [`AsyncDcontextLayerBuilder`] for configuring the layer.
85    pub fn builder() -> AsyncDcontextLayerBuilder<S> {
86        AsyncDcontextLayerBuilder {
87            include_span_info: false,
88            _subscriber: PhantomData,
89        }
90    }
91}
92
93impl<S> Default for AsyncDcontextLayer<S> {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99/// Builder for configuring an [`AsyncDcontextLayer`].
100pub struct AsyncDcontextLayerBuilder<S> {
101    include_span_info: bool,
102    _subscriber: PhantomData<fn(S)>,
103}
104
105impl<S> AsyncDcontextLayerBuilder<S> {
106    /// Include span metadata (name, target, level) as a [`SpanInfo`](crate::SpanInfo) context value.
107    ///
108    /// When enabled, each span's first enter will set a [`SpanInfo`] value under
109    /// the key `"dcontext.span"` in the task-local store.
110    pub fn include_span_info(mut self) -> Self {
111        self.include_span_info = true;
112        self
113    }
114
115    /// Build the configured [`AsyncDcontextLayer`].
116    pub fn build(self) -> AsyncDcontextLayer<S> {
117        AsyncDcontextLayer {
118            include_span_info: self.include_span_info,
119            _subscriber: PhantomData,
120        }
121    }
122}
123
124// ── Store-specific helpers (async_ctx) ─────────────────────────
125
126use tracing_subscriber::registry::SpanRef;
127
128/// Apply field extraction from span extensions into the task-local context.
129fn apply_field_extraction<S>(span: &SpanRef<'_, S>)
130where
131    S: for<'a> LookupSpan<'a>,
132{
133    let metadata_fields = get_tracing_fields();
134    if !metadata_fields.iter().any(|e| e.extract.is_some()) {
135        return;
136    }
137
138    let extensions = span.extensions();
139    let fields = match extensions.get::<ExtractedFields>() {
140        Some(f) => f,
141        None => return,
142    };
143
144    for entry in metadata_fields {
145        if let Some(ref extract) = entry.extract {
146            let value = if let Some(v) = fields.string_values.get(entry.span_field) {
147                extract.from_str.as_ref().and_then(|f| f(v))
148            } else if let Some(&v) = fields.u64_values.get(entry.span_field) {
149                extract.from_u64.as_ref().and_then(|f| f(v))
150            } else if let Some(&v) = fields.i64_values.get(entry.span_field) {
151                extract.from_i64.as_ref().and_then(|f| f(v))
152            } else if let Some(&v) = fields.bool_values.get(entry.span_field) {
153                extract.from_bool.as_ref().and_then(|f| f(v))
154            } else {
155                None
156            };
157
158            if let Some(val) = value {
159                dcontext::async_ctx::set_raw_value(entry.context_key, val);
160            }
161        }
162    }
163}
164
165/// Set span info in the task-local context.
166fn set_span_info<S>(span: &SpanRef<'_, S>)
167where
168    S: for<'a> LookupSpan<'a>,
169{
170    let metadata = span.metadata();
171    let info = SpanInfo {
172        name: metadata.name().to_string(),
173        target: metadata.target().to_string(),
174        level: metadata.level().to_string(),
175    };
176    dcontext::async_ctx::set_context(SPAN_INFO_KEY, info);
177}
178
179/// Record context values from task-local store into span fields.
180fn record_context_to_span<S>(span: &SpanRef<'_, S>)
181where
182    S: for<'a> LookupSpan<'a>,
183{
184    let metadata_fields = get_tracing_fields();
185    if !metadata_fields.iter().any(|e| e.span_fmt_fn.is_some()) {
186        return;
187    }
188
189    let user_set: HashSet<&str> = {
190        let extensions = span.extensions();
191        extensions
192            .get::<ExtractedFields>()
193            .map(|ef| ef.user_set_fields.iter().copied().collect())
194            .unwrap_or_default()
195    };
196
197    let to_record: Vec<(&'static str, String)> = metadata_fields
198        .iter()
199        .filter_map(|entry| {
200            let fmt_fn = entry.span_fmt_fn.as_ref()?;
201            if user_set.contains(entry.record_field) {
202                return None;
203            }
204            let formatted = dcontext::async_ctx::with_context_value(entry.context_key, |any_val| {
205                fmt_fn(any_val)
206            })
207            .flatten()?;
208            Some((entry.record_field, formatted))
209        })
210        .collect();
211
212    if !to_record.is_empty() {
213        let current = tracing::Span::current();
214        layer_common::SELF_RECORDING.with(|f| f.set(true));
215        for (field_name, value) in &to_record {
216            current.record(*field_name, value.as_str());
217        }
218        layer_common::SELF_RECORDING.with(|f| f.set(false));
219    }
220}
221
222// ── Layer implementation ───────────────────────────────────────
223
224impl<S> Layer<S> for AsyncDcontextLayer<S>
225where
226    S: Subscriber + for<'a> LookupSpan<'a>,
227{
228    fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
229        if let Some(span) = ctx.span(id) {
230            layer_common::extract_span_fields(attrs, &span);
231        }
232    }
233
234    fn on_record(&self, id: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
235        if let Some(span) = ctx.span(id) {
236            layer_common::merge_recorded_fields(values, &span);
237        }
238    }
239
240    fn on_enter(&self, id: &span::Id, ctx: Context<'_, S>) {
241        let span = match ctx.span(id) {
242            Some(s) => s,
243            None => return,
244        };
245
246        // Check if we've already pushed a scope for this span (re-enter after yield).
247        {
248            let extensions = span.extensions();
249            if extensions.get::<AsyncScopeState>().is_some() {
250                return;
251            }
252        }
253
254        // First enter: push scope on task-local store.
255        let name = span.metadata().name();
256        let guard = match dcontext::async_ctx::try_push_scope(name) {
257            Some(guard) => guard,
258            None => return,
259        };
260        let depth = guard.expected_depth();
261        std::mem::forget(guard);
262
263        // Level 2: Apply field extraction into task-local context
264        apply_field_extraction(&span);
265
266        // Level 3: Set span info
267        if self.include_span_info {
268            set_span_info(&span);
269        }
270
271        // Level 4: Record context values into span fields
272        record_context_to_span(&span);
273
274        // Store depth in span extensions for verification on close
275        let mut extensions = span.extensions_mut();
276        extensions.insert(AsyncScopeState { depth });
277    }
278
279    fn on_exit(&self, _id: &span::Id, _ctx: Context<'_, S>) {
280        // Intentionally do nothing — scopes persist across yields
281    }
282
283    fn on_close(&self, id: span::Id, ctx: Context<'_, S>) {
284        let span = match ctx.span(&id) {
285            Some(s) => s,
286            None => return,
287        };
288
289        let state = {
290            let mut extensions = span.extensions_mut();
291            extensions.remove::<AsyncScopeState>()
292        };
293
294        if let Some(state) = state {
295            let current = dcontext::async_ctx::current_depth();
296            if current == Some(state.depth) {
297                dcontext::async_ctx::pop_scope(state.depth);
298            }
299        }
300
301        {
302            let mut extensions = span.extensions_mut();
303            extensions.remove::<ExtractedFields>();
304        }
305    }
306}