class

KemalIdentity::SecurityEventSink

Inherits Reference < Object

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

record(event : SecurityEvent) : Nil

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.

Source