java 91 lines · 3 tabs

Track Failed Login Attempts with a Custom Micrometer Counter in Spring Boot

Shared by codesnips Sep 2026
3 tabs
package com.example.security.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;

@Service
public class LoginMetrics {

    private static final String METER_NAME = "logins.failed";

    private final MeterRegistry registry;

    public LoginMetrics(MeterRegistry registry) {
        this.registry = registry;
    }

    public void recordFailure(String reason) {
        Counter.builder(METER_NAME)
                .description("Number of failed login attempts")
                .tag("reason", sanitize(reason))
                .register(registry)
                .increment();
    }

    private String sanitize(String reason) {
        if (reason == null || reason.isBlank()) {
            return "unknown";
        }
        // Keep tag cardinality low: only allow a small, known charset
        return reason.toLowerCase().replaceAll("[^a-z_]", "_");
    }
}
3 files · java Explain with highlit

This snippet shows how a custom application-level metric is exposed through Micrometer in a Spring Boot service, using failed logins as a concrete example. Metrics like these are essential for observability: a spike in failed authentications often signals a credential-stuffing attack, a broken client, or an expired shared secret, and having a first-class counter makes that visible on a dashboard and alertable.

In LoginMetrics, the counting logic is encapsulated behind a small service so the rest of the application never touches MeterRegistry directly. Rather than registering a single global counter, Counter.builder is used lazily inside recordFailure with a reason tag, and MeterRegistry deduplicates meters by name plus tags, so repeated calls for the same reason increment the same series. Tagging by reason keeps cardinality low while still allowing queries such as failures grouped by bad_password versus unknown_user. The sanitize helper guards against unbounded cardinality — a classic Micrometer pitfall — by never letting arbitrary strings (like usernames) become tag values.

The AuthenticationFailureListener wires the metric to Spring Security without polluting business code. Spring publishes an AuthenticationFailureBadCredentialsEvent and related events whenever authentication fails, and an @EventListener method simply translates the exception type into a stable reason and delegates to LoginMetrics. This event-driven approach means the counter stays accurate even for failures that never reach a controller, and it decouples metric collection from the authentication flow itself.

Finally, MetricsConfig demonstrates a MeterRegistryCustomizer that applies a common application tag to every meter, which is how deployments distinguish services when many share one Prometheus. Because Micrometer is a facade, the same counter is exported to Prometheus, Datadog, or any backend without code changes; only the registry dependency differs.

The main trade-off is cardinality discipline: every distinct tag combination is a separate time series, so tags must be bounded, controlled values. When wired to the Actuator, this counter appears as logins_failed_total and can drive alerts the moment failures deviate from baseline.


Related snips

Share this code

Here's the card — post it anywhere.

Track Failed Login Attempts with a Custom Micrometer Counter in Spring Boot — share card
Link copied