Skip to main content

telemetry_safe/
lib.rs

1// `cargo package` verifies the crate from the packaged tarball, so the doc
2// source must live inside this crate rather than only at the workspace root.
3#![doc = include_str!("../README.md")]
4
5// Derive macros refer to the public crate path so downstream crates and this
6// crate's own tests expand identically.
7extern crate self as telemetry_safe;
8
9pub use telemetry_safe_core::{
10    TelemetryDebug, TelemetryDisplay, ToTelemetry, prelude, telemetry, telemetry_debug,
11};
12pub use telemetry_safe_derive::ToTelemetry;
13
14#[cfg(test)]
15mod tests {
16    #![allow(dead_code)]
17
18    use super::{ToTelemetry, telemetry};
19    use std::collections::{BTreeMap, BTreeSet};
20    use std::fmt::{self, Formatter};
21
22    #[derive(ToTelemetry)]
23    struct AccountSnapshot {
24        id: u64,
25        state: AccountState,
26        #[telemetry(skip)]
27        secret_note: &'static str,
28    }
29
30    #[derive(ToTelemetry)]
31    enum Outcome {
32        Accepted { account: AccountSnapshot },
33        Rejected(RejectReason),
34    }
35
36    #[derive(ToTelemetry)]
37    struct RejectReason {
38        code: RejectCode,
39    }
40
41    struct AccountState(&'static str);
42
43    impl ToTelemetry for AccountState {
44        fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
45            f.write_str(self.0)
46        }
47    }
48
49    struct RejectCode(&'static str);
50
51    impl ToTelemetry for RejectCode {
52        fn fmt_telemetry(&self, f: &mut Formatter<'_>) -> fmt::Result {
53            f.write_str(self.0)
54        }
55    }
56
57    #[test]
58    fn derived_struct_skips_sensitive_fields() {
59        let snapshot = AccountSnapshot {
60            id: 42,
61            state: AccountState("active"),
62            secret_note: "pii",
63        };
64
65        assert_eq!(
66            telemetry(&snapshot).to_string(),
67            r#"AccountSnapshot { id: 42, state: active }"#
68        );
69    }
70
71    #[test]
72    fn derived_enum_formats_variants() {
73        let outcome = Outcome::Rejected(RejectReason {
74            code: RejectCode("policy"),
75        });
76
77        assert_eq!(
78            telemetry(&outcome).to_string(),
79            r#"Rejected(RejectReason { code: policy })"#
80        );
81    }
82
83    #[test]
84    fn collections_require_safe_elements() {
85        let mut map = BTreeMap::new();
86        map.insert(
87            1_u64,
88            AccountSnapshot {
89                id: 7,
90                state: AccountState("pending"),
91                secret_note: "hidden",
92            },
93        );
94
95        let mut set = BTreeSet::new();
96        set.insert(1_u64);
97
98        assert_eq!(
99            telemetry(&map).to_string(),
100            r#"{1: AccountSnapshot { id: 7, state: pending }}"#
101        );
102        assert_eq!(telemetry(&set).to_string(), "{1}");
103    }
104
105    #[test]
106    fn display_wrapper_can_be_used_in_format_args() {
107        let snapshot = AccountSnapshot {
108            id: 1,
109            state: AccountState("active"),
110            secret_note: "hidden",
111        };
112
113        let rendered = format!("account={}", telemetry(&snapshot));
114
115        assert_eq!(rendered, "account=AccountSnapshot { id: 1, state: active }");
116    }
117}