//! 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(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); } }