use serde_json::{Map, Value};
pub const SCHEMA_VERSION: u64 = 1;
pub const MESSAGE_FIELD: &str = "message";
pub struct StaticFields<'a> {
pub service: &'a str,
pub service_version: &'a str,
pub run_context: &'a str,
pub run_id: &'a str,
pub parent_op_id: Option<&'a str>,
}
pub fn build_record(
statics: &StaticFields<'_>,
level: &str,
target: &str,
ts: &str,
mut span_fields: Map<String, Value>,
mut event_fields: Map<String, Value>,
) -> Map<String, Value> {
let message = event_fields
.remove(MESSAGE_FIELD)
.and_then(|value| value.as_str().map(str::to_string))
.unwrap_or_default();
let mut record = Map::new();
record.insert("schema".into(), Value::from(SCHEMA_VERSION));
record.insert("ts".into(), Value::from(ts));
record.insert("level".into(), Value::from(level));
record.insert("target".into(), Value::from(target));
record.insert(MESSAGE_FIELD.into(), Value::from(message));
record.insert("service".into(), Value::from(statics.service));
record.insert(
"service_version".into(),
Value::from(statics.service_version),
);
record.insert("run_context".into(), Value::from(statics.run_context));
record.insert("run_id".into(), Value::from(statics.run_id));
if let Some(parent) = statics.parent_op_id {
record.insert("parent_op_id".into(), Value::from(parent));
}
if let Some(op_id) = span_fields.remove(crate::correlation::OP_ID_FIELD) {
record.insert(crate::correlation::OP_ID_FIELD.into(), op_id);
}
record.append(&mut span_fields);
for (key, value) in event_fields {
record.insert(key, value);
}
record
}
pub fn to_line(record: &Map<String, Value>) -> String {
Value::Object(record.clone()).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn statics() -> StaticFields<'static> {
StaticFields {
service: "dig-node",
service_version: "1.2.3",
run_context: "service",
run_id: "11111111-1111-4111-8111-111111111111",
parent_op_id: None,
}
}
#[test]
fn golden_minimal_line() {
let record = build_record(
&statics(),
"INFO",
"dig_node::serve",
"2026-07-16T00:00:00Z",
Map::new(),
{
let mut m = Map::new();
m.insert("message".into(), Value::from("listening"));
m
},
);
assert_eq!(
to_line(&record),
r#"{"schema":1,"ts":"2026-07-16T00:00:00Z","level":"INFO","target":"dig_node::serve","message":"listening","service":"dig-node","service_version":"1.2.3","run_context":"service","run_id":"11111111-1111-4111-8111-111111111111"}"#
);
}
#[test]
fn op_id_ordered_after_correlation_and_parent_included() {
let mut statics = statics();
statics.parent_op_id = Some("op-parent");
let mut span = Map::new();
span.insert("op_id".into(), Value::from("op-child"));
span.insert("peer".into(), Value::from("1.2.3.4"));
let record = build_record(
&statics,
"WARN",
"t",
"2026-07-16T00:00:00Z",
span,
Map::new(),
);
let line = to_line(&record);
let parent = line.find("parent_op_id").unwrap();
let op = line.find("\"op_id\"").unwrap();
let peer = line.find("peer").unwrap();
assert!(parent < op && op < peer, "line: {line}");
}
#[test]
fn event_field_shadows_span_field() {
let mut span = Map::new();
span.insert("stage".into(), Value::from("connect"));
let mut event = Map::new();
event.insert("stage".into(), Value::from("handshake"));
let record = build_record(&statics(), "INFO", "t", "2026-07-16T00:00:00Z", span, event);
assert_eq!(record.get("stage"), Some(&Value::from("handshake")));
}
#[test]
fn missing_message_becomes_empty_string() {
let record = build_record(
&statics(),
"INFO",
"t",
"2026-07-16T00:00:00Z",
Map::new(),
Map::new(),
);
assert_eq!(record.get("message"), Some(&Value::from("")));
}
}