use serde::Serialize;
use tracing::warn;
#[derive(Serialize)]
struct AuditEvent<'a> {
actor: &'a str,
action: &'a str,
target: Option<&'a str>,
payload: serde_json::Value,
occurred_at: chrono::DateTime<chrono::Utc>,
}
fn operator_sub() -> String {
std::env::var("KANADE_ACTOR")
.or_else(|_| std::env::var("USERNAME"))
.or_else(|_| std::env::var("USER"))
.unwrap_or_else(|_| "unknown".to_string())
}
pub async fn record(
client: &async_nats::Client,
action: &str,
target: Option<&str>,
mut payload: serde_json::Value,
) {
const ACTOR: &str = "operator";
if let Some(obj) = payload.as_object_mut() {
obj.entry("sub".to_string())
.or_insert_with(|| operator_sub().into());
obj.entry("source".to_string())
.or_insert_with(|| "cli".into());
}
let subject = match target {
Some(t) => format!("audit.{ACTOR}.{action}.{t}"),
None => format!("audit.{ACTOR}.{action}"),
};
let event = AuditEvent {
actor: ACTOR,
action,
target,
payload,
occurred_at: chrono::Utc::now(),
};
let body = match serde_json::to_vec(&event) {
Ok(b) => b,
Err(e) => {
warn!(error = %e, subject = %subject, "audit serialize failed");
return;
}
};
if let Err(e) = client.publish(subject.clone(), body.into()).await {
warn!(error = %e, subject = %subject, "audit publish failed");
return;
}
if let Err(e) = client.flush().await {
warn!(error = %e, subject = %subject, "audit flush failed");
}
}