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