1#![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
147mod 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#[sealed]
161pub trait SpanExt {
162 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 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
203fn 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
217fn 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)] 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 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
263type Meta = &'static Metadata<'static>;
265
266type 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#[derive(Clone, Copy, Debug, Default)]
277pub struct HierarchicalRecord {
278 with_context: Option<WithContextFn>,
281}
282
283impl HierarchicalRecord {
284 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 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 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 type CurrentFieldValueFn = fn(
507 dispatch: &Dispatch,
508 id: &span::Id,
509 key: &'static str,
510 ) -> Option<String>;
511
512 type Lookup = &'static [&'static str];
514
515 #[derive(Clone, Copy, Debug)]
521 struct FieldValueRecorder {
522 current_field_value: Option<CurrentFieldValueFn>,
526
527 lookup: Lookup,
529 }
530
531 impl FieldValueRecorder {
532 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 #[derive(Clone, Debug, Default)]
596 struct Fields(HashMap<&'static str, String>);
597
598 impl Fields {
599 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}