DEV Community

Ankit Sood
Ankit Sood

Posted on

Building Custom HTTP Request Metrics in Spring Boot: A Deep Dive into Observability

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 …

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Enable endpoints:

management.endpoints.web.exposure.include=health,metrics,prometheus
Enter fullscreen mode Exit fullscreen mode

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;
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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/...
Enter fullscreen mode Exit fullscreen mode

and now visit:

http://localhost:8080/actuator/prometheus
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)