Start Learning
Javaneer
Back to stage
Module 13·Advanced Observability

Metrics with Micrometer

Micrometer as SLF4J-for-metrics: counters, gauges, timers and distribution summaries, dimensional tags done right, and exporting to Prometheus.

15 min readAdvanced
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  💥  catastrophe

Never 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 car's dashboard instruments

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.

Instrument the overdue-fine feature

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.
Was this lesson helpful?
Edit this page on GitHub