KemalIdentity::SecurityEventSink
Where security events go, for an application that wants them somewhere other than a log.
class SiemSink < KemalIdentity::SecurityEventSink
def record(event : KemalIdentity::SecurityEvent) : Nil
@queue.push({name: event.name, actor: event.subject, at: event.at})
end
end
KemalIdentity.event_sink = SiemSink.new
Must not raise
The same rule RateLimiter has, for a sharper reason. Measured in
blueprints/0025-maturity-validation-results.md: a Log::Backend that raises takes
authentication down under :direct dispatch — the exception leaves
Passwords::Authenticator#authenticate and every login becomes a 500 — while under :async
it kills the dispatcher fiber and the audit trail goes quiet with nothing said.
For a security library the second is the worse one. So a sink is documented not to raise, and
KemalIdentity.event_sink= wraps it anyway: an exception is caught, counted, and reported
through Log at error level, which is a channel the broken sink does not own. Authentication
is never refused because a sink failed, and a sink that is failing is never silent.
Correlation without global state
A request id belongs to the request, not to a process. Hold it in the sink you construct per application and read it from wherever your framework keeps request-scoped state — nothing here reaches for a class variable, and nothing here needs one.
Instance methods
Records one event. Called on the request path, so it should enqueue rather than block on
somebody else's server — the same reason Notifier#deliver says so.