Skip to content

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.

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

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 present

If you run a non-Micrometer telemetry stack, register your own IamMetrics bean; IAM’s beans are @ConditionalOnMissingBean and back off.

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)

With the Prometheus registry, the meters surface on the Actuator scrape endpoint:

management:
endpoints:
web:
exposure:
include: health, prometheus
# GET /actuator/prometheus
iam_resolve_duration_seconds_count{scope="present"} 1834.0
iam_resolve_duration_seconds_sum{scope="present"} 12.47
iam_cache_total{result="hit"} 58210.0
iam_cache_total{result="miss"} 1834.0
iam_check_denied_total{resource="loan",action="approve"} 4.0

iam.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.

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-Id header, so a caller can quote it when reporting a problem.

  • Problem-Details error bodies — stamped as a correlationId extension 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.

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.