Skip to main content

telemetry_safe_core/
lib.rs

1//! Core primitives for compile-time safe telemetry formatting.
2
3use std::fmt::{self, Debug, Display, Formatter};
4
5/// Formats a value only through an explicitly approved telemetry representation.
6pub trait ToTelemetry {
7    /// Writes the representation that may leave the process boundary.
8    ///
9    /// `fmt` keeps adapters allocation-free so high-volume telemetry paths do not
10    /// need a parallel "safe String" API just to satisfy backends like tracing.
11    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result;
12}
13
14/// Wraps a value so telemetry-safe formatting can be used where `Display` is expected.
15#[must_use]
16pub struct TelemetryDisplay<'a, T: ?Sized>(&'a T);
17
18impl<'a, T: ToTelemetry + ?Sized> TelemetryDisplay<'a, T> {
19    /// Creates a `Display` adapter for telemetry backends that accept `%value`.
20    pub fn new(value: &'a T) -> Self {
21        Self(value)
22    }
23}
24
25impl<T: ToTelemetry + ?Sized> Display for TelemetryDisplay<'_, T> {
26    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
27        self.0.fmt_telemetry(f)
28    }
29}
30
31/// Wraps a value so derive output can feed `debug_*` builders without exposing `Debug`.
32#[must_use]
33pub struct TelemetryDebug<'a, T: ?Sized>(&'a T);
34
35impl<'a, T: ToTelemetry + ?Sized> TelemetryDebug<'a, T> {
36    /// Creates a `Debug` adapter for APIs that only accept `?value`-style inputs.
37    ///
38    /// Keeping this separate from `Display` avoids accidentally widening the
39    /// surface area to the ambient `Debug` implementation of the wrapped type.
40    pub fn new(value: &'a T) -> Self {
41        Self(value)
42    }
43}
44
45impl<T: ToTelemetry + ?Sized> Debug for TelemetryDebug<'_, T> {
46    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
47        self.0.fmt_telemetry(f)
48    }
49}
50
51/// Exposes a telemetry-safe value as `Display`.
52pub fn telemetry<T: ToTelemetry + ?Sized>(value: &T) -> TelemetryDisplay<'_, T> {
53    TelemetryDisplay::new(value)
54}
55
56/// Exposes a telemetry-safe value as `Debug`.
57pub fn telemetry_debug<T: ToTelemetry + ?Sized>(value: &T) -> TelemetryDebug<'_, T> {
58    TelemetryDebug::new(value)
59}
60
61macro_rules! impl_to_telemetry_via_display {
62    ($($ty:ty),* $(,)?) => {
63        $(
64            impl ToTelemetry for $ty {
65                fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
66                    Display::fmt(self, f)
67                }
68            }
69        )*
70    };
71}
72
73impl_to_telemetry_via_display!(
74    bool,
75    char,
76    i8,
77    i16,
78    i32,
79    i64,
80    i128,
81    isize,
82    u8,
83    u16,
84    u32,
85    u64,
86    u128,
87    usize,
88    std::num::NonZeroI8,
89    std::num::NonZeroI16,
90    std::num::NonZeroI32,
91    std::num::NonZeroI64,
92    std::num::NonZeroI128,
93    std::num::NonZeroIsize,
94    std::num::NonZeroU8,
95    std::num::NonZeroU16,
96    std::num::NonZeroU32,
97    std::num::NonZeroU64,
98    std::num::NonZeroU128,
99    std::num::NonZeroUsize,
100    std::net::IpAddr,
101    std::net::Ipv4Addr,
102    std::net::Ipv6Addr,
103    std::net::SocketAddr,
104    std::net::SocketAddrV4,
105    std::net::SocketAddrV6
106);
107
108impl<T: ToTelemetry + ?Sized> ToTelemetry for &T {
109    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
110        (*self).fmt_telemetry(f)
111    }
112}
113
114impl<T: ToTelemetry + ?Sized> ToTelemetry for Box<T> {
115    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
116        self.as_ref().fmt_telemetry(f)
117    }
118}
119
120impl<T: ToTelemetry> ToTelemetry for Option<T> {
121    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
122        match self {
123            Some(value) => f
124                .debug_tuple("Some")
125                .field(&telemetry_debug(value))
126                .finish(),
127            None => f.write_str("None"),
128        }
129    }
130}
131
132impl<T: ToTelemetry, E: ToTelemetry> ToTelemetry for Result<T, E> {
133    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
134        match self {
135            Ok(value) => f.debug_tuple("Ok").field(&telemetry_debug(value)).finish(),
136            Err(err) => f.debug_tuple("Err").field(&telemetry_debug(err)).finish(),
137        }
138    }
139}
140
141impl<T: ToTelemetry> ToTelemetry for [T] {
142    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
143        // The adapter keeps recursive safety checks on each element while still
144        // producing the familiar collection syntax backend operators expect.
145        let mut list = f.debug_list();
146        for item in self {
147            list.entry(&telemetry_debug(item));
148        }
149        list.finish()
150    }
151}
152
153impl<T: ToTelemetry, const N: usize> ToTelemetry for [T; N] {
154    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
155        self.as_slice().fmt_telemetry(f)
156    }
157}
158
159impl<T: ToTelemetry> ToTelemetry for Vec<T> {
160    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
161        self.as_slice().fmt_telemetry(f)
162    }
163}
164
165impl<K: ToTelemetry, V: ToTelemetry> ToTelemetry for std::collections::BTreeMap<K, V> {
166    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
167        let mut map = f.debug_map();
168        for (key, value) in self {
169            map.entry(&telemetry_debug(key), &telemetry_debug(value));
170        }
171        map.finish()
172    }
173}
174
175impl<T: ToTelemetry> ToTelemetry for std::collections::BTreeSet<T> {
176    fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
177        let mut set = f.debug_set();
178        for item in self {
179            set.entry(&telemetry_debug(item));
180        }
181        set.finish()
182    }
183}
184
185/// Re-exports the small surface most applications need at call sites.
186pub mod prelude {
187    pub use crate::{ToTelemetry, telemetry, telemetry_debug};
188}
189
190#[cfg(test)]
191mod tests {
192    use super::{ToTelemetry, telemetry};
193    use std::fmt::{self, Formatter};
194
195    struct Token(u64);
196
197    impl ToTelemetry for Token {
198        fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
199            write!(f, "token-{}", self.0)
200        }
201    }
202
203    #[test]
204    fn primitives_and_manual_types_are_displayable() {
205        assert_eq!(telemetry(&123_u64).to_string(), "123");
206        assert_eq!(telemetry(&Token(7)).to_string(), "token-7");
207        assert_eq!(telemetry(&Some(Token(2))).to_string(), "Some(token-2)");
208    }
209
210    #[test]
211    fn collections_use_safe_rendering_recursively() {
212        let values = vec![Token(1), Token(2)];
213        assert_eq!(telemetry(&values).to_string(), "[token-1, token-2]");
214    }
215}