Skip to main content

dcontext_tracing/
tracing_field.rs

1//! Unified tracing metadata for context-to-log, context-to-span, and span-to-context integration.
2//!
3//! [`TracingField`] is the single metadata type that controls all three directions:
4//!
5//! - **Extract** (span field → context): When a tracing span contains a matching
6//!   field, the value is extracted and set as a context variable.
7//! - **Enrich log** (context → log event): The current context value is formatted
8//!   and included in every log event via [`WithContextFields`](crate::WithContextFields).
9//! - **Record span** (context → span field): The current context value is recorded
10//!   into pre-declared span fields on span enter.
11//!
12//! All behaviors are opt-in per key, configured at registration time.
13
14use std::any::Any;
15use std::fmt;
16use std::sync::{Arc, OnceLock};
17
18// ── TracingField metadata ──────────────────────────────────────
19
20/// Unified tracing metadata for a context key.
21///
22/// Controls three behaviors:
23/// - **Extract**: populate a context variable from a tracing span field
24/// - **Enrich log**: include the context value in log output
25/// - **Record span**: record the context value into span fields
26///
27/// Created via [`TracingFieldBuilder`]:
28///
29/// ```rust,ignore
30/// use dcontext_tracing::TracingField;
31///
32/// builder.register_with::<String>("request_id", |opts| {
33///     opts.cached().with_metadata(
34///         TracingField::builder("rid")
35///             .extract_from_str(|s| Some(s.to_string()))
36///             .enrich_display::<String>()  // enables both log + span
37///             .build()
38///     )
39/// });
40/// ```
41pub struct TracingField {
42    /// Field name for log output and span recording.
43    log_name: &'static str,
44    /// Span field name to extract from. If `None`, uses the context key.
45    span_field: Option<&'static str>,
46    /// Span field name to record into. If `None`, uses `log_name`.
47    record_field: Option<&'static str>,
48    /// Extract closures: each calls `dcontext::set_context` internally.
49    extract: Option<ExtractFns>,
50    /// Format function for log enrichment (context → log event).
51    log_fmt_fn: Option<Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync>>,
52    /// Format function for span recording (context → span field).
53    span_fmt_fn: Option<Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync>>,
54}
55
56/// Closures for extracting tracing field values into context.
57///
58/// Each closure takes (field_value, context_key) and calls `set_context`
59/// internally, capturing the concrete type T.
60pub(crate) struct ExtractFns {
61    pub from_str: Option<Arc<dyn Fn(&str, &'static str) + Send + Sync>>,
62    pub from_u64: Option<Arc<dyn Fn(u64, &'static str) + Send + Sync>>,
63    pub from_i64: Option<Arc<dyn Fn(i64, &'static str) + Send + Sync>>,
64    pub from_bool: Option<Arc<dyn Fn(bool, &'static str) + Send + Sync>>,
65}
66
67impl TracingField {
68    /// Start building a `TracingField` with the given log field name.
69    ///
70    /// The `log_name` is the field name used in log output and (by default)
71    /// span recording. It can differ from the context key.
72    pub fn builder(log_name: &'static str) -> TracingFieldBuilder {
73        TracingFieldBuilder {
74            log_name,
75            span_field: None,
76            record_field: None,
77            from_str: None,
78            from_u64: None,
79            from_i64: None,
80            from_bool: None,
81            log_fmt_fn: None,
82            span_fmt_fn: None,
83        }
84    }
85
86    /// The field name used in log output.
87    pub fn log_name(&self) -> &'static str {
88        self.log_name
89    }
90
91    /// The span field name to extract from, if extraction is enabled.
92    pub fn span_field(&self) -> Option<&'static str> {
93        self.span_field
94    }
95
96    /// The span field name to record into.
97    /// Returns `record_field` if set, otherwise `log_name`.
98    pub fn record_field(&self) -> &'static str {
99        self.record_field.unwrap_or(self.log_name)
100    }
101
102    /// Whether this field has extraction enabled.
103    pub fn has_extract(&self) -> bool {
104        self.extract.is_some()
105    }
106
107    /// Whether this field has any enrichment enabled (log or span).
108    pub fn has_enrich(&self) -> bool {
109        self.log_fmt_fn.is_some() || self.span_fmt_fn.is_some()
110    }
111
112    /// Whether this field has log enrichment enabled.
113    pub fn has_log_enrich(&self) -> bool {
114        self.log_fmt_fn.is_some()
115    }
116
117    /// Whether this field has span recording enabled.
118    pub fn has_span_record(&self) -> bool {
119        self.span_fmt_fn.is_some()
120    }
121
122    /// Format a type-erased value for log enrichment.
123    /// Returns `None` if log enrichment is not enabled or the type doesn't match.
124    pub fn format(&self, any_val: &dyn Any) -> Option<String> {
125        self.log_fmt_fn.as_ref().and_then(|f| f(any_val))
126    }
127
128    /// Format a type-erased value for span recording.
129    /// Returns `None` if span recording is not enabled or the type doesn't match.
130    pub fn format_for_span(&self, any_val: &dyn Any) -> Option<String> {
131        self.span_fmt_fn.as_ref().and_then(|f| f(any_val))
132    }
133}
134
135// ── TracingFieldBuilder ────────────────────────────────────────
136
137/// Builder for [`TracingField`] metadata.
138pub struct TracingFieldBuilder {
139    log_name: &'static str,
140    span_field: Option<&'static str>,
141    record_field: Option<&'static str>,
142    from_str: Option<Arc<dyn Fn(&str, &'static str) + Send + Sync>>,
143    from_u64: Option<Arc<dyn Fn(u64, &'static str) + Send + Sync>>,
144    from_i64: Option<Arc<dyn Fn(i64, &'static str) + Send + Sync>>,
145    from_bool: Option<Arc<dyn Fn(bool, &'static str) + Send + Sync>>,
146    log_fmt_fn: Option<Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync>>,
147    span_fmt_fn: Option<Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync>>,
148}
149
150impl TracingFieldBuilder {
151    /// Set the span field name to extract from.
152    ///
153    /// If not set, the context key (from registration) is used as the
154    /// span field name for extraction.
155    pub fn span_field(mut self, name: &'static str) -> Self {
156        self.span_field = Some(name);
157        self
158    }
159
160    /// Set the span field name to record into.
161    ///
162    /// If not set, `log_name` is used as the span field name for recording.
163    /// Use this when the span field you want to record into differs from
164    /// the log output field name.
165    pub fn record_as(mut self, name: &'static str) -> Self {
166        self.record_field = Some(name);
167        self
168    }
169
170    // ── Extract (span → context) ───────────────────────────────
171
172    /// Extract from string span field values.
173    ///
174    /// The closure receives the string value and returns `Some(T)` if
175    /// conversion succeeds. `set_context` is called automatically.
176    pub fn extract_from_str<T>(mut self, f: impl Fn(&str) -> Option<T> + Send + Sync + 'static) -> Self
177    where
178        T: Clone + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
179    {
180        self.from_str = Some(Arc::new(move |value, key| {
181            if let Some(v) = f(value) {
182                dcontext::set_context(key, v);
183            }
184        }));
185        self
186    }
187
188    /// Extract from u64 span field values.
189    pub fn extract_from_u64<T>(mut self, f: impl Fn(u64) -> Option<T> + Send + Sync + 'static) -> Self
190    where
191        T: Clone + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
192    {
193        self.from_u64 = Some(Arc::new(move |value, key| {
194            if let Some(v) = f(value) {
195                dcontext::set_context(key, v);
196            }
197        }));
198        self
199    }
200
201    /// Extract from i64 span field values.
202    pub fn extract_from_i64<T>(mut self, f: impl Fn(i64) -> Option<T> + Send + Sync + 'static) -> Self
203    where
204        T: Clone + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
205    {
206        self.from_i64 = Some(Arc::new(move |value, key| {
207            if let Some(v) = f(value) {
208                dcontext::set_context(key, v);
209            }
210        }));
211        self
212    }
213
214    /// Extract from bool span field values.
215    pub fn extract_from_bool<T>(mut self, f: impl Fn(bool) -> Option<T> + Send + Sync + 'static) -> Self
216    where
217        T: Clone + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
218    {
219        self.from_bool = Some(Arc::new(move |value, key| {
220            if let Some(v) = f(value) {
221                dcontext::set_context(key, v);
222            }
223        }));
224        self
225    }
226
227    // ── Enrich: shorthand (enables BOTH log + span) ────────────
228
229    /// Enrich both log output and span fields using [`Display`](std::fmt::Display).
230    ///
231    /// This is shorthand for calling both `.enrich_log_display::<T>()`
232    /// and `.enrich_span_display::<T>()`.
233    pub fn enrich_display<T: fmt::Display + 'static>(self) -> Self {
234        let fmt: Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync> =
235            Arc::new(|any_val| any_val.downcast_ref::<T>().map(|v| v.to_string()));
236        Self {
237            log_fmt_fn: Some(Arc::clone(&fmt)),
238            span_fmt_fn: Some(fmt),
239            ..self
240        }
241    }
242
243    /// Enrich both log output and span fields using [`Debug`](std::fmt::Debug).
244    ///
245    /// This is shorthand for calling both `.enrich_log_debug::<T>()`
246    /// and `.enrich_span_debug::<T>()`.
247    pub fn enrich_debug<T: fmt::Debug + 'static>(self) -> Self {
248        let fmt: Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync> =
249            Arc::new(|any_val| any_val.downcast_ref::<T>().map(|v| format!("{:?}", v)));
250        Self {
251            log_fmt_fn: Some(Arc::clone(&fmt)),
252            span_fmt_fn: Some(fmt),
253            ..self
254        }
255    }
256
257    /// Enrich both log output and span fields with a custom formatting function.
258    ///
259    /// This is shorthand for calling both `.enrich_log_custom::<T>(f)`
260    /// and `.enrich_span_custom::<T>(f)` with the same function.
261    pub fn enrich_custom<T: 'static>(
262        self,
263        f: impl Fn(&T) -> String + Send + Sync + 'static,
264    ) -> Self {
265        let fmt: Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync> =
266            Arc::new(move |any_val| any_val.downcast_ref::<T>().map(&f));
267        Self {
268            log_fmt_fn: Some(Arc::clone(&fmt)),
269            span_fmt_fn: Some(fmt),
270            ..self
271        }
272    }
273
274    // ── Enrich log only (context → log event) ──────────────────
275
276    /// Enrich log output only using [`Display`](std::fmt::Display).
277    ///
278    /// The value will appear in log events via [`WithContextFields`](crate::WithContextFields)
279    /// but will NOT be automatically recorded into span fields.
280    pub fn enrich_log_display<T: fmt::Display + 'static>(mut self) -> Self {
281        self.log_fmt_fn = Some(Arc::new(|any_val| {
282            any_val.downcast_ref::<T>().map(|v| v.to_string())
283        }));
284        self
285    }
286
287    /// Enrich log output only using [`Debug`](std::fmt::Debug).
288    pub fn enrich_log_debug<T: fmt::Debug + 'static>(mut self) -> Self {
289        self.log_fmt_fn = Some(Arc::new(|any_val| {
290            any_val.downcast_ref::<T>().map(|v| format!("{:?}", v))
291        }));
292        self
293    }
294
295    /// Enrich log output only with a custom formatting function.
296    pub fn enrich_log_custom<T: 'static>(
297        mut self,
298        f: impl Fn(&T) -> String + Send + Sync + 'static,
299    ) -> Self {
300        self.log_fmt_fn = Some(Arc::new(move |any_val| {
301            any_val.downcast_ref::<T>().map(&f)
302        }));
303        self
304    }
305
306    // ── Record span only (context → span field) ────────────────
307
308    /// Record into span fields using [`Display`](std::fmt::Display).
309    ///
310    /// On span enter, the current context value is formatted and recorded
311    /// into the span field (must be pre-declared with `tracing::field::Empty`).
312    /// Spans without the field are silently skipped.
313    pub fn enrich_span_display<T: fmt::Display + 'static>(mut self) -> Self {
314        self.span_fmt_fn = Some(Arc::new(|any_val| {
315            any_val.downcast_ref::<T>().map(|v| v.to_string())
316        }));
317        self
318    }
319
320    /// Record into span fields using [`Debug`](std::fmt::Debug).
321    pub fn enrich_span_debug<T: fmt::Debug + 'static>(mut self) -> Self {
322        self.span_fmt_fn = Some(Arc::new(|any_val| {
323            any_val.downcast_ref::<T>().map(|v| format!("{:?}", v))
324        }));
325        self
326    }
327
328    /// Record into span fields with a custom formatting function.
329    ///
330    /// On span enter, the current context value is passed to `f` and the
331    /// result is recorded into the span field.
332    pub fn enrich_span_custom<T: 'static>(
333        mut self,
334        f: impl Fn(&T) -> String + Send + Sync + 'static,
335    ) -> Self {
336        self.span_fmt_fn = Some(Arc::new(move |any_val| {
337            any_val.downcast_ref::<T>().map(&f)
338        }));
339        self
340    }
341
342    /// Build the [`TracingField`] metadata.
343    pub fn build(self) -> TracingField {
344        let extract = if self.from_str.is_some()
345            || self.from_u64.is_some()
346            || self.from_i64.is_some()
347            || self.from_bool.is_some()
348        {
349            Some(ExtractFns {
350                from_str: self.from_str,
351                from_u64: self.from_u64,
352                from_i64: self.from_i64,
353                from_bool: self.from_bool,
354            })
355        } else {
356            None
357        };
358
359        TracingField {
360            log_name: self.log_name,
361            span_field: self.span_field,
362            record_field: self.record_field,
363            extract,
364            log_fmt_fn: self.log_fmt_fn,
365            span_fmt_fn: self.span_fmt_fn,
366        }
367    }
368}
369
370// ── Discovery cache ────────────────────────────────────────────
371
372/// Cached info about a single TracingField entry, resolved from registry.
373pub(crate) struct TracingFieldEntry {
374    pub context_key: &'static str,
375    /// Span field name for extraction (may differ from context_key).
376    pub span_field: &'static str,
377    /// Span field name to record into.
378    pub record_field: &'static str,
379    /// Extract closures (if extraction is enabled).
380    pub extract: Option<ExtractFns>,
381    /// Log field name (for log enrichment output).
382    pub log_name: &'static str,
383    /// Format function for log enrichment.
384    pub log_fmt_fn: Option<Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync>>,
385    /// Format function for span recording.
386    pub span_fmt_fn: Option<Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync>>,
387}
388
389/// Global cache of discovered TracingField entries.
390/// Populated lazily on first access (after registry is initialized).
391static TRACING_FIELDS: OnceLock<Vec<TracingFieldEntry>> = OnceLock::new();
392
393/// Get the cached list of TracingField entries, discovering from registry on first call.
394pub(crate) fn get_tracing_fields() -> &'static [TracingFieldEntry] {
395    TRACING_FIELDS.get_or_init(|| {
396        dcontext::keys_with_metadata::<TracingField, _>(|key, meta| {
397            let span_field = meta.span_field.unwrap_or(key);
398            let record_field = meta.record_field.unwrap_or(meta.log_name);
399            TracingFieldEntry {
400                context_key: key,
401                span_field,
402                record_field,
403                extract: meta.extract.as_ref().map(|e| ExtractFns {
404                    from_str: e.from_str.as_ref().map(Arc::clone),
405                    from_u64: e.from_u64.as_ref().map(Arc::clone),
406                    from_i64: e.from_i64.as_ref().map(Arc::clone),
407                    from_bool: e.from_bool.as_ref().map(Arc::clone),
408                }),
409                log_name: meta.log_name,
410                log_fmt_fn: meta.log_fmt_fn.as_ref().map(Arc::clone),
411                span_fmt_fn: meta.span_fmt_fn.as_ref().map(Arc::clone),
412            }
413        })
414    })
415}
416
417// ── Collect log fields (public API) ────────────────────────────
418
419/// Collect the current context log fields as `(name, formatted_value)` pairs.
420///
421/// Returns only fields that have log enrichment enabled and a value set in the
422/// current context. Never panics.
423///
424/// This is the public primitive for custom formatters.
425pub fn collect_log_fields() -> Vec<(&'static str, String)> {
426    let entries = get_tracing_fields();
427    let mut result = Vec::new();
428    for entry in entries {
429        if let Some(ref fmt_fn) = entry.log_fmt_fn {
430            if let Some(formatted) = dcontext::with_context_value(entry.context_key, |any_val| {
431                fmt_fn(any_val)
432            })
433            .flatten()
434            {
435                result.push((entry.log_name, formatted));
436            }
437        }
438    }
439    result
440}
441
442// ── FormatEvent wrapper ────────────────────────────────────────
443
444use tracing::Subscriber;
445use tracing_subscriber::fmt::format::Writer;
446use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
447use tracing_subscriber::registry::LookupSpan;
448
449/// A [`FormatEvent`] wrapper that enriches log events with context values.
450///
451/// Wraps an inner formatter and prepends context fields to every log line.
452/// Fields are discovered lazily from registry metadata on first use.
453///
454/// # Example
455///
456/// ```rust,ignore
457/// use tracing_subscriber::prelude::*;
458/// use dcontext_tracing::WithContextFields;
459///
460/// tracing_subscriber::registry()
461///     .with(dcontext_tracing::DcontextLayer::new())
462///     .with(tracing_subscriber::fmt::layer()
463///         .event_format(WithContextFields::wrap(
464///             tracing_subscriber::fmt::format().without_time()
465///         )))
466///     .init();
467/// ```
468pub struct WithContextFields<F> {
469    inner: F,
470}
471
472impl<F> WithContextFields<F> {
473    /// Wrap an inner formatter with context field enrichment.
474    ///
475    /// Fields are discovered lazily from the registry on first log event.
476    /// Safe to call before `initialize()` — discovery happens on first use.
477    pub fn wrap(inner: F) -> Self {
478        Self { inner }
479    }
480}
481
482impl<S, N, F> FormatEvent<S, N> for WithContextFields<F>
483where
484    F: FormatEvent<S, N>,
485    S: Subscriber + for<'a> LookupSpan<'a>,
486    N: for<'writer> FormatFields<'writer> + 'static,
487{
488    fn format_event(
489        &self,
490        ctx: &FmtContext<'_, S, N>,
491        mut writer: Writer<'_>,
492        event: &tracing::Event<'_>,
493    ) -> fmt::Result {
494        let entries = get_tracing_fields();
495        for entry in entries {
496            if let Some(ref fmt_fn) = entry.log_fmt_fn {
497                if let Some(formatted) =
498                    dcontext::with_context_value(entry.context_key, |any_val| fmt_fn(any_val))
499                        .flatten()
500                {
501                    write!(writer, "{}={} ", entry.log_name, formatted)?;
502                }
503            }
504        }
505        self.inner.format_event(ctx, writer, event)
506    }
507}