veecle_telemetry/collector/
pretty_exporter.rs1use super::Export;
2use crate::protocol::transient::{InstanceMessage, LogMessage, TelemetryMessage};
3use std::string::String;
4
5#[derive(Debug, Default)]
29pub struct ConsolePrettyExporter(());
30
31impl ConsolePrettyExporter {
32 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 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 attributes.truncate(attributes.len() - 2);
73 attributes + "]"
74 };
75
76 let severity = std::format!("{severity:?}");
78
79 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 (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 (
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}