veecle_telemetry/collector/
pretty_exporter.rs1use super::Export;
2use crate::protocol::{InstanceMessage, LogMessage, TelemetryMessage};
3use std::string::String;
4
5#[derive(Debug, Default)]
23pub struct ConsolePrettyExporter(());
24
25impl ConsolePrettyExporter {
26 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 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 attributes.truncate(attributes.len() - 2);
67 attributes + "]"
68 };
69
70 let severity = std::format!("{severity:?}");
72
73 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 (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 (
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}