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}