~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/asyncai

0
0
main
53 lines · 1.6 KB Raw History
//! 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);
    }
}