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