Files
agentic-dev/packages/core-audit/src/multi-sink-audit-log.ts

46 lines
1.5 KiB
TypeScript

import type { AuditEntry } from "@repo/core-shared/audit";
import type { IAuditLog } from "./audit-log.interface";
/**
* Fan-out wrapper. Delivers each entry to every inner sink with settle-all
* semantics — one failing sink doesn't drop the audit entry from others.
*
* Failures emit a structured `audit-sink-error` JSON line to stderr.
* Stderr (not via OTel/Sentry) avoids recursion: if Sentry is one of the
* sinks failing and we routed the error back through Sentry's reporter,
* we'd loop. Stderr is consumed by the same log shipper as audit entries
* themselves, so the operator sees the failure in their aggregator.
*/
export class MultiSinkAuditLog implements IAuditLog {
constructor(private readonly sinks: IAuditLog[]) {}
async record(entry: AuditEntry): Promise<void> {
const results = await Promise.allSettled(this.sinks.map((s) => s.record(entry)));
for (const r of results) {
if (r.status === "rejected") {
this.reportSinkError(r.reason);
}
}
}
async eraseSubject(actorId: string, mode: "pseudonymize" | "delete"): Promise<void> {
const results = await Promise.allSettled(
this.sinks.map((s) => s.eraseSubject(actorId, mode)),
);
for (const r of results) {
if (r.status === "rejected") {
this.reportSinkError(r.reason);
}
}
}
private reportSinkError(reason: unknown): void {
const line = JSON.stringify({
_type: "audit-sink-error",
error: String(reason),
at: new Date().toISOString(),
});
process.stderr.write(line + "\n");
}
}