use uuid::Uuid;
use super::outcome::{FailureClass, Substrate};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FinalizerAuditGap {
pub(crate) failure_class: FailureClass,
pub(crate) diagnostic_id: Uuid,
pub(crate) record_id: Uuid,
pub(crate) substrate: Substrate,
pub(crate) namespace: String,
pub(crate) entry_point: &'static str,
}
pub(crate) trait LogSink: Send + Sync {
fn record_audit_gap(&self, gap: &FinalizerAuditGap);
}
pub(crate) struct TracingLogSink;
impl LogSink for TracingLogSink {
fn record_audit_gap(&self, gap: &FinalizerAuditGap) {
tracing::error!(
failure_class = ?gap.failure_class,
diagnostic_id = %gap.diagnostic_id,
record_id = %gap.record_id,
substrate = ?gap.substrate,
namespace = %gap.namespace,
entry_point = gap.entry_point,
"secret_gate_finalizer: audit-gap (failure-audit write itself failed)"
);
}
}
#[cfg(test)]
pub(crate) struct CapturingLogSink {
pub(crate) gaps: std::sync::Mutex<Vec<FinalizerAuditGap>>,
}
#[cfg(test)]
impl CapturingLogSink {
pub(crate) fn new() -> Self {
Self {
gaps: std::sync::Mutex::new(Vec::new()),
}
}
pub(crate) fn snapshot(&self) -> Vec<FinalizerAuditGap> {
self.gaps.lock().unwrap().clone()
}
}
#[cfg(test)]
impl LogSink for CapturingLogSink {
fn record_audit_gap(&self, gap: &FinalizerAuditGap) {
self.gaps.lock().unwrap().push(gap.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capturing_sink_survives_and_records_exactly_one_gap() {
let sink = CapturingLogSink::new();
let gap = FinalizerAuditGap {
failure_class: FailureClass::RecordWrite,
diagnostic_id: Uuid::new_v4(),
record_id: Uuid::new_v4(),
substrate: Substrate::Entity,
namespace: "local".to_string(),
entry_point: "entity.create",
};
sink.record_audit_gap(&gap);
let captured = sink.snapshot();
assert_eq!(captured.len(), 1);
assert_eq!(captured[0], gap);
}
#[test]
fn tracing_sink_does_not_panic_on_record() {
let sink = TracingLogSink;
sink.record_audit_gap(&FinalizerAuditGap {
failure_class: FailureClass::Stamp,
diagnostic_id: Uuid::new_v4(),
record_id: Uuid::new_v4(),
substrate: Substrate::Note,
namespace: "local".to_string(),
entry_point: "note.create",
});
}
}