tracing_record_hierarchical/
lib.rs

1// These links overwrite the ones in `README.md`
2// to become proper intra-doc links in Rust docs.
3//! [`HierarchicalRecord`]: HierarchicalRecord
4//! [`Layer`]: Layer
5//! [`must_record_hierarchical()`]: SpanExt::must_record_hierarchical
6//! [`record_hierarchical()`]: SpanExt::record_hierarchical
7//! [`Span::record`]: Span::record()
8//! [`Span`]: tracing::Span
9//! [`SpanExt`]: SpanExt
10//! [`tracing`]: tracing
11//! [`tracing::Span`]: tracing::Span
12//! [subscriber]: tracing#subscribers
13#![doc = include_str!("../README.md")]
14#![deny(
15    macro_use_extern_crate,
16    nonstandard_style,
17    rust_2018_idioms,
18    rustdoc::all,
19    trivial_casts,
20    trivial_numeric_casts
21)]
22#![forbid(non_ascii_idents, unsafe_code)]
23#![warn(
24    clippy::absolute_paths,
25    clippy::as_conversions,
26    clippy::as_ptr_cast_mut,
27    clippy::assertions_on_result_states,
28    clippy::branches_sharing_code,
29    clippy::clear_with_drain,
30    clippy::clone_on_ref_ptr,
31    clippy::collection_is_never_read,
32    clippy::create_dir,
33    clippy::dbg_macro,
34    clippy::debug_assert_with_mut_call,
35    clippy::decimal_literal_representation,
36    clippy::default_union_representation,
37    clippy::derive_partial_eq_without_eq,
38    clippy::else_if_without_else,
39    clippy::empty_drop,
40    clippy::empty_line_after_outer_attr,
41    clippy::empty_structs_with_brackets,
42    clippy::equatable_if_let,
43    clippy::exit,
44    clippy::expect_used,
45    clippy::fallible_impl_from,
46    clippy::filetype_is_file,
47    clippy::float_cmp_const,
48    clippy::fn_to_numeric_cast,
49    clippy::fn_to_numeric_cast_any,
50    clippy::format_push_string,
51    clippy::get_unwrap,
52    clippy::if_then_some_else_none,
53    clippy::implied_bounds_in_impls,
54    clippy::imprecise_flops,
55    clippy::index_refutable_slice,
56    clippy::iter_on_empty_collections,
57    clippy::iter_on_single_items,
58    clippy::iter_with_drain,
59    clippy::large_include_file,
60    clippy::large_stack_frames,
61    clippy::let_underscore_untyped,
62    clippy::lossy_float_literal,
63    clippy::manual_clamp,
64    clippy::map_err_ignore,
65    clippy::mem_forget,
66    clippy::missing_assert_message,
67    clippy::missing_asserts_for_indexing,
68    clippy::missing_const_for_fn,
69    clippy::missing_docs_in_private_items,
70    clippy::multiple_inherent_impl,
71    clippy::multiple_unsafe_ops_per_block,
72    clippy::mutex_atomic,
73    clippy::mutex_integer,
74    clippy::needless_collect,
75    clippy::needless_pass_by_ref_mut,
76    clippy::needless_raw_strings,
77    clippy::nonstandard_macro_braces,
78    clippy::option_if_let_else,
79    clippy::or_fun_call,
80    clippy::panic_in_result_fn,
81    clippy::partial_pub_fields,
82    clippy::pedantic,
83    clippy::print_stderr,
84    clippy::print_stdout,
85    clippy::pub_without_shorthand,
86    clippy::rc_buffer,
87    clippy::rc_mutex,
88    clippy::readonly_write_lock,
89    clippy::redundant_clone,
90    clippy::redundant_type_annotations,
91    clippy::ref_patterns,
92    clippy::rest_pat_in_fully_bound_structs,
93    clippy::same_name_method,
94    clippy::semicolon_inside_block,
95    clippy::shadow_unrelated,
96    clippy::significant_drop_in_scrutinee,
97    clippy::significant_drop_tightening,
98    clippy::str_to_string,
99    clippy::string_add,
100    clippy::string_lit_as_bytes,
101    clippy::string_lit_chars_any,
102    clippy::string_slice,
103    clippy::string_to_string,
104    clippy::suboptimal_flops,
105    clippy::suspicious_operation_groupings,
106    clippy::suspicious_xor_used_as_pow,
107    clippy::tests_outside_test_module,
108    clippy::todo,
109    clippy::trailing_empty_array,
110    clippy::transmute_undefined_repr,
111    clippy::trivial_regex,
112    clippy::try_err,
113    clippy::undocumented_unsafe_blocks,
114    clippy::unimplemented,
115    clippy::unnecessary_safety_comment,
116    clippy::unnecessary_safety_doc,
117    clippy::unnecessary_self_imports,
118    clippy::unnecessary_struct_initialization,
119    clippy::unneeded_field_pattern,
120    clippy::unused_peekable,
121    clippy::unwrap_in_result,
122    clippy::unwrap_used,
123    clippy::use_debug,
124    clippy::use_self,
125    clippy::useless_let_if_seq,
126    clippy::verbose_file_reads,
127    clippy::wildcard_enum_match_arm,
128    future_incompatible,
129    let_underscore_drop,
130    meta_variable_misuse,
131    missing_copy_implementations,
132    missing_debug_implementations,
133    missing_docs,
134    semicolon_in_expressions_from_macros,
135    unreachable_pub,
136    unused_crate_dependencies,
137    unused_extern_crates,
138    unused_import_braces,
139    unused_labels,
140    unused_lifetimes,
141    unused_qualifications,
142    unused_results,
143    unused_tuple_struct_fields,
144    variant_size_differences
145)]
146
147/// For surviving MSRV check only.
148mod unused_deps {
149    use lazy_static as _;
150}
151
152use std::fmt::Display;
153
154use sealed::sealed;
155use tracing::{self as log, field, span, Dispatch, Metadata, Span, Subscriber};
156use tracing_subscriber::{registry::LookupSpan, Layer};
157
158/// Extension of a [`tracing::Span`] providing more ergonomic handling of
159/// [`tracing::Span::record`]s.
160#[sealed]
161pub trait SpanExt {
162    /// Same as [`tracing::Span::record()`], but ensures that the provided
163    /// `field`'s `value` will be written into the first [`tracing::Span`] with
164    /// this `field` up by hierarchy.
165    fn record_hierarchical<Q, V>(&self, field: &Q, value: V) -> &Self
166    where
167        Q: field::AsField + Display + ?Sized,
168        V: field::Value;
169
170    /// Same as [`SpanExt::record_hierarchical()`], but panics.
171    ///
172    /// # Panics
173    ///
174    /// In case none of [`tracing::Span`]s in the hierarchy has the provided
175    /// `field`.
176    fn must_record_hierarchical<Q, V>(&self, field: &Q, value: V) -> &Self
177    where
178        Q: field::AsField + Display + ?Sized,
179        V: field::Value;
180}
181
182#[sealed]
183impl SpanExt for Span {
184    fn record_hierarchical<Q, V>(&self, field: &Q, value: V) -> &Self
185    where
186        Q: field::AsField + Display + ?Sized,
187        V: field::Value,
188    {
189        record(self, field, value, false);
190        self
191    }
192
193    fn must_record_hierarchical<Q, V>(&self, field: &Q, value: V) -> &Self
194    where
195        Q: field::AsField + Display + ?Sized,
196        V: field::Value,
197    {
198        record(self, field, value, true);
199        self
200    }
201}
202
203/// Records the provided `value` to the `field` of the provided [`Span`] if it
204/// has the one, otherwise tries to record it to its parent [`Span`].
205fn record<Q, V>(span: &Span, field: &Q, value: V, do_panic: bool)
206where
207    Q: field::AsField + Display + ?Sized,
208    V: field::Value,
209{
210    if span.has_field(field) {
211        _ = span.record(field, value);
212    } else {
213        record_parent(span, field, value, do_panic);
214    }
215}
216
217/// Walks up the parents' hierarchy of the provided [`Span`] and tries to record
218/// the provided `value` into the `field` of the first [`Span`] having it, or
219/// the "root" [`Span`] is reached.
220fn record_parent<Q, V>(span: &Span, field: &Q, value: V, do_panic: bool)
221where
222    Q: field::AsField + Display + ?Sized,
223    V: field::Value,
224{
225    _ = span.with_subscriber(|(id, dispatch)| {
226        #[allow(clippy::expect_used)] // intentional
227        let ctx = dispatch.downcast_ref::<HierarchicalRecord>().expect(
228            "add `HierarchicalRecord` `Layer` to your `tracing::Subscriber`",
229        );
230
231        if let Some((id, meta)) = ctx.with_context(
232            dispatch,
233            id,
234            &|meta: Meta| field.as_field(meta),
235            &|span_id, meta, field_name| {
236                let value: &dyn field::Value = &value;
237                dispatch.record(
238                    span_id,
239                    &span::Record::new(
240                        &meta.fields().value_set(&[(&field_name, Some(value))]),
241                    ),
242                );
243            },
244        ) {
245            // `Span` wants to record a field that has no corresponding parent.
246            // This means that we walked the entire hierarchy of `Span`s to the
247            // root, yet this field did not find it's corresponding `Span`. We
248            // know that, because otherwise the iteration in `record_parent()`
249            // would end, and this function would not be called again anymore,
250            // yet it is. Nothing to do, but report the error.
251
252            log::error!(
253                "`Span(id={id:?}, meta={meta:?})` doesn't have `{field}` field"
254            );
255            assert!(
256                !do_panic,
257                "`Span(id={id:?}, meta={meta:?})` doesn't have `{field}` field"
258            );
259        };
260    });
261}
262
263/// Shortcut for a [`tracing::Span`]'s `'static` [`Metadata`].
264type Meta = &'static Metadata<'static>;
265
266/// Shortcut for a [`HierarchicalRecord::with_context`] method signature.
267type WithContextFn = fn(
268    dispatch: &Dispatch,
269    id: &span::Id,
270    find_field: &dyn Fn(Meta) -> Option<field::Field>,
271    record: &dyn Fn(&span::Id, Meta, field::Field),
272) -> Option<(span::Id, Meta)>;
273
274/// [`Layer`] helping [`field`]s to find their corresponding [`Span`] in the
275/// hierarchy of [`Span`]s.
276#[derive(Clone, Copy, Debug, Default)]
277pub struct HierarchicalRecord {
278    /// This function "remembers" the type of the subscriber, so that we can do
279    /// something aware of them without knowing those types at the call-site.
280    with_context: Option<WithContextFn>,
281}
282
283impl HierarchicalRecord {
284    /// Allows a function to be called in the context of the "remembered"
285    /// subscriber.
286    fn with_context(
287        self,
288        dispatch: &Dispatch,
289        id: &span::Id,
290        find_field: &dyn Fn(Meta) -> Option<field::Field>,
291        record: &dyn Fn(&span::Id, Meta, field::Field),
292    ) -> Option<(span::Id, Meta)> {
293        (self.with_context?)(dispatch, id, find_field, record)
294    }
295}
296
297impl<S> Layer<S> for HierarchicalRecord
298where
299    S: for<'span> LookupSpan<'span> + Subscriber,
300{
301    fn on_layer(&mut self, _: &mut S) {
302        self.with_context = Some(|dispatch, id, find_field, record| {
303            let subscriber = dispatch.downcast_ref::<S>()?;
304            let span = subscriber.span(id)?;
305
306            let parent = span.parent().and_then(|parent| {
307                parent.scope().find_map(|s| {
308                    let meta = s.metadata();
309                    let field = find_field(meta)?;
310                    Some((s.id(), meta, field))
311                })
312            });
313
314            parent.map_or_else(
315                || Some((span.id(), span.metadata())),
316                |(parent_id, parent_meta, parent_field)| {
317                    record(&parent_id, parent_meta, parent_field);
318                    None
319                },
320            )
321        });
322    }
323}
324
325#[cfg(test)]
326mod spec {
327    use std::{collections::HashMap, fmt::Debug};
328
329    use tracing::{
330        field::{self, Visit},
331        span, Dispatch, Span, Subscriber,
332    };
333    use tracing_subscriber::{layer, registry::LookupSpan, Layer};
334
335    use super::{HierarchicalRecord, SpanExt as _};
336
337    #[test]
338    fn does_nothing_if_not_in_span() {
339        with_subscriber(|| {
340            _ = Span::current().must_record_hierarchical("field", "value");
341
342            assert_eq!(try_current("field").as_deref(), None);
343        });
344    }
345
346    #[test]
347    fn records_into_parent_span() {
348        with_subscriber(|| {
349            tracing::info_span!("parent", field = field::Empty).in_scope(
350                || {
351                    tracing::info_span!("child").in_scope(|| {
352                        _ = Span::current()
353                            .record_hierarchical("field", "value");
354
355                        assert_eq!(
356                            try_current("field").as_deref(),
357                            Some("value"),
358                        );
359                    });
360                },
361            );
362        });
363    }
364
365    #[test]
366    fn must_records_into_parent_span() {
367        with_subscriber(|| {
368            tracing::info_span!("parent", field = field::Empty).in_scope(
369                || {
370                    tracing::info_span!("child").in_scope(|| {
371                        _ = Span::current()
372                            .must_record_hierarchical("field", "value");
373
374                        assert_eq!(
375                            try_current("field").as_deref(),
376                            Some("value"),
377                        );
378                    });
379                },
380            );
381        });
382    }
383
384    #[test]
385    fn must_records_into_toplevel_parent_span() {
386        with_subscriber(|| {
387            tracing::info_span!("grand-grandparent", field = field::Empty)
388                .in_scope(|| {
389                    tracing::info_span!("grandparent").in_scope(|| {
390                        tracing::info_span!("parent").in_scope(|| {
391                            tracing::info_span!("child").in_scope(|| {
392                                _ = Span::current()
393                                    .must_record_hierarchical("field", "value");
394
395                                assert_eq!(
396                                    try_current("field").as_deref(),
397                                    Some("value"),
398                                );
399                            });
400                        });
401                    });
402                });
403        });
404    }
405
406    #[test]
407    fn must_records_into_intermediate_parent_span() {
408        with_subscriber(|| {
409            tracing::info_span!("grand-grandparent").in_scope(|| {
410                tracing::info_span!("grandparent", field = field::Empty)
411                    .in_scope(|| {
412                        tracing::info_span!("parent").in_scope(|| {
413                            tracing::info_span!("child").in_scope(|| {
414                                _ = Span::current()
415                                    .must_record_hierarchical("field", "value");
416
417                                assert_eq!(
418                                    try_current("field").as_deref(),
419                                    Some("value"),
420                                );
421                            });
422                        });
423                    });
424            });
425        });
426    }
427
428    #[test]
429    fn no_panic_on_missing_field() {
430        with_subscriber(|| {
431            tracing::info_span!("parent", abc = field::Empty).in_scope(|| {
432                tracing::info_span!("child").in_scope(|| {
433                    _ = Span::current().record_hierarchical("field", "value");
434
435                    assert_eq!(try_current("field").as_deref(), None);
436                    assert_eq!(try_current("abc").as_deref(), None);
437                });
438            });
439        });
440    }
441
442    #[test]
443    #[should_panic = "doesn't have `field` field"]
444    fn must_panics_on_missing_field() {
445        with_subscriber(|| {
446            tracing::info_span!("parent", abc = field::Empty).in_scope(|| {
447                tracing::info_span!("child").in_scope(|| {
448                    _ = Span::current()
449                        .must_record_hierarchical("field", "value");
450                });
451            });
452        });
453    }
454
455    #[test]
456    #[should_panic = "doesn't have `field` field"]
457    fn must_panics_on_missing_field_and_no_parents() {
458        with_subscriber(|| {
459            tracing::info_span!("child").in_scope(|| {
460                _ = Span::current().must_record_hierarchical("field", "value");
461            });
462        });
463    }
464
465    #[test]
466    #[should_panic = "add `HierarchicalRecord` `Layer` to your \
467                      `tracing::Subscriber`"]
468    fn panics_when_no_layer() {
469        let subscriber = tracing_subscriber::registry();
470
471        tracing::subscriber::with_default(subscriber, || {
472            tracing::info_span!("parent", abc = field::Empty).in_scope(|| {
473                _ = Span::current().must_record_hierarchical("field", "value");
474            });
475        });
476    }
477
478    /// Wraps the provided function into a [`Subscriber`] for tests.
479    fn with_subscriber(f: impl FnOnce()) {
480        use tracing_subscriber::layer::SubscriberExt as _;
481
482        tracing::subscriber::with_default(
483            tracing_subscriber::registry()
484                .with(HierarchicalRecord::default())
485                .with(FieldValueRecorder {
486                    current_field_value: None,
487                    lookup: &["field"],
488                }),
489            f,
490        );
491    }
492
493    /// Tries to extract the specified field's value from the current [`Span`].
494    fn try_current(name: &'static str) -> Option<String> {
495        Span::current()
496            .with_subscriber(|(id, dispatch)| {
497                dispatch
498                    .downcast_ref::<FieldValueRecorder>()?
499                    .current_field_value(dispatch, id, name)
500            })
501            .flatten()
502    }
503
504    /// Shortcut for a [`FieldValueRecorder::current_field_value()`] method
505    /// signature.
506    type CurrentFieldValueFn = fn(
507        dispatch: &Dispatch,
508        id: &span::Id,
509        key: &'static str,
510    ) -> Option<String>;
511
512    /// Shortcut for a list of field names to lookup from a [`Span`].
513    type Lookup = &'static [&'static str];
514
515    /// Helper [`Layer`] for field recording, which records [`Span`] fields in
516    /// [`Extensions`] to be retrieved back using the [`try_current()`]
517    /// function.
518    ///
519    /// [`Extensions`]: tracing_subscriber::registry::Extensions
520    #[derive(Clone, Copy, Debug)]
521    struct FieldValueRecorder {
522        /// Function remembering the type of the subscriber, allowing us to do
523        /// something aware of them without knowing those types at the
524        /// call-site.
525        current_field_value: Option<CurrentFieldValueFn>,
526
527        /// Field names to extract from a [`Span`].
528        lookup: Lookup,
529    }
530
531    impl FieldValueRecorder {
532        /// Allows a function to be called in the context of the "remembered"
533        /// subscriber.
534        fn current_field_value(
535            self,
536            dispatch: &Dispatch,
537            id: &span::Id,
538            key: &'static str,
539        ) -> Option<String> {
540            (self.current_field_value?)(dispatch, id, key)
541        }
542    }
543
544    impl<S> Layer<S> for FieldValueRecorder
545    where
546        S: for<'span> LookupSpan<'span> + Subscriber,
547    {
548        fn on_layer(&mut self, _: &mut S) {
549            self.current_field_value = Some(|dispatch, id, key| {
550                let sub = dispatch.downcast_ref::<S>()?;
551                let span = sub.span(id)?;
552
553                span.scope().find_map(|span| {
554                    let ext = span.extensions();
555                    let Fields(field) = ext.get::<Fields>()?;
556                    Some(field.get(key)?.clone().into())
557                })
558            });
559        }
560
561        fn on_new_span(
562            &self,
563            attrs: &span::Attributes<'_>,
564            id: &span::Id,
565            ctx: layer::Context<'_, S>,
566        ) {
567            if let Some(span) = ctx.span(id) {
568                let fields = Fields::from_record(
569                    &span::Record::new(attrs.values()),
570                    self.lookup,
571                );
572                span.extensions_mut().insert(fields);
573            }
574        }
575
576        fn on_record(
577            &self,
578            id: &span::Id,
579            values: &span::Record<'_>,
580            ctx: layer::Context<'_, S>,
581        ) {
582            if let Some(span) = ctx.span(id) {
583                let new_fields = Fields::from_record(values, self.lookup);
584                if let Some(fields) = span.extensions_mut().get_mut::<Fields>()
585                {
586                    for (k, v) in &new_fields.0 {
587                        drop(fields.0.insert(k, v.clone()));
588                    }
589                }
590            }
591        }
592    }
593
594    /// Values of [`Span`] fields along with their names.
595    #[derive(Clone, Debug, Default)]
596    struct Fields(HashMap<&'static str, String>);
597
598    impl Fields {
599        /// Extracts [`Fields`] from the provided [`span::Record`].
600        fn from_record(record: &span::Record<'_>, lookup: Lookup) -> Self {
601            #[derive(Debug)]
602            struct Visitor {
603                fields: Fields,
604                lookup: Lookup,
605            }
606
607            impl Visit for Visitor {
608                fn record_debug(&mut self, _: &field::Field, _: &dyn Debug) {}
609
610                fn record_str(&mut self, field: &field::Field, value: &str) {
611                    let key = field.name();
612                    if self.lookup.contains(&key) {
613                        drop(self.fields.0.insert(key, value.into()));
614                    }
615                }
616            }
617
618            let mut visitor = Visitor { fields: Fields::default(), lookup };
619            record.record(&mut visitor);
620            visitor.fields
621        }
622    }
623}