use serde_json::Value;
use super::vocabulary;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuditFinding {
pub kind: AuditFindingKind,
pub key: String,
pub surface: String,
pub context: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AuditFindingKind {
UnknownVocabKey,
MetricMissingInstrument,
OrphanSpan,
}
impl AuditFinding {
pub fn line(&self) -> String {
match self.kind {
AuditFindingKind::UnknownVocabKey => format!(
"HARN-OBS-AUDIT: {surface} `{ctx}` attribute `{key}` is not declared in the harn.* vocabulary",
surface = self.surface,
ctx = self.context,
key = self.key,
),
AuditFindingKind::MetricMissingInstrument => format!(
"HARN-OBS-AUDIT: metric `{key}` lacks an `instrument` field (use harness.obs.{{counter,histogram,gauge}}; raw obs.metric() is not OTel-compatible)",
key = self.key,
),
AuditFindingKind::OrphanSpan => format!(
"HARN-OBS-AUDIT: span `{key}` has no trace_id (span must open through harness.obs.span/start_span)",
key = self.key,
),
}
}
}
pub fn audit_events(events: &[Value]) -> Vec<AuditFinding> {
let mut findings = Vec::new();
for entry in events {
audit_one(entry, &mut findings);
}
findings
}
fn audit_one(entry: &Value, findings: &mut Vec<AuditFinding>) {
let payload = entry.get("payload").unwrap_or(entry);
let Some(map) = payload.as_object() else {
return;
};
let kind = map.get("kind").and_then(Value::as_str).unwrap_or("");
let name = map
.get("name")
.and_then(Value::as_str)
.or_else(|| map.get("message").and_then(Value::as_str))
.unwrap_or("")
.to_string();
if kind == "metric" && !map.contains_key("instrument") {
findings.push(AuditFinding {
kind: AuditFindingKind::MetricMissingInstrument,
key: name.clone(),
surface: "metric".to_string(),
context: String::new(),
});
}
if kind == "span_end" {
let has_trace_id = map
.get("trace_id")
.and_then(Value::as_str)
.is_some_and(|id| !id.is_empty());
if !has_trace_id {
findings.push(AuditFinding {
kind: AuditFindingKind::OrphanSpan,
key: name.clone(),
surface: "span".to_string(),
context: String::new(),
});
}
}
if let Some(Value::Object(fields)) = map.get("fields") {
for key in fields.keys() {
if vocabulary::is_violation(key) {
findings.push(AuditFinding {
kind: AuditFindingKind::UnknownVocabKey,
key: key.clone(),
surface: kind.to_string(),
context: name.clone(),
});
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn metric_without_instrument_is_flagged() {
let events = vec![json!({"payload": {
"kind": "metric",
"name": "harn.mcp.calls",
"value": 1,
"fields": {},
}})];
let findings = audit_events(&events);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].kind, AuditFindingKind::MetricMissingInstrument);
assert!(findings[0].line().contains("harn.mcp.calls"));
}
#[test]
fn metric_with_instrument_passes() {
let events = vec![json!({"payload": {
"kind": "metric",
"name": "harn.mcp.calls",
"value": 1,
"instrument": "counter",
"fields": {"harn.mcp.server": "fs"},
}})];
assert!(audit_events(&events).is_empty());
}
#[test]
fn unknown_vocab_attribute_is_flagged() {
let events = vec![json!({"payload": {
"kind": "log",
"message": "boop",
"fields": {"harn.mcp.boops": "wat"},
}})];
let findings = audit_events(&events);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].kind, AuditFindingKind::UnknownVocabKey);
assert_eq!(findings[0].key, "harn.mcp.boops");
}
#[test]
fn span_end_without_trace_id_is_flagged() {
let events = vec![json!({"payload": {
"kind": "span_end",
"name": "raw_span",
"fields": {},
}})];
let findings = audit_events(&events);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].kind, AuditFindingKind::OrphanSpan);
}
#[test]
fn user_attributes_outside_harn_prefix_pass() {
let events = vec![json!({"payload": {
"kind": "log",
"message": "user log",
"fields": {"user.id": 7, "custom.tag": "ok"},
}})];
assert!(audit_events(&events).is_empty());
}
#[test]
fn compose_payload_arrays_descend_into_inner_entries() {
let events = vec![json!({"payload": {
"kind": "metric",
"name": "harn.pg.queries",
"instrument": "counter",
"fields": {"harn.pg.bogus": "nope"},
}})];
let findings = audit_events(&events);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].kind, AuditFindingKind::UnknownVocabKey);
assert_eq!(findings[0].key, "harn.pg.bogus");
}
}