use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use rdkafka::config::ClientConfig;
use rdkafka::producer::{FutureProducer, FutureRecord};
use serde::Serialize;
use tokio::sync::mpsc;
#[derive(Debug, Clone, Serialize)]
pub struct DecisionEvent {
#[serde(rename = "eventType")]
pub event_type: &'static str,
pub sub: String,
pub permission: String,
pub decision: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(rename = "policyVersion", skip_serializing_if = "Option::is_none")]
pub policy_version: Option<u64>,
pub service: String,
#[serde(rename = "occurredAt")]
pub occurred_at_ms: i64,
}
impl DecisionEvent {
pub fn new(
service: impl Into<String>,
sub: impl Into<String>,
permission: impl Into<String>,
decision: impl Into<String>,
reason: Option<String>,
policy_version: Option<u64>,
) -> Self {
Self {
event_type: "DECISION",
sub: sub.into(),
permission: permission.into(),
decision: decision.into(),
reason,
policy_version,
service: service.into(),
occurred_at_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0),
}
}
}
#[derive(Clone)]
pub struct DecisionAuditSink {
tx: mpsc::Sender<DecisionEvent>,
dropped: Arc<AtomicU64>,
}
impl DecisionAuditSink {
pub fn spawn(
brokers: &str,
topic: impl Into<String>,
capacity: usize,
) -> Result<Self, anyhow::Error> {
let producer: FutureProducer = ClientConfig::new()
.set("bootstrap.servers", brokers)
.set("message.timeout.ms", "5000")
.set("acks", "1")
.create()?;
let topic = topic.into();
let (tx, mut rx) = mpsc::channel::<DecisionEvent>(capacity);
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
let Ok(payload) = serde_json::to_vec(&event) else {
continue;
};
let key = event.sub.clone();
let record = FutureRecord::to(&topic).key(&key).payload(&payload);
if let Err((e, _)) = producer.send(record, Duration::from_secs(5)).await {
#[cfg(feature = "observability")]
tracing::warn!(error = %e, "decision audit publish failed");
let _ = e;
}
}
});
Ok(Self { tx, dropped: Arc::new(AtomicU64::new(0)) })
}
pub fn record(&self, event: DecisionEvent) {
if self.tx.try_send(event).is_err() {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
}
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}