Configuration, Profiles & Actuator

Metrics with Micrometer

18 min Lesson 8 of 13

Metrics with Micrometer

Knowing that your application is alive (a health check) is necessary but not sufficient. To operate a production service confidently you need to answer questions like: how many requests per second is this endpoint handling? What is the 95th-percentile response time? How full is the database connection pool? These answers come from metrics, and in Spring Boot 3 the library that collects and exports them is Micrometer.

What Micrometer Is

Micrometer is a vendor-neutral metrics facade — the same API works whether you export data to Prometheus, Datadog, InfluxDB, CloudWatch, or a dozen other backends. The Spring Boot Actuator dependency pulls Micrometer in automatically; you do not need to add it separately.

Micrometer is to metrics what SLF4J is to logging. Write once against the Micrometer API; swap the monitoring back-end by changing a dependency and a few properties — no application code changes required.

The central object is the MeterRegistry. Spring Boot auto-configures one and adds it to the application context. Inject it wherever you need to record measurements.

The /metrics Endpoint

When spring-boot-starter-actuator is on the classpath, the /actuator/metrics endpoint is available (HTTP GET). It lists every meter name that Micrometer has registered:

GET /actuator/metrics { "names": [ "application.started.time", "disk.free", "disk.total", "http.server.requests", "jvm.buffer.count", "jvm.gc.pause", "jvm.memory.max", "jvm.memory.used", "jvm.threads.live", "process.cpu.usage", "system.cpu.count", "hikaricp.connections.active", "hikaricp.connections.idle" ] }

Drill into a specific meter by appending its name:

GET /actuator/metrics/jvm.memory.used { "name": "jvm.memory.used", "description": "The amount of used memory", "baseUnit": "bytes", "measurements": [ { "statistic": "VALUE", "value": 83886080 } ], "availableTags": [ { "tag": "area", "values": ["heap", "nonheap"] }, { "tag": "id", "values": ["G1 Eden Space", "Metaspace", ...] } ] }

Filter by tag using a query parameter: /actuator/metrics/jvm.memory.used?tag=area:heap

Built-In Meters

Spring Boot registers a rich set of meters automatically — you get all of the following with zero configuration:

  • JVM metrics (jvm.*) — memory areas, GC pause counts and durations, thread states, class loading.
  • HTTP server metrics (http.server.requests) — request count, total time, max time, tagged by URI, method, and HTTP status.
  • Process metrics (process.*) — CPU usage, uptime, file descriptors.
  • System metrics (system.*) — CPU count, load average, disk space.
  • HikariCP metrics (hikaricp.*) — pool size, active connections, idle connections, pending acquisitions, timeouts.
  • Logback metrics (logback.events) — event counts by level (INFO, WARN, ERROR).

Recording Custom Metrics

The four primary meter types cover almost every measurement need:

  • Counter — a monotonically increasing value. Use for events: orders placed, emails sent, errors caught.
  • Gauge — a current snapshot that can go up or down. Use for sizes: queue depth, cache size, active sessions.
  • Timer — records durations and counts. Use for operations with latency: HTTP calls, database queries.
  • DistributionSummary — records a distribution of values without a time unit. Use for sizes: request payload bytes, batch sizes.

Counter Example

import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.stereotype.Service; @Service public class OrderService { private final Counter ordersPlaced; public OrderService(MeterRegistry registry) { this.ordersPlaced = Counter.builder("orders.placed") .description("Total number of orders placed") .tag("channel", "web") .register(registry); } public void placeOrder(Order order) { // ... business logic ... ordersPlaced.increment(); } }

Timer Example

import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.stereotype.Service; @Service public class PaymentService { private final Timer paymentTimer; public PaymentService(MeterRegistry registry) { this.paymentTimer = Timer.builder("payment.processing") .description("Time spent processing payments") .tag("provider", "stripe") .publishPercentiles(0.5, 0.95, 0.99) // p50, p95, p99 .register(registry); } public PaymentResult charge(PaymentRequest req) { return paymentTimer.record(() -> { // ... call payment provider ... return new PaymentResult(); }); } }

Gauge Example

import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.stereotype.Component; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @Component public class TaskQueue { private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(); public TaskQueue(MeterRegistry registry) { Gauge.builder("task.queue.size", queue, BlockingQueue::size) .description("Number of pending tasks in the queue") .register(registry); } public void enqueue(Runnable task) { queue.offer(task); } }
Prefer constructor injection of meters. Build and register the meter once in the constructor, then call increment() / record() many times. Creating a new meter object per method call is wasteful and causes duplicate-registration warnings.

Tags: The Key to Useful Dashboards

Raw counts are rarely useful on their own. Tags turn a single meter into a multi-dimensional dataset. When you tag orders.placed with channel=web and channel=mobile separately, your monitoring tool can aggregate or split the series as needed.

Keep tags low-cardinality — a finite, small set of values. Never use user IDs, order IDs, or any unbounded value as a tag. Each unique tag combination creates a new time-series in your monitoring backend; high-cardinality tags explode storage and query costs.

High-cardinality tags destroy monitoring systems. A tag like tag("userId", userId.toString()) creates millions of unique series. Use low-cardinality categorical values only: status codes, region names, feature flags.

Exporting to Prometheus

The /actuator/metrics endpoint is convenient for ad-hoc inspection, but production monitoring systems scrape metrics in bulk. Prometheus is the most common choice. Add the registry dependency:

<!-- pom.xml --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>

Spring Boot auto-configures the Prometheus registry and exposes the /actuator/prometheus endpoint in Prometheus text format. Add one line to your scrape config and every meter you registered — built-in and custom — flows into Prometheus automatically.

# application.properties management.endpoints.web.exposure.include=health,info,metrics,prometheus

Common Pitfalls

  • Forgetting to call .register(registry) — the meter silently does nothing and never appears in /actuator/metrics.
  • Recording time manually with System.currentTimeMillis() instead of using a Timer — you lose percentiles, histogram buckets, and the Micrometer clock abstraction (which can be replaced in tests).
  • Using @Timed on a non-Spring-managed bean — the AOP proxy that drives the annotation is only created for Spring beans.

Summary

Micrometer gives Spring Boot 3 applications a rich, vendor-neutral metrics layer. You get JVM, HTTP, pool, and system meters for free; add domain-specific Counter, Timer, Gauge, and DistributionSummary meters through the injected MeterRegistry. Keep tags low-cardinality, publish percentiles on latency-sensitive timers, and export to Prometheus (or any other registry) with a single dependency. The next lesson covers securing and customizing the Actuator endpoints so that only the right consumers can access this data.