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_]", "_");
}
}
package com.example.security.metrics;
import org.springframework.context.event.EventListener;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.authentication.event.AuthenticationFailureDisabledEvent;
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
@Component
public class AuthenticationFailureListener {
private final LoginMetrics loginMetrics;
public AuthenticationFailureListener(LoginMetrics loginMetrics) {
this.loginMetrics = loginMetrics;
}
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent event) {
loginMetrics.recordFailure(reasonFor(event));
}
private String reasonFor(AbstractAuthenticationFailureEvent event) {
if (event instanceof AuthenticationFailureBadCredentialsEvent) {
return "bad_credentials";
}
if (event instanceof AuthenticationFailureDisabledEvent) {
return "account_disabled";
}
AuthenticationException ex = event.getException();
return ex == null ? "unknown" : ex.getClass().getSimpleName();
}
}
package com.example.security.metrics;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> commonTags(
@Value("${spring.application.name:auth-service}") String appName) {
return registry -> registry.config().commonTags("application", appName);
}
@Bean
public MeterFilter denyHighCardinalityUsers() {
// Defensive: drop any accidental per-user tag on our login meters
return MeterFilter.ignoreTags("username", "principal");
}
}
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
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
Share this code
Here's the card — post it anywhere.