veecle_telemetry/collector/
pretty_exporter.rs

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