Skip to content

Commit cd70cfc

Browse files
refactor: improve pretty collector format (#107)
* Refactor of the pretty-printer to make it testable. * Improve formatted output. * Print to stderr instead of stdout. --------- Signed-off-by: Wim Looman <[email protected]> Co-authored-by: arctic-alpaca <[email protected]>
1 parent 0f739d6 commit cd70cfc

File tree

3 files changed

+161
-22
lines changed

3 files changed

+161
-22
lines changed
Lines changed: 156 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use super::Export;
22
use crate::protocol::{InstanceMessage, LogMessage, TelemetryMessage};
3-
use std::prelude::v1::String;
3+
use std::string::String;
44

5-
/// Exporter that pretty prints telemetry messages to stdout.
5+
/// Exporter that pretty prints telemetry messages to stderr.
66
///
77
/// This exporter only supports log messages (e.g. `error!("foo")`).
88
///
@@ -18,10 +18,15 @@ use std::prelude::v1::String;
1818
/// use veecle_telemetry::protocol::ExecutionId;
1919
///
2020
/// let execution_id = ExecutionId::random(&mut rand::rng());
21-
/// set_exporter(execution_id, &ConsolePrettyExporter).unwrap();
21+
/// set_exporter(execution_id, &ConsolePrettyExporter::DEFAULT).unwrap();
2222
/// ```
23-
#[derive(Debug)]
24-
pub struct ConsolePrettyExporter;
23+
#[derive(Debug, Default)]
24+
pub struct ConsolePrettyExporter(());
25+
26+
impl ConsolePrettyExporter {
27+
/// A `const` version of `ConsolePrettyExporter::default()` to allow use as a `&'static`.
28+
pub const DEFAULT: Self = ConsolePrettyExporter(());
29+
}
2530

2631
impl Export for ConsolePrettyExporter {
2732
fn export(
@@ -31,22 +36,152 @@ impl Export for ConsolePrettyExporter {
3136
message,
3237
}: InstanceMessage,
3338
) {
34-
if let TelemetryMessage::Log(LogMessage {
35-
time_unix_nano,
36-
severity,
37-
body,
38-
attributes,
39-
..
40-
}) = message
41-
{
42-
let attributes = attributes
43-
.iter()
44-
.fold(String::new(), |mut formatted, key_value| {
45-
formatted.push_str(", ");
46-
formatted.push_str(&std::format!("{}", key_value));
47-
formatted
48-
});
49-
std::println!("[{severity:?}:{time_unix_nano}] {body}: \"{attributes}\"");
39+
format_message(message, std::io::stderr());
40+
}
41+
}
42+
43+
fn 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)]
85+
mod 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+
);
50167
}
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+
);
51186
}
52187
}

veecle-telemetry/src/macros.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ macro_rules! attributes {
373373
$crate::attributes_inner!(@ { }, { $($kvs)* })
374374
};
375375
}
376+
pub use crate::attributes;
376377

377378
/// The actual implementation of `attributes!`, separated out to avoid accidentally recursing into
378379
/// the `$($tt)*` case from the inner cases.

veecle-telemetry/src/value.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,10 @@ impl ToStatic for Value<'_> {
155155
impl<'a> core::fmt::Display for Value<'a> {
156156
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
157157
match self {
158-
Value::String(value) => write!(f, "{value}"),
158+
// For strings, debug print so they will get delimiters, since we are explicitly
159+
// representing strings rather than directly human-targeted text, and they will be used
160+
// in situations where knowing where the string ends is important.
161+
Value::String(value) => write!(f, "{value:?}"),
159162
Value::Bool(value) => write!(f, "{value}"),
160163
Value::I64(value) => write!(f, "{value}"),
161164
Value::F64(value) => write!(f, "{value}"),

0 commit comments

Comments
 (0)