Metrics with Micrometer
Micrometer as SLF4J-for-metrics: counters, gauges, timers and distribution summaries, dimensional tags done right, and exporting to Prometheus.
On this page
Micrometer is to metrics what SLF4J is to logging: a vendor-neutral facade you code against once, with pluggable backends (Prometheus, Datadog, CloudWatch...) chosen by a dependency. Spring Boot auto-configures it, so BookVault already emits dozens of metrics. This lesson is about instrumenting your own - picking the right meter type and, above all, tagging without blowing up your metrics backend.
The meter types
Micrometer offers a few meter types, and choosing the right one is most of the skill:
- Counter - a value that only ever increases: books borrowed, requests served, errors. You ask questions about its rate ("borrows per second"), not its raw total.
- Gauge - a value that goes up and down, sampled now: active loans, queue depth, cache size. A gauge observes a live value; you don't set it, you point it at something to read.
- Timer - records how long things take and how often: it captures count, total time, and max, and (configured) latency percentiles. The workhorse for request/operation latency.
- Distribution Summary - like a timer but for non-time distributions: payload sizes, batch counts.
@Component
class LoanMetrics {
private final Counter borrows;
private final Timer checkoutTimer;
LoanMetrics(MeterRegistry registry) {
this.borrows = Counter.builder("bookvault.loans.borrowed")
.description("Total books borrowed").register(registry);
this.checkoutTimer = Timer.builder("bookvault.checkout.duration")
.publishPercentiles(0.95, 0.99).register(registry);
// a gauge observes a live value - here, the current active-loan count
Gauge.builder("bookvault.loans.active", loanService, LoanService::activeCount)
.register(registry);
}
void recordBorrow() { borrows.increment(); }
<T> T timeCheckout(Supplier<T> op) { return checkoutTimer.record(op); }
}Tags: the dimensional model - and the cardinality trap
Modern metrics are dimensional: one metric name with tags (key-value labels) you slice by. Instead of
borrows_fiction, borrows_scifi, you record one bookvault.loans.borrowed with a genre tag and query
sum by (genre):
Counter.builder("bookvault.loans.borrowed")
.tag("genre", book.genre()) // good: bounded set of values
.tag("branch", book.branch()) // good: dozens of branches
.register(registry).increment();This is powerful - but tags are also the single biggest way to melt a metrics system. Every unique combination of tag values creates a separate stored time series. That's cardinality, and it multiplies:
genre (12) × branch (30) = 360 series ✅ fine
+ member_id (500,000) = 180,000,000 series 💥 catastropheNever tag with unbounded values
Tagging by user ID, email, book ID, request URL with IDs, or any high-cardinality value is the classic outage- maker - it can generate millions of time series and take down your metrics backend (and your bill). Rule: tags must have a small, bounded set of possible values (status, genre, region, HTTP method). High-cardinality identifiers belong in traces and logs, not metric tags.
What you already get free
Before writing custom meters, know Spring Boot auto-instruments a lot: http.server.requests (a Timer tagged
by URI, method, status - your latency and error rate), JVM memory and GC, thread pools, DataSource
connections, and cache stats. Export them to Prometheus by adding micrometer-registry-prometheus, which
exposes /actuator/prometheus for Prometheus to scrape. Add custom meters only for business signals
(borrows, checkout duration) the framework can't know about.
A counter is the odometer - only ever climbs; what you care about is the rate (km per hour) not the raw number. A gauge is the fuel or temperature needle - it rises and falls to show the value right now. A timer is a stopwatch on each trip that also tallies how many trips and their spread. Tags are labeling each reading by route and driver so you can compare - but label every reading by a unique trip GPS coordinate (unbounded) and your logbook becomes millions of one-off entries no one can read. Bounded labels group; unbounded ones explode.
BookVault adds fines for overdue returns. Product wants: (1) how many fines are charged, sliced by branch; (2) the current total unpaid fine amount across the system; (3) how long the fine-calculation step takes at p99. Pick the meter type for each, and name one tag you must NOT add.
Why must you avoid tagging a metric with a high-cardinality value like member_id?
Key takeaways
- Micrometer is a vendor-neutral metrics facade (like SLF4J for logs) with pluggable registries; Spring Boot auto-configures it.
- Choose the meter type: Counter (monotonic, ask for rate), Gauge (live up/down value), Timer (latency with percentiles), Distribution Summary (non-time distributions).
- Dimensional metrics use one name plus tags you slice by - but every unique tag-value combination is a separate time series.
- Never tag with unbounded values (member_id, URLs with IDs) - it causes a cardinality explosion that can take down the backend; tags must be low-cardinality.
- Spring Boot auto-instruments http.server.requests, JVM, pools, and caches; add custom meters only for business signals, and export via micrometer-registry-prometheus.