Observability: Metrics & Correlation IDs
IAM instruments its own hot path (permission resolution, cache lookups, @RequiresPermission
denials) and stamps a correlation id on every request. Both are zero-config and zero-cost
until you opt in — Micrometer is a compileOnly dependency, so a host without it pays
nothing, and a host that already ships it gets IAM’s meters for free.
What IAM emits
Section titled “What IAM emits”| Signal | Out of the box | Turns on when |
|---|---|---|
| Metrics | Permission-resolution timing, cache hit/miss, denial counts | micrometer-core is on the classpath and a MeterRegistry bean exists |
| Correlation ids | One id per request — in logs (MDC), echoed on responses, on Problem-Details bodies, on audit rows | Always on — no classpath dependency |
Metrics
Section titled “Metrics”Turning them on — add a registry
Section titled “Turning them on — add a registry”IAM records through its own IamMetrics seam, never through Micrometer types directly. When
micrometer-core is absent, a no-op implementation is wired and nothing is measured. Add a
Micrometer-backed registry and the meters appear with no IAM configuration at all — the
usual way is the Actuator starter:
// Brings in micrometer-core + a MeterRegistry; IAM's meters light up automatically.implementation("org.springframework.boot:spring-boot-starter-actuator")// Plus your monitoring backend's registry, e.g. Prometheus:implementation("io.micrometer:micrometer-registry-prometheus")That’s it — no iam.metrics.* flag needed to switch it on. iam.metrics.enabled exists only
to switch it off again for a host that has Micrometer but wants IAM silent in it:
iam: metrics: enabled: false # default true — suppresses IAM's meters even with micrometer-core presentIf you run a non-Micrometer telemetry stack, register your own IamMetrics bean; IAM’s beans
are @ConditionalOnMissingBean and back off.
The meters
Section titled “The meters”Verified against MicrometerIamMetrics:
| Meter | Type | Tags | Recorded when |
|---|---|---|---|
iam.resolve.duration |
Timer | scope = present | absent |
A user’s effective permissions are freshly resolved (cache miss). scope=present when the resolution was scope-qualified |
iam.cache |
Counter | result = hit | miss |
Every permission-cache lookup |
iam.check.denied |
Counter | resource, action |
@RequiresPermission denies a request (field denials count under their resource) |
Reading them
Section titled “Reading them”With the Prometheus registry, the meters surface on the Actuator scrape endpoint:
management: endpoints: web: exposure: include: health, prometheus# GET /actuator/prometheusiam_resolve_duration_seconds_count{scope="present"} 1834.0iam_resolve_duration_seconds_sum{scope="present"} 12.47iam_cache_total{result="hit"} 58210.0iam_cache_total{result="miss"} 1834.0iam_check_denied_total{resource="loan",action="approve"} 4.0iam.cache{result=hit} over hit + miss is your permission-cache hit ratio; a climbing
iam.check.denied for one (resource, action) is either a missing grant or someone probing.
See Caching for what drives those hits and misses.
Correlation ids
Section titled “Correlation ids”Every request gets a correlation id, with no dependency to add. A filter running ahead of the
tenant filter and Spring Security either reuses an inbound X-Correlation-Id header (so a
gateway or service-mesh id survives end-to-end) or generates a UUID — so even a 401 from the
auth layer is correlated. Inbound values are attacker-supplied, so they’re truncated to 64
characters and restricted to a safe token alphabet before being reflected anywhere.
That one id then shows up in four places:
-
Server logs — it’s put in the SLF4J MDC under
iam.correlation_id. Add it to your pattern to see it on every line:logging:pattern:level: "%5p [%X{iam.correlation_id}]" -
The response — echoed back on the
X-Correlation-Idheader, so a caller can quote it when reporting a problem. -
Problem-Details error bodies — stamped as a
correlationIdextension member (RFC 7807 §3.2), joining the user-visible error to your logs without guesswork. -
Audit rows — carried onto every recorded audit event as
IamAuditEventView.correlationId, so the durable record and any downstream stream share the same id.
One id now links the user-visible error, the log line, the audit row, and any event you stream out.
Streaming audit events
Section titled “Streaming audit events”The audit trail is where per-tenant and per-actor detail lives. IAM writes it to the
iam_audit_logs table by default and lets you tap the same events — correlation id included
— to Kafka, an SIEM, or a webhook by registering an IamAuditSink bean. Delivery is after-commit,
per-sink isolated, and at-most-once.
See the Stream Audit Events recipe for the full sink example and
delivery contract, and SPI Hooks for the IamAuditSink /
IamAuditEventView reference.