Skip to main content

dcontext_tracing/
tracing_field.rs

1//! Unified tracing metadata for context-to-log and span-to-context integration.
2//!
3//! [`TracingField`] is the single metadata type that controls both 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** (context → log): The current context value is formatted and
8//!   included in every log event via [`WithContextFields`](crate::WithContextFields).
9//!
10//! Both behaviors are opt-in per key, configured at registration time.
11
12use std::any::Any;
13use std::fmt;
14use std::sync::{Arc, OnceLock};
15
16// ── TracingField metadata ──────────────────────────────────────
17
18/// Unified tracing metadata for a context key.
19///
20/// Controls two behaviors:
21/// - **Extract**: populate a context variable from a tracing span field
22/// - **Enrich**: include the context value in log output
23///
24/// Created via [`TracingFieldBuilder`]:
25///
26/// ```rust,ignore
27/// use dcontext_tracing::TracingField;
28///
29/// builder.register_with::<String>("request_id", |opts| {
30///     opts.cached().with_metadata(
31///         TracingField::builder("rid")
32///             .extract_from_str(|s| Some(s.to_string()))
33///             .enrich_display::<String>()
34///             .build()
35///     )
36/// });
37/// ```
38pub struct TracingField {
39    /// Field name for log output (the "enrichment name").
40    log_name: &'static str,
41    /// Span field name to extract from. If `None`, uses the context key.
42    span_field: Option<&'static str>,
43    /// Extract closures: each calls `dcontext::set_context` internally.
44    /// The `&'static str` arg is the context key (bound at discovery time).
45    extract: Option<ExtractFns>,
46    /// Format function for log enrichment.
47    fmt_fn: Option<Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync>>,
48}
49
50/// Closures for extracting tracing field values into context.
51///
52/// Each closure takes (field_value, context_key) and calls `set_context`
53/// internally, capturing the concrete type T.
54pub(crate) struct ExtractFns {
55    pub from_str: Option<Arc<dyn Fn(&str, &'static str) + Send + Sync>>,
56    pub from_u64: Option<Arc<dyn Fn(u64, &'static str) + Send + Sync>>,
57    pub from_i64: Option<Arc<dyn Fn(i64, &'static str) + Send + Sync>>,
58    pub from_bool: Option<Arc<dyn Fn(bool, &'static str) + Send + Sync>>,
59}
60
61impl TracingField {
62    /// Start building a `TracingField` with the given log field name.
63    ///
64    /// The `log_name` is the field name used in log output (enrichment direction).
65    /// It can differ from the context key.
66    pub fn builder(log_name: &'static str) -> TracingFieldBuilder {
67        TracingFieldBuilder {
68            log_name,
69            span_field: None,
70            from_str: None,
71            from_u64: None,
72            from_i64: None,
73            from_bool: None,
74            fmt_fn: None,
75        }
76    }
77
78    /// The field name used in log output.
79    pub fn log_name(&self) -> &'static str {
80        self.log_name
81    }
82
83    /// The span field name to extract from, if extraction is enabled.
84    pub fn span_field(&self) -> Option<&'static str> {
85        self.span_field
86    }
87
88    /// Whether this field has extraction enabled.
89    pub fn has_extract(&self) -> bool {
90        self.extract.is_some()
91    }
92
93    /// Whether this field has enrichment enabled.
94    pub fn has_enrich(&self) -> bool {
95        self.fmt_fn.is_some()
96    }
97
98    /// Format a type-erased value for log enrichment.
99    /// Returns `None` if enrichment is not enabled or the type doesn't match.
100    pub fn format(&self, any_val: &dyn Any) -> Option<String> {
101        self.fmt_fn.as_ref().and_then(|f| f(any_val))
102    }
103
104    /// Access the extract functions (used by the layer).
105    pub(crate) fn extract_fns(&self) -> Option<&ExtractFns> {
106        self.extract.as_ref()
107    }
108}
109
110// ── TracingFieldBuilder ────────────────────────────────────────
111
112/// Builder for [`TracingField`] metadata.
113pub struct TracingFieldBuilder {
114    log_name: &'static str,
115    span_field: Option<&'static str>,
116    from_str: Option<Arc<dyn Fn(&str, &'static str) + Send + Sync>>,
117    from_u64: Option<Arc<dyn Fn(u64, &'static str) + Send + Sync>>,
118    from_i64: Option<Arc<dyn Fn(i64, &'static str) + Send + Sync>>,
119    from_bool: Option<Arc<dyn Fn(bool, &'static str) + Send + Sync>>,
120    fmt_fn: Option<Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync>>,
121}
122
123impl TracingFieldBuilder {
124    /// Set the span field name to extract from.
125    ///
126    /// If not set, the context key (from registration) is used as the
127    /// span field name.
128    pub fn span_field(mut self, name: &'static str) -> Self {
129        self.span_field = Some(name);
130        self
131    }
132
133    // ── Extract (span → context) ───────────────────────────────
134
135    /// Extract from string span field values.
136    ///
137    /// The closure receives the string value and returns `Some(T)` if
138    /// conversion succeeds. `set_context` is called automatically.
139    pub fn extract_from_str<T>(mut self, f: impl Fn(&str) -> Option<T> + Send + Sync + 'static) -> Self
140    where
141        T: Clone + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
142    {
143        self.from_str = Some(Arc::new(move |value, key| {
144            if let Some(v) = f(value) {
145                dcontext::set_context(key, v);
146            }
147        }));
148        self
149    }
150
151    /// Extract from u64 span field values.
152    pub fn extract_from_u64<T>(mut self, f: impl Fn(u64) -> Option<T> + Send + Sync + 'static) -> Self
153    where
154        T: Clone + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
155    {
156        self.from_u64 = Some(Arc::new(move |value, key| {
157            if let Some(v) = f(value) {
158                dcontext::set_context(key, v);
159            }
160        }));
161        self
162    }
163
164    /// Extract from i64 span field values.
165    pub fn extract_from_i64<T>(mut self, f: impl Fn(i64) -> Option<T> + Send + Sync + 'static) -> Self
166    where
167        T: Clone + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
168    {
169        self.from_i64 = Some(Arc::new(move |value, key| {
170            if let Some(v) = f(value) {
171                dcontext::set_context(key, v);
172            }
173        }));
174        self
175    }
176
177    /// Extract from bool span field values.
178    pub fn extract_from_bool<T>(mut self, f: impl Fn(bool) -> Option<T> + Send + Sync + 'static) -> Self
179    where
180        T: Clone + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
181    {
182        self.from_bool = Some(Arc::new(move |value, key| {
183            if let Some(v) = f(value) {
184                dcontext::set_context(key, v);
185            }
186        }));
187        self
188    }
189
190    // ── Enrich (context → log) ─────────────────────────────────
191
192    /// Enrich log output using [`Display`](std::fmt::Display).
193    pub fn enrich_display<T: fmt::Display + 'static>(mut self) -> Self {
194        self.fmt_fn = Some(Arc::new(|any_val| {
195            any_val.downcast_ref::<T>().map(|v| v.to_string())
196        }));
197        self
198    }
199
200    /// Enrich log output using [`Debug`](std::fmt::Debug).
201    pub fn enrich_debug<T: fmt::Debug + 'static>(mut self) -> Self {
202        self.fmt_fn = Some(Arc::new(|any_val| {
203            any_val.downcast_ref::<T>().map(|v| format!("{:?}", v))
204        }));
205        self
206    }
207
208    /// Enrich log output with a custom formatting function.
209    pub fn enrich_custom<T: 'static>(
210        mut self,
211        f: impl Fn(&T) -> String + Send + Sync + 'static,
212    ) -> Self {
213        self.fmt_fn = Some(Arc::new(move |any_val| {
214            any_val.downcast_ref::<T>().map(&f)
215        }));
216        self
217    }
218
219    /// Build the [`TracingField`] metadata.
220    pub fn build(self) -> TracingField {
221        let extract = if self.from_str.is_some()
222            || self.from_u64.is_some()
223            || self.from_i64.is_some()
224            || self.from_bool.is_some()
225        {
226            Some(ExtractFns {
227                from_str: self.from_str,
228                from_u64: self.from_u64,
229                from_i64: self.from_i64,
230                from_bool: self.from_bool,
231            })
232        } else {
233            None
234        };
235
236        TracingField {
237            log_name: self.log_name,
238            span_field: self.span_field,
239            extract,
240            fmt_fn: self.fmt_fn,
241        }
242    }
243}
244
245// ── Discovery cache ────────────────────────────────────────────
246
247/// Cached info about a single TracingField entry, resolved from registry.
248pub(crate) struct TracingFieldEntry {
249    pub context_key: &'static str,
250    /// Span field name for extraction (may differ from context_key).
251    pub span_field: &'static str,
252    /// Extract closures (if extraction is enabled).
253    pub extract: Option<ExtractFns>,
254    /// Log field name (for enrichment output).
255    pub log_name: &'static str,
256    /// Format function (if enrichment is enabled).
257    pub fmt_fn: Option<Arc<dyn Fn(&dyn Any) -> Option<String> + Send + Sync>>,
258}
259
260/// Global cache of discovered TracingField entries.
261/// Populated lazily on first access (after registry is initialized).
262static TRACING_FIELDS: OnceLock<Vec<TracingFieldEntry>> = OnceLock::new();
263
264/// Get the cached list of TracingField entries, discovering from registry on first call.
265pub(crate) fn get_tracing_fields() -> &'static [TracingFieldEntry] {
266    TRACING_FIELDS.get_or_init(|| {
267        dcontext::keys_with_metadata::<TracingField, _>(|key, meta| {
268            let span_field = meta.span_field.unwrap_or(key);
269            TracingFieldEntry {
270                context_key: key,
271                span_field,
272                extract: meta.extract.as_ref().map(|e| ExtractFns {
273                    from_str: e.from_str.as_ref().map(Arc::clone),
274                    from_u64: e.from_u64.as_ref().map(Arc::clone),
275                    from_i64: e.from_i64.as_ref().map(Arc::clone),
276                    from_bool: e.from_bool.as_ref().map(Arc::clone),
277                }),
278                log_name: meta.log_name,
279                fmt_fn: meta.fmt_fn.as_ref().map(Arc::clone),
280            }
281        })
282    })
283}
284
285// ── Collect log fields (public API) ────────────────────────────
286
287/// Collect the current context log fields as `(name, formatted_value)` pairs.
288///
289/// Returns only fields that have enrichment enabled and a value set in the
290/// current context. Never panics.
291///
292/// This is the public primitive for custom formatters.
293pub fn collect_log_fields() -> Vec<(&'static str, String)> {
294    let entries = get_tracing_fields();
295    let mut result = Vec::new();
296    for entry in entries {
297        if let Some(ref fmt_fn) = entry.fmt_fn {
298            if let Some(formatted) = dcontext::with_context_value(entry.context_key, |any_val| {
299                fmt_fn(any_val)
300            })
301            .flatten()
302            {
303                result.push((entry.log_name, formatted));
304            }
305        }
306    }
307    result
308}
309
310// ── FormatEvent wrapper ────────────────────────────────────────
311
312use tracing::Subscriber;
313use tracing_subscriber::fmt::format::Writer;
314use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
315use tracing_subscriber::registry::LookupSpan;
316
317/// A [`FormatEvent`] wrapper that enriches log events with context values.
318///
319/// Wraps an inner formatter and prepends context fields to every log line.
320/// Fields are discovered lazily from registry metadata on first use.
321///
322/// # Example
323///
324/// ```rust,ignore
325/// use tracing_subscriber::prelude::*;
326/// use dcontext_tracing::WithContextFields;
327///
328/// tracing_subscriber::registry()
329///     .with(dcontext_tracing::DcontextLayer::new())
330///     .with(tracing_subscriber::fmt::layer()
331///         .event_format(WithContextFields::wrap(
332///             tracing_subscriber::fmt::format().without_time()
333///         )))
334///     .init();
335/// ```
336pub struct WithContextFields<F> {
337    inner: F,
338}
339
340impl<F> WithContextFields<F> {
341    /// Wrap an inner formatter with context field enrichment.
342    ///
343    /// Fields are discovered lazily from the registry on first log event.
344    /// Safe to call before `initialize()` — discovery happens on first use.
345    pub fn wrap(inner: F) -> Self {
346        Self { inner }
347    }
348}
349
350impl<S, N, F> FormatEvent<S, N> for WithContextFields<F>
351where
352    F: FormatEvent<S, N>,
353    S: Subscriber + for<'a> LookupSpan<'a>,
354    N: for<'writer> FormatFields<'writer> + 'static,
355{
356    fn format_event(
357        &self,
358        ctx: &FmtContext<'_, S, N>,
359        mut writer: Writer<'_>,
360        event: &tracing::Event<'_>,
361    ) -> fmt::Result {
362        let entries = get_tracing_fields();
363        for entry in entries {
364            if let Some(ref fmt_fn) = entry.fmt_fn {
365                if let Some(formatted) =
366                    dcontext::with_context_value(entry.context_key, |any_val| fmt_fn(any_val))
367                        .flatten()
368                {
369                    write!(writer, "{}={} ", entry.log_name, formatted)?;
370                }
371            }
372        }
373        self.inner.format_event(ctx, writer, event)
374    }
375}