DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on • Originally published at github.com

I built a live Spring Boot Actuator dashboard: health aggregation, Micrometer meter types, and the Prometheus scrape format

Every Spring Boot tutorial shows you the same two screenshots: the /actuator/health JSON blob, and a wall of /actuator/prometheus text. Then it moves on. What none of them show is the behavior — how health rolls up from its components, how a Counter differs from a Gauge differs from a Timer, and how those numbers turn into the scrape format a monitoring stack reads.

So I built a live one, and it runs in your browser with no backend.

▶ Live demo: https://dev48v.github.io/actuator-metrics/
Source: https://github.com/dev48v/actuator-metrics

Health is the worst component, not an average

/actuator/health doesn't report "mostly fine." It aggregates every registered HealthIndicator and reports the worst status it finds:

{
  "status": "DOWN",
  "components": {
    "db":        { "status": "DOWN" },
    "diskSpace": { "status": "UP" },
    "ping":      { "status": "UP" },
    "redis":     { "status": "UP" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Three green components and one red one → the top-level status is DOWN. In the demo you flip any component and watch the aggregate flip with it. This matters more than it looks: your load balancer's readiness probe reads that top-level status. One dependency down = the whole instance pulled out of rotation.

The meter types are the part people conflate

Micrometer has distinct meter types and picking the wrong one gives you meaningless data. The demo has one card per type, all updating as you send traffic:

  • Counterhttp.server.requests. Monotonically increasing. You never read a counter directly; you rate() it in your query. "How many requests?" → Counter.
  • Gaugejvm.memory.used, system.cpu.usage. A value sampled right now; it can go up or down. Never increment a gauge in code — you register a function and Micrometer samples it. "How much memory right now?" → Gauge.
  • Timer — request latency. Records count and total time, and gives you mean, max, and percentiles (p95/p99) plus a histogram. "How long did it take?" → Timer.
  • DistributionSummary — like a Timer but for magnitudes that aren't time (payload sizes, batch counts).

The single most common mistake is reaching for a Gauge when you want a Counter, then being confused when a restart resets your "total." Counters are built to be rate()-d; gauges are point-in-time samples.

The Prometheus format is just those meters, serialized

The scrape endpoint is not a separate system — it's the same registry rendered as text:

# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{method="GET",uri="/api/orders",status="200"} 42
http_server_requests_seconds_sum{method="GET",uri="/api/orders",status="200"} 3.114
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Eden Space"} 309329920
Enter fullscreen mode Exit fullscreen mode

Each # TYPE line tells Prometheus how to treat the metric; the {label="value"} pairs are your tags (dimensions you can group and filter by). A Timer becomes a _count + _sum pair — which is exactly why you can compute average latency as rate(_sum) / rate(_count) in PromQL. Seeing the Timer card and the Prometheus text side by side makes that click.

Try it

Send a few requests and watch the Counter climb and the Timer histogram fill. Turn on auto-traffic. Then flip db to DOWN and watch requests start failing — the 5xx split in the Counter and the tail in the Timer both move. Same numbers, three views: dashboard, meters, scrape text.

If it helps the meter types finally stick, a star helps others find it: https://github.com/dev48v/actuator-metrics

Top comments (0)