Audit
AuditMiddleware emits structured audit events around each request. (v0.5.0) AuditEnricherMiddleware lets callers attach domain-safe metadata to those events without changing handlers.
Minimal example
use BetterRoute\Middleware\Audit\AuditMiddleware;
use BetterRoute\Middleware\Audit\ErrorLogAuditLogger;
$audit = new AuditMiddleware(
logger: new ErrorLogAuditLogger()
);
Event schema highlights
Produced by AuditEventFactory:
event(http_request)timestamprequestIdandtraceIdroute,methodoutcome(success|error)statusCodeerrorCodeanderrormessage (on failure)durationMs- compatibility alias:
status(ok|error) - (v0.5.0) any keys merged from
RequestContext::$attributes['audit'](see below)
Enrichment (v0.5.0)
AuditMiddleware reads RequestContext::$attributes['audit'] (associative array) and merges it into the emitted event under the existing schema. Anything you write to that attribute — directly or via AuditEnricherMiddleware — appears alongside the built-in fields.
use BetterRoute\Middleware\Audit\AuditEnricherMiddleware;
use BetterRoute\Middleware\Audit\AuditMiddleware;
$router->middleware([
new AuditEnricherMiddleware(
staticFields: ['resource' => 'account', 'channel' => 'public-client'],
includeClientIp: true
),
new AuditMiddleware($logger),
]);
AuditEnricherMiddleware adds the following keys to the audit attribute:
| Key | Source | Notes |
|---|---|---|
authProvider / authUserId / authSubject | RequestContext::$attributes['auth'] | Only emitted for fields actually present on the auth attribute. |
idempotencyKey | request Idempotency-Key header | Stored as a SHA-1 hash — never the raw value. |
clientIp | ClientIpResolver | Only when includeClientIp: true. Pass a custom resolver to honor your trusted proxies. |
any keys in staticFields | constructor arg | Passed through verbatim. Keep these domain-safe (no PII). |
Existing values on audit are preserved — handlers or earlier middleware can call $ctx->withAttribute('audit', [...]) and the enricher merges into them rather than replacing.
Order matters
Place AuditEnricherMiddleware after any auth middleware that populates auth, and before AuditMiddleware so the enriched attribute is in place when the event is emitted:
... -> AuthMiddleware -> AuditEnricherMiddleware -> AuditMiddleware -> handler
If you only need AuditMiddleware to read audit attributes you set in the handler (e.g. domainEvent => 'order.cancelled'), the enricher is optional.
Constructor
new AuditEnricherMiddleware(
array $staticFields = [],
?ClientIpResolver $clientIpResolver = null,
bool $includeClientIp = false
);
Scenario: audit trail for regulated writes
- attach
AuditEnricherMiddleware+AuditMiddlewareto the sensitive route group - enricher carries the auth provider/subject and a hashed idempotency key, so duplicate retries are correlatable
- feed events into SIEM/log pipeline
- correlate by
requestIdacross systems
Common mistakes
- Logging only failures (lose the success baseline)
- Emitting raw idempotency keys, tokens, or PII through
staticFields— keepauditpayloads safe to ship to a log aggregator - Placing the enricher before auth —
authUserIdwill be missing on every event - Writing unstructured free-text events directly to the logger
Validation checklist
- both success and error flows emit events
- duration is captured in milliseconds
- enriched events carry
authProvider/authUserId/authSubjectwhen an auth middleware ran upstream idempotencyKeyin the event is a hash, not the raw header value- event payload is JSON serializable