veecle_telemetry/collector/
pretty_exporter.rs

1use super::Export;
2use crate::protocol::{InstanceMessage, LogMessage, TelemetryMessage};
3use std::string::String;
4
5/// Exporter that pretty prints telemetry messages to stderr.
6///
7/// This exporter only supports log messages (e.g. `error!("foo")`).
8///
9/// <div class="warning">
10/// Only intended for experimentation and examples.
11/// `telemetry-ui` is strongly recommended for anything beyond experimentation.
12/// </div>
13///
14/// # Examples
15///
16/// ```rust
17/// use veecle_telemetry::collector::{ConsolePrettyExporter, set_exporter, ProcessId};
18///
19/// let process_id = ProcessId::random(&mut rand::rng());
20/// set_exporter(process_id, &ConsolePrettyExporter::DEFAULT).unwrap();
21/// ```
22#[derive(Debug, Default)]
23pub struct ConsolePrettyExporter(());
24
25impl ConsolePrettyExporter {
26    /// A `const` version of `ConsolePrettyExporter::default()` to allow use as a `&'static`.
27    pub const DEFAULT: Self = ConsolePrettyExporter(());
28}
29
30impl Export for ConsolePrettyExporter {
31    fn export(
32        &self,
33        InstanceMessage {
34            thread_id: _,
35            message,
36        }: InstanceMessage,
37    ) {
38        format_message(message, std::io::stderr());
39    }
40}
41
42fn format_message(message: TelemetryMessage, mut output: impl std::io::Write) {
43    if let TelemetryMessage::Log(LogMessage {
44        time_unix_nano,
45        severity,
46        body,
47        attributes,
48        ..
49    }) = message
50    {
51        // Millisecond accuracy is probably enough for a console logger.
52        let time = time_unix_nano / 1_000_000;
53
54        let attributes = if attributes.is_empty() {
55            String::new()
56        } else {
57            let mut attributes =
58                attributes
59                    .iter()
60                    .fold(String::from(" ["), |mut formatted, key_value| {
61                        use std::fmt::Write;
62                        write!(formatted, "{key_value}, ").unwrap();
63                        formatted
64                    });
65            // Remove trailing `, `.
66            attributes.truncate(attributes.len() - 2);
67            attributes + "]"
68        };
69
70        // `Debug` doesn't apply padding, so pre-render to allow padding below.
71        let severity = std::format!("{severity:?}");
72
73        // Severity is up to 5 characters, pad it to stay consistent.
74        //
75        // Using a min-width of 6 for time means that if it is boot-time it will remain
76        // consistently 6 digits wide until ~15 minutes have passed, after that it changes
77        // slowly enough to not be distracting.
78        // For Unix time it will already be 13 digits wide until 2286.
79        std::writeln!(output, "[{severity:>5}:{time:6}] {body}{attributes}").unwrap();
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::format_message;
86    use crate::macros::attributes;
87    use crate::protocol::{LogMessage, Severity, TelemetryMessage};
88    use indoc::indoc;
89    use pretty_assertions::assert_eq;
90    use std::vec::Vec;
91
92    #[test]
93    fn smoke_test() {
94        let mut output = Vec::new();
95
96        let ns = 1_000_000_000;
97        let messages = [
98            // First some "boot time" messages with very low timestamps.
99            (1_000_000, Severity::Trace, "booting", attributes!() as &[_]),
100            (
101                5_000_000,
102                Severity::Debug,
103                "booted",
104                attributes!(truth = true, lies = false),
105            ),
106            (
107                5 * ns,
108                Severity::Info,
109                "running",
110                attributes!(mille = 1000, milli = 0.001),
111            ),
112            (60 * ns, Severity::Warn, "running late", attributes!()),
113            (61 * ns, Severity::Error, "really late", attributes!()),
114            (3600 * ns, Severity::Fatal, "terminating", attributes!()),
115            // Then some "Unix time" messages sent around 2060.
116            (
117                2703621600 * ns,
118                Severity::Trace,
119                "Then are _we_ inhabited by history",
120                attributes!() as &[_],
121            ),
122            (
123                2821816800 * ns,
124                Severity::Debug,
125                "Light dawns and marble heads, what the hell does this mean",
126                attributes!(),
127            ),
128            (
129                2860956000 * ns,
130                Severity::Info,
131                "This terror that hunts",
132                attributes!(Typed = true, date = "1960-08-29"),
133            ),
134            (
135                3118950000 * ns,
136                Severity::Warn,
137                "I have no words, the finest cenotaph",
138                attributes!(),
139            ),
140            (
141                3119036400 * ns,
142                Severity::Error,
143                "A sun to read the dark",
144                attributes!(or = "A son to rend the dark"),
145            ),
146            (
147                3122146800 * ns,
148                Severity::Fatal,
149                "_Tirer comme des lapins_",
150                attributes!(translated = "Shot like rabbits"),
151            ),
152        ];
153
154        for (time_unix_nano, severity, body, attributes) in messages {
155            format_message(
156                TelemetryMessage::Log(LogMessage {
157                    time_unix_nano,
158                    severity,
159                    body: body.into(),
160                    attributes: attributes.into(),
161                }),
162                &mut output,
163            );
164        }
165
166        assert_eq!(
167            str::from_utf8(&output).unwrap(),
168            indoc! { r#"
169            [Trace:     1] booting
170            [Debug:     5] booted [truth: true, lies: false]
171            [ Info:  5000] running [mille: 1000, milli: 0.001]
172            [ Warn: 60000] running late
173            [Error: 61000] really late
174            [Fatal:3600000] terminating
175            [Trace:2703621600000] Then are _we_ inhabited by history
176            [Debug:2821816800000] Light dawns and marble heads, what the hell does this mean
177            [ Info:2860956000000] This terror that hunts [Typed: true, date: "1960-08-29"]
178            [ Warn:3118950000000] I have no words, the finest cenotaph
179            [Error:3119036400000] A sun to read the dark [or: "A son to rend the dark"]
180            [Fatal:3122146800000] _Tirer comme des lapins_ [translated: "Shot like rabbits"]
181        "# }
182        );
183    }
184}