Recipe: Stream Audit Events
Problem: compliance wants IAM’s audit trail in the central SIEM, not locked in an application table. You need a stream — without risking the business request when the broker hiccups.
Register an IamAuditSink
Section titled “Register an IamAuditSink”IamAuditSink is a fun interface: one method, one bean, done.
@Componentclass KafkaAuditSink( private val kafka: KafkaTemplate<String, String>, private val objectMapper: ObjectMapper,) : IamAuditSink {
override fun onAuditEvent(event: IamAuditEventView) { val payload = objectMapper.writeValueAsString( mapOf( "tenantId" to event.tenantId, "actor" to event.actorEmail, "entityType" to event.entityType, "entityId" to event.entityId, "action" to event.action, "oldValue" to event.oldValue, // JSON snapshot, if captured "newValue" to event.newValue, "correlationId" to event.correlationId, "at" to event.createdAt, ) ) kafka.send("iam-audit", event.tenantId.toString(), payload) }}Sinks receive IamAuditEventView — a flat, framework-free projection, never IAM’s persistence
types. IamAuditSink/IamAuditEventView ship in the contract module iam-api, so a
sink-only module needs just implementation("dev.mpofusindie:iam-api"). Register as many sinks
as you like; each is invoked independently.
Mind the view’s honest types when you map it: entityId is nullable, oldValue/newValue/
metadata are JSON strings (parse, don’t re-serialize), and correlationId is a free-form
String, not a UUID — parse it leniently.
What you get — the delivery contract
Section titled “What you get — the delivery contract”- The table stays the durable record. Sinks are a tap in addition to the default JPA
iam_audit_logstable; registering one changes nothing about default behavior. - After commit only. Delivery defers to
afterCommitwhen the audit write is transactional — a sink never sees an event whose transaction rolled back. - Best-effort, isolated. A throwing sink is caught and logged per sink; it can neither break the business request nor starve other sinks.
- At-most-once, in-process. No retries, no crash-recovery between commit and delivery.
Need guaranteed delivery? Poll
iam_audit_logsas your outbox — it is the transactionally-written record. - Not atomic with the business change. Delivery runs after the business transaction
commits, so a sink executes with no ambient transaction — a DB-writing sink must open its
own unit of work (e.g. a
PROPAGATION_REQUIRES_NEWTransactionTemplate); it cannot enlist in the business transaction. A sink failure loses only the mirror (the durableiam_audit_logsrow already exists), so never treat a sink mirror as system-of-record without a reconciliation job againstiam_audit_logs. - Threading follows the audit write — synchronous audits deliver on the request thread.
Sinks doing slow I/O should hand off internally (as
KafkaTemplate.sendalready does).
Correlation ids come free
Section titled “Correlation ids come free”Every request gets a correlation id: an inbound X-Correlation-Id header is reused (so a
gateway/mesh id survives end-to-end) or a UUID is generated; either way it’s echoed on the
response, stamped into Problem-Details error payloads, and delivered on
event.correlationId. To see it in your own logs, add it to the pattern:
logging: pattern: level: "%5p [%X{iam.correlation_id}]"One id now joins the user-visible error, the server log line, the audit row, and the SIEM event. For the full picture — where the id comes from and the metrics IAM emits alongside it — see the Observability guide.