tempo_telemetry_util/lib.rs
1//! Utilities to make working with tracing and telemetry easier.
2
3/// Formats a [`std::time::Duration`] using the [`std::fmt::Display`].
4///
5/// # Example
6///
7/// ```
8/// use tempo_telemetry_util::display_duration;
9///
10/// let timeout = std::time::Duration::from_millis(1500);
11/// tracing::warn!(
12/// timeout = %display_duration(timeout),
13/// "computation did not finish in the prescribed time",
14/// );
15/// ```
16pub fn display_duration(duration: std::time::Duration) -> DisplayDuration {
17 DisplayDuration(duration)
18}
19
20pub struct DisplayDuration(std::time::Duration);
21impl std::fmt::Display for DisplayDuration {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 use jiff::{
24 SignedDuration,
25 fmt::{
26 StdFmtWrite,
27 friendly::{Designator, SpanPrinter},
28 },
29 };
30 static PRINTER: SpanPrinter = SpanPrinter::new().designator(Designator::Short);
31 match SignedDuration::try_from(self.0) {
32 Ok(duration) => PRINTER
33 .print_duration(&duration, StdFmtWrite(f))
34 .map_err(|_| std::fmt::Error),
35 Err(_) => write!(f, "<duration greater than {:#}>", SignedDuration::MAX),
36 }
37 }
38}
39
40/// Emit an error as a tracing event with its full source chain intact.
41///
42/// This utility provides a streamlined way to emit errors as tracing event fields
43/// and their full source-chain without verbose conversion to `&dyn std::error::Error`
44/// trait objects.
45///
46/// # Why this exists
47///
48/// To emit errors as fields in tracing events in the way tracing intended (that is,
49/// via `tracing::Value for dyn std::error::Error)`, one can either use
50/// `error = &error as &dyn std::error::Error` for typed errors, or alternatively
51/// `error = AsRef::<std::error::Error::as_ref(&error)` for dynamic errors such
52/// `eyre::Report`. Both are verbose and not nice to use. Many users instead just reach
53/// for the sigils `%` or `?`. But `%` uses the `Display` formatting for a type,
54/// skipping its source chain. And `?` uses `Debug`, which can leak implementation details,
55/// is hard to read, and can break formatting (in the case of eyre) -- and its inconsistent.
56///
57/// The [`error_field`] utility allows treating both errors the same way, while making
58/// use of the tracing machinery.
59///
60/// # Notes on the implementation
61///
62/// [`tracing::Value`] is implemented for `E: dyn std::error::Error`, but
63/// actually using it requires a verbose `error as &dyn std::error::Error`
64/// for types that actually implement that trait. Or worse,
65/// `AsRef::<dyn std::error::Error>::as_ref(&eyre_report)` for [`eyre::Report`],
66/// which by itself does not implement the trait.
67///
68/// Right now the implementation requires an additional heap allocation of the
69/// type-erased error object. Because usually errors are not handled in the hot
70/// path of an application this should be an acceptable performance hit.
71///
72/// # Examples
73///
74/// ```
75/// use eyre::WrapErr;
76/// use tempo_telemetry_util::error_field;
77/// let read_error: Result<(), std::io::Error> = Err(std::io::ErrorKind::NotFound.into());
78/// if let Err(error) = Err::<(), _>(std::io::Error::from(std::io::ErrorKind::NotFound))
79/// .wrap_err("failed opening config")
80/// .wrap_err("failed to start server")
81/// {
82/// tracing::error!(
83/// error = error_field(&error),
84/// );
85/// }
86/// ```
87/// This will print (using the standard `tracing_subscriber::fmt::init()` formatting subscriber):
88/// ```text
89/// 2025-08-08T14:38:17.541852Z ERROR tempo_telemetry_util: error=failed starting server error.sources=[failed opening config, entity not found]
90/// ```
91pub fn error_field<E, TMarker>(error: &E) -> Box<dyn tracing::Value + '_>
92where
93 E: AsTracingValue<TMarker>,
94{
95 error.as_tracing_value(private::Token)
96}
97
98#[doc(hidden)]
99// NOTE: the marker is necessary to not run into impl conflicts due to the
100// generic impl for E: std::error::Error. If eyre::Report ever implemented
101// std::error::Error then impl AsTracingValue for E would no longer be unambiguous.
102//
103// This returns a boxed trait object because casting to borrowed (i.e. `&dyn Trait`)
104// objects led to lifetime issues.
105pub trait AsTracingValue<TMarker> {
106 fn as_tracing_value(&self, _: private::Token) -> Box<dyn tracing::Value + '_>;
107}
108
109mod private {
110 pub struct Token;
111 pub struct Generic;
112 pub struct Eyre;
113}
114
115impl<E: std::error::Error + 'static> AsTracingValue<private::Generic> for E {
116 fn as_tracing_value(&self, _: private::Token) -> Box<dyn tracing::Value + '_> {
117 Box::new(self as &(dyn std::error::Error + 'static))
118 }
119}
120
121impl AsTracingValue<private::Eyre> for eyre::Report {
122 fn as_tracing_value(&self, _: private::Token) -> Box<dyn tracing::Value + '_> {
123 Box::new(AsRef::<dyn std::error::Error>::as_ref(self))
124 }
125}