Modern APIs often serve multiple clients i.e mobile apps, partner services, internal tools and Knowing who is calling you and how often is critical for:
- Capacity planning: scale before peak traffic hits.
- Fair usage & billing: detect high-volume consumers.
- Alerting & SLOs: raise alarms when specific clients misbehave.
Let’s build a zero-dependency filter that tracks request counts by HTTP method and a custom consumer header and exports those metrics through Micrometer.
Architecture at a Glance
┌──────────┐ ┌────────────────────┐ ┌────────────┐
│ Client │ ───▶ │Custom MetricsFilter│ ───▶ │ Controller │
└──────────┘ └────────────────────┘ └────────────┘
│
▼
Micrometer Registry
│
▼
Prometheus / Datadog / CloudWatch …
Our solution uses a Spring Boot servlet filter that intercepts every HTTP request and records metrics using Micrometer.
Flow looks like:
- Request in → Filter → Controller
- Filter → Micrometer Counter with tags for method and consumer.
- Metrics scraped or pushed to your observability backend.
Now, let's go through the building process step by step:
Step 1: Add Micrometer
Spring Boot 3+ includes Micrometer by default when you use the spring-boot-starter-actuator.
For Prometheus:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Enable endpoints:
management.endpoints.web.exposure.include=health,metrics,prometheus
Step 2: The Filter Code
@Component
@Order(1)
public class CustomHttpMetricsFilter extends OncePerRequestFilter {
private static final ConcurrentHashMap<String, Counter> COUNTERS = new ConcurrentHashMap<>();
private static final String METRIC = "http_requests";
@Autowired
private MeterRegistry registry;
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain)
throws ServletException, IOException {
incrementCounter(req);
chain.doFilter(req, res);
}
private void incrementCounter(HttpServletRequest req) {
String consumer = req.getHeader("CONSUMER.ID");
if (consumer == null) consumer = "null";
String key = req.getMethod() + ":" + consumer;
COUNTERS.compute(key, (k, existing) -> {
Counter c = existing;
if (c == null) {
c = Counter.builder(METRIC)
.tag("method", req.getMethod())
.tag("consumer", consumer)
.description("Per-consumer HTTP request count")
.register(registry);
}
c.increment();
return c;
});
}
}
Key points
-
OncePerRequestFilter
guarantees one execution per HTTP request. -
ConcurrentHashMap
and compute makes thread-safe cache of Counter objects. -
Tags
method and consumer make each counter unique and queryable.
Step 3: Observe the Metrics
Run the app and hit it with a few GET/POST requests:
curl -H "CONSUMER.ID: mobile-app" http://localhost:8080/api/...
and now visit:
http://localhost:8080/actuator/prometheus
We should see the following in the response:
http_requests_total{method="GET", consumer="mobile-app"} 42.0
http_requests_total{method="POST", consumer="partner-service"} 5.0
Why Not Use @Timed?
Micrometer provides @Timed for controller methods, but that’s per-endpoint and doesn’t automatically group by a custom header.
Our filter gives you:
- Cross-cutting coverage: every request, even for static resources or error pages.
- Dynamic tags: any header or attribute can become a label.
Some Production Tips & Enhancements
Label Cardinality
Too many unique consumer IDs can lead to huge time series.
Solution: restrict IDs to a known set or hash/anonymize them.Metrics Cleanup is required as counter objects never shrink.
Solution: implement TTL eviction or register only for known consumers.Latency & Status Codes
To measure request duration and tags for HTTP status add a timer .Security
Expose /actuator/prometheus only to your metrics scraper.-
Testing
Use MockMvc and assert
Final Thoughts
With fewer than 50 lines of code, you get real-time, per-consumer traffic visibility. Whether you’re debugging a sudden spike or preparing for a product launch, these metrics become invaluable.
“You can’t improve what you don’t measure.”
Start measuring today—your future self will thank you.
Top comments (0)