1. What is Micrometer?
- Vendor-neutral metrics facade for JVM-based apps (like SLF4J for metrics)
- Supports 15+ monitoring systems: Prometheus, InfluxDB, Datadog, New Relic, Cloudwatch, etc.
- Built into Spring Boot Actuator since Spring Boot 2.x
- Core concepts: MeterRegistry, Meter, Tags (dimensions)
Maven Dependency
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>application.yml Configuration
# application.yml
management:
endpoints:
web:
exposure:
include: health, info, prometheus, metrics
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
environment: ${SPRING_PROFILES_ACTIVE:local}2. Meter Types — When to Use What
| Type | Purpose | Use Case | Example |
|---|---|---|---|
| Counter | Monotonically increasing count | Request count, errors, events | orders.placed.total |
| Gauge | Current value (can go up/down) | Queue size, active threads, cache size | thread.pool.active |
| Timer | Duration + count of events | Request latency, method execution time | http.server.requests |
| DistributionSummary | Distribution of values (no time) | Payload sizes, batch sizes | http.response.size |
| LongTaskTimer | Duration of long-running tasks | Batch jobs, file processing | report.generation |
| FunctionCounter | Counter from external source | JMX values, external counters | cache.evictions |
| FunctionTimer | Timer from external source | External timing data | external.api.time |
| TimeGauge | Gauge reporting time values | Uptime, last run timestamp | process.uptime |
Decision Tree
"How many times did X happen?"
→ Counter
"What is the current value of X?"
→ Gauge
"How long did X take?"
→ Timer
"What is the distribution of X values?"
→ DistributionSummary
"How long has X been running (still in progress)?"
→ LongTaskTimer
3. Counter
// Simple counter
Counter counter = Counter.builder("orders.placed")
.description("Total orders placed")
.tag("channel", "web")
.register(meterRegistry);
counter.increment();
counter.increment(5.0);
// With @Counted annotation (Spring AOP)
@Counted(value = "orders.placed", description = "Total orders placed")
public Order placeOrder(OrderRequest request) { ... }
// Functional counter (from existing value)
FunctionCounter.builder("cache.evictions", cache, Cache::evictionCount)
.register(meterRegistry);Required AOP Configuration for @Counted
@Counted will NOT work without registering the CountedAspect bean. Spring Boot does not auto-configure it.
// Required: Register CountedAspect bean
@Configuration
@EnableAspectJAutoProxy
public class MetricsConfig {
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
// Maven dependency (if not already via spring-boot-starter-aop)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>4. Gauge
// Simple gauge
Gauge.builder("queue.size", queue, Queue::size)
.description("Current queue depth")
.tag("queue", "orders")
.register(meterRegistry);
// AtomicInteger gauge
AtomicInteger activeUsers = new AtomicInteger(0);
Gauge.builder("users.active", activeUsers, AtomicInteger::get)
.register(meterRegistry);
// Collection size gauge
Gauge.builder("cache.entries", cache, Map::size)
.register(meterRegistry);
// Multi-gauge (for dynamic values)
MultiGauge multiGauge = MultiGauge.builder("pool.connections")
.register(meterRegistry);
multiGauge.register(
pools.stream()
.map(p -> Row.of(Tags.of("pool", p.getName()), p.getActiveCount()))
.collect(Collectors.toList())
);⚠️ Warning
Never create gauges in a loop or per-request! Gauges hold a weak reference to the observed object. The object must be held elsewhere (field, bean, etc.)
5. Timer
// Manual timer
Timer timer = Timer.builder("http.requests")
.description("HTTP request duration")
.tag("method", "GET")
.tag("uri", "/api/orders")
.publishPercentiles(0.5, 0.95, 0.99) // client-side percentiles
.publishPercentileHistogram() // for Prometheus histogram_quantile
.sla(Duration.ofMillis(100), Duration.ofMillis(500), Duration.ofSeconds(1)) // SLA buckets
.register(meterRegistry);
// Record execution
timer.record(() -> doSomething());
timer.record(Duration.ofMillis(250));
// With Sample (for async/split timing)
Timer.Sample sample = Timer.start(meterRegistry);
// ... async work ...
sample.stop(timer);
// With @Timed annotation
@Timed(value = "orders.process", percentiles = {0.5, 0.95, 0.99},
histogram = true, description = "Order processing time")
public void processOrder(Order order) { ... }Required AOP Configuration for @Timed
@Timed will NOT work without registering the TimedAspect bean. Spring Boot does not auto-configure it.
// Required: Register TimedAspect bean
@Configuration
@EnableAspectJAutoProxy
public class MetricsConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
// Both aspects in one config class — register once, use everywhere
// Imports:
// import io.micrometer.core.aop.TimedAspect;
// import io.micrometer.core.aop.CountedAspect;
// import io.micrometer.core.instrument.MeterRegistry;Spring Boot Auto-Timed HTTP Requests
# application.yml
management:
metrics:
web:
server:
request:
autotime:
enabled: true
percentiles: 0.5, 0.95, 0.99
percentiles-histogram: true6. DistributionSummary
// Track payload sizes
DistributionSummary summary = DistributionSummary.builder("http.response.size")
.description("Response payload size in bytes")
.baseUnit("bytes")
.publishPercentiles(0.5, 0.95)
.publishPercentileHistogram()
.minimumExpectedValue(100.0)
.maximumExpectedValue(10_000_000.0)
.register(meterRegistry);
summary.record(responseBody.length());
// Track batch sizes
DistributionSummary.builder("batch.size")
.tag("type", "import")
.register(meterRegistry)
.record(batch.size());8. Spring Boot Auto-Configured Metrics
Built-in metrics you get for free:
# JVM
jvm.memory.used, jvm.memory.max, jvm.memory.committed
jvm.gc.pause, jvm.gc.memory.allocated
jvm.threads.live, jvm.threads.daemon, jvm.threads.peak
jvm.buffer.memory.used, jvm.buffer.count
# HTTP Server (Spring MVC / WebFlux)
http.server.requests (timer: method, uri, status, outcome)
# HTTP Client (RestTemplate / WebClient)
http.client.requests (timer: method, uri, status)
# DataSource / Connection Pool
hikaricp.connections.active, hikaricp.connections.idle
hikaricp.connections.max, hikaricp.connections.usage
# Cache (if Spring Cache enabled)
cache.gets, cache.puts, cache.evictions, cache.size
# Spring Integration
spring.integration.send (channels)
spring.integration.receive (channels)
# Reactor (WebFlux / Project Reactor)
reactor.scheduler.tasks.active
reactor.flow.duration
# System
process.cpu.usage, system.cpu.usage
process.uptime, process.start.time
disk.free, disk.total
# Logback
logback.events (tag: level)9. Custom Metrics Patterns
Business Metrics
@Component
public class OrderMetrics {
private final Counter ordersPlaced;
private final Counter ordersFailed;
private final Timer orderProcessingTime;
private final DistributionSummary orderValue;
private final AtomicInteger pendingOrders;
public OrderMetrics(MeterRegistry registry) {
ordersPlaced = Counter.builder("orders.placed.total")
.description("Total successful orders").register(registry);
ordersFailed = Counter.builder("orders.failed.total")
.description("Total failed orders").register(registry);
orderProcessingTime = Timer.builder("orders.processing.duration")
.publishPercentileHistogram().register(registry);
orderValue = DistributionSummary.builder("orders.value")
.baseUnit("CHF").publishPercentiles(0.5, 0.95).register(registry);
Gauge.builder("orders.pending", pendingOrders = new AtomicInteger(),
AtomicInteger::get).register(registry);
}
public void recordOrder(Order order, Duration processingTime) {
ordersPlaced.increment();
orderProcessingTime.record(processingTime);
orderValue.record(order.getTotal().doubleValue());
}
}Circuit Breaker Metrics (Resilience4j)
// Auto-configured with micrometer-registry + resilience4j
resilience4j.circuitbreaker.state // gauge: 0=closed, 1=open, 2=half-open
resilience4j.circuitbreaker.calls // timer: kind=successful|failed|ignored
resilience4j.circuitbreaker.failure.rate // gauge
resilience4j.retry.calls // counter: kind=successful|failedCaching Metrics
@Bean
public CacheMetricsRegistrar cacheMetrics(MeterRegistry registry) {
// Automatically registers cache.gets, cache.puts, cache.evictions
}10. Prometheus Query Examples (PromQL)
# Request rate (per second, 5m window)
rate(http_server_requests_seconds_count{application="my-app"}[5m])
# Error rate percentage
rate(http_server_requests_seconds_count{status=~"5.."}[5m])
/ rate(http_server_requests_seconds_count[5m]) * 100
# 95th percentile response time (from histogram)
histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))
# Average response time
rate(http_server_requests_seconds_sum[5m])
/ rate(http_server_requests_seconds_count[5m])
# JVM heap usage percentage
jvm_memory_used_bytes{area="heap"}
/ jvm_memory_max_bytes{area="heap"} * 100
# Active HikariCP connections
hikaricp_connections_active{pool="HikariPool-1"}
# Orders per minute
rate(orders_placed_total[1m]) * 60
# Pending orders across all instances
sum(orders_pending)
# Top 5 slowest endpoints
topk(5, rate(http_server_requests_seconds_sum[5m])
/ rate(http_server_requests_seconds_count[5m]))11. Reactor / WebFlux Specific Metrics
// Enable Reactor metrics
@Bean
public ReactorMetricsAutoConfiguration reactorMetrics() { ... }
// Or manually:
Hooks.onEachOperator(Metrics.METRIC_KEY,
OperatorTracingHook.newBuilder().build());
// WebClient metrics
WebClient.builder()
.filter(ExchangeFilterFunctions.metricsFilter(meterRegistry))
.build();
// Custom Reactor metrics
Flux.range(1, 100)
.name("my.flux.pipeline")
.metrics()
.subscribe();
// Key Reactor metrics:
// reactor.flow.duration — time spent in operator
// reactor.scheduler.tasks.active — active scheduled tasks
// reactor.onNext.delay — subscriber signal delays12. Spring Integration Metrics
// Auto-configured when micrometer is on classpath
// Metrics for channels and handlers:
spring.integration.send // timer: channel name, result
spring.integration.receive // timer: channel name
spring.integration.handler // timer: handler name
// Custom metrics in IntegrationFlow
@Bean
public IntegrationFlow myFlow() {
return IntegrationFlows.from("inputChannel")
.handle((payload, headers) -> {
meterRegistry.counter("messages.processed",
"type", headers.get("type").toString()
).increment();
return payload;
})
.channel("outputChannel")
.get();
}
// Wire-tap for metrics without modifying main flow
.wireTap(sf -> sf.handle(msg ->
meterRegistry.counter("messages.observed").increment()
))