Metrics & Micrometer
Micrometer is the metrics facade bundled with Spring Boot Actuator — think of it as SLF4J, but for metrics. You instrument your code against a vendor-neutral API, and a registry ships those measurements to whatever monitoring backend you choose (Prometheus, Datadog, CloudWatch, Graphite) by swapping one dependency. Out of the box you already get JVM memory, garbage collection, CPU, HTTP request timings, and connection-pool metrics for free.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
The metrics endpoint
Expose metrics and Actuator lists every registered meter.
management:
endpoints:
web:
exposure:
include: health, metrics, prometheus
curl http://localhost:8080/actuator/metrics
Output (truncated):
{
"names": [
"jvm.memory.used", "jvm.gc.pause", "system.cpu.usage",
"http.server.requests", "hikaricp.connections.active",
"process.uptime", "logback.events"
]
}
Drill into one meter by name, optionally filtering by tag:
curl 'http://localhost:8080/actuator/metrics/http.server.requests?tag=status:200'
Output:
{
"name": "http.server.requests",
"measurements": [
{ "statistic": "COUNT", "value": 1842 },
{ "statistic": "TOTAL_TIME", "value": 37.21 },
{ "statistic": "MAX", "value": 0.418 }
],
"availableTags": [
{ "tag": "uri", "values": ["/orders/{id}", "/orders"] },
{ "tag": "method", "values": ["GET", "POST"] }
]
}
Meter types
A meter is a named measurement. The three you use most:
| Meter | Use it for | Direction |
|---|---|---|
Counter | Things that only increase: requests, errors, messages processed | monotonic |
Gauge | A value that goes up and down: queue depth, active sessions, cache size | sampled |
Timer | Duration and rate of events: method latency, external call time | distribution |
There are also DistributionSummary (sizes, e.g. payload bytes) and LongTaskTimer (in-flight long-running tasks).
Custom metrics with MeterRegistry
Inject the MeterRegistry and register meters. Always attach tags (dimensions) instead of baking values into the metric name.
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Counter ordersPlaced;
private final Timer checkoutTimer;
public OrderService(MeterRegistry registry) {
this.ordersPlaced = Counter.builder("orders.placed")
.description("Total orders successfully placed")
.tag("channel", "web")
.register(registry);
this.checkoutTimer = registry.timer("orders.checkout.duration");
}
public Order checkout(Cart cart) {
return checkoutTimer.record(() -> {
Order order = process(cart);
ordersPlaced.increment();
return order;
});
}
}
A Gauge tracks live state by holding a reference to the object being measured — register it once, not on every change:
import io.micrometer.core.instrument.Gauge;
Gauge.builder("orders.queue.size", queue, BlockingQueue::size)
.description("Pending orders waiting to ship")
.register(registry);
Warning: Never put unbounded values (user IDs, order IDs, raw URLs) in tags. Each unique tag combination is a separate time series — high cardinality explodes memory in both your app and the monitoring backend.
The @Timed annotation
For declarative timing, add spring-boot-starter-aop and annotate a method. A TimedAspect bean activates @Timed.
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsConfig {
@Bean
TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
import io.micrometer.core.annotation.Timed;
@Timed(value = "report.generation", percentiles = {0.95, 0.99}, histogram = true)
public Report generateMonthlyReport(int month) {
// ... expensive work timed automatically
}
Note:
http.server.requestsis already aTimerfor every controller method, so you rarely need@Timedon web endpoints. Reach for it on services, schedulers, and batch jobs.
Exporting to Prometheus
Prometheus is the most common backend. Add the registry and a scrape endpoint appears.
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
Expose the prometheus endpoint (already included above). Now /actuator/prometheus serves the text exposition format Prometheus scrapes:
curl http://localhost:8080/actuator/prometheus
Output (excerpt):
# HELP orders_placed_total Total orders successfully placed
# TYPE orders_placed_total counter
orders_placed_total{channel="web"} 1842.0
# HELP orders_checkout_duration_seconds
# TYPE orders_checkout_duration_seconds summary
orders_checkout_duration_seconds_count 1842.0
orders_checkout_duration_seconds_sum 37.2104
Point a Prometheus scrape config at the endpoint:
scrape_configs:
- job_name: order-service
metrics_path: /actuator/prometheus
static_configs:
- targets: ['order-service:8080']
Add common tags so every metric carries the service identity, which keeps dashboards clean across many instances:
management:
metrics:
tags:
application: order-service
region: eu-west-1
endpoint:
metrics:
access: read-only
Best Practices
- Use
Counterfor totals,Gaugefor live values,Timerfor durations — don’t mix them up. - Keep tag cardinality low; never tag by user/order/request ID.
- Add
management.metrics.tags.applicationso multi-instance dashboards are filterable. - Let
http.server.requestscover endpoint latency; reserve@Timedfor non-web work. - Export with
micrometer-registry-prometheusand scrape/actuator/prometheus.