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