1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//! NDJSON stderr logging. Every log line is one JSON object with `level` and
//! `event`; never free-form text.
use serde_json::{Map, Value};
/// Emits `{"level": level, "event": event, ...fields}` as one stderr line.
/// `fields` must be a JSON object (anything else is logged under `data`).
pub fn emit(level: &str, event: &str, fields: Value) {
eprintln!("{}", record(level, event, fields));
}
/// Ends a binary: an error becomes one NDJSON `fatal` line and exit code 1.
pub fn exit_on_error<E: std::fmt::Display>(binary: &str, result: Result<(), E>) {
if let Err(error) = result {
emit(
"error",
"fatal",
serde_json::json!({"binary": binary, "error": error.to_string()}),
);
std::process::exit(1);
}
}
pub fn record(level: &str, event: &str, fields: Value) -> Value {
let mut map = match fields {
Value::Object(map) => map,
Value::Null => Map::new(),
other => Map::from_iter([("data".to_string(), other)]),
};
map.insert("level".to_string(), Value::String(level.to_string()));
map.insert("event".to_string(), Value::String(event.to_string()));
Value::Object(map)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::record;
#[test]
fn records_are_single_json_objects() {
let line = record(
"error",
"session.write_failed",
json!({"error": "disk\nfull"}),
);
assert_eq!(line["level"], "error");
assert_eq!(line["event"], "session.write_failed");
assert!(!line.to_string().contains('\n'));
assert_eq!(record("info", "x", json!(3))["data"], 3);
}
}