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}