Contents
- Dependencies & Setup
- Built-in Metrics
- Custom Metrics — Counter, Timer, Gauge
- @Timed & @Counted Annotations
- Prometheus Endpoint & Scraping
- Distributed Tracing with Micrometer Tracing
- @Observed — Automatic Spans
- Baggage & Contextual Logging
- Grafana Dashboard Setup
<!-- Actuator — exposes metrics and health endpoints -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer Prometheus registry — exposes /actuator/prometheus -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Micrometer Tracing bridge for Brave (Zipkin) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
metrics:
tags:
application: ${spring.application.name} # add app name tag to every metric
tracing:
sampling:
probability: 1.0 # sample 100% in dev; use 0.1 in production
spring:
application:
name: order-service
# Zipkin endpoint
management:
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
Spring Boot auto-configures dozens of metrics out of the box — no code needed. Browse them at /actuator/metrics.
| Metric | Description |
| http.server.requests | HTTP request count, latency, status by URI/method |
| jvm.memory.used | JVM heap and non-heap memory usage |
| jvm.gc.pause | Garbage collection pause time and count |
| process.cpu.usage | Process CPU utilisation (0.0–1.0) |
| hikaricp.connections.active | Active/idle/pending DB connection pool stats |
| spring.data.repository.invocations | Spring Data repository method call counts |
| cache.gets / cache.puts | Cache hit/miss rates (if Spring Cache is used) |
# Query a specific metric with tags via curl
curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=status:200&tag=uri:/api/orders"
# Example response:
# { "name": "http.server.requests",
# "measurements": [
# { "statistic": "COUNT", "value": 1542 },
# { "statistic": "TOTAL_TIME", "value": 38.4 },
# { "statistic": "MAX", "value": 0.12 } ] }
Use the MeterRegistry to register custom business metrics. Three instruments cover most use cases: Counter (monotonically increasing count), Timer (latency + count), and Gauge (current value that can go up and down).
import io.micrometer.core.instrument.*;
@Service
public class OrderService {
private final MeterRegistry registry;
private final Counter ordersCreated;
private final Counter ordersFailed;
private final Timer orderProcessingTime;
public OrderService(MeterRegistry registry) {
this.registry = registry;
// Counter — increment on each event
this.ordersCreated = Counter.builder("orders.created")
.description("Number of orders successfully created")
.tag("region", "us-east")
.register(registry);
this.ordersFailed = Counter.builder("orders.failed")
.description("Number of orders that failed to create")
.register(registry);
// Timer — records latency and count
this.orderProcessingTime = Timer.builder("order.processing.duration")
.description("Time taken to process an order")
.publishPercentiles(0.5, 0.95, 0.99) // p50, p95, p99 latency
.publishPercentileHistogram()
.register(registry);
}
public Order createOrder(CreateOrderRequest req) {
return orderProcessingTime.record(() -> {
try {
Order order = processOrder(req);
ordersCreated.increment();
return order;
} catch (Exception e) {
ordersFailed.increment(Tags.of("reason", e.getClass().getSimpleName()));
throw e;
}
});
}
// Gauge — track current queue depth
@PostConstruct
public void registerQueueGauge() {
Gauge.builder("order.queue.size", orderQueue, Queue::size)
.description("Current number of orders in the processing queue")
.register(registry);
}
}
For simpler cases, annotate methods directly instead of injecting MeterRegistry. Requires TimedAspect and CountedAspect beans.
import io.micrometer.core.annotation.*;
// Register the AOP aspects once in a @Configuration class
@Bean public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
@Bean public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
// Use on service methods
@Service
public class PaymentService {
@Timed(value = "payment.charge.duration",
description = "Time to execute a payment charge",
percentiles = {0.5, 0.95, 0.99})
public PaymentResult charge(PaymentRequest request) {
// ... call payment gateway
return result;
}
@Counted(value = "payment.refund.count",
description = "Number of refunds issued")
public void refund(String paymentId) {
// ... issue refund
}
}
The Actuator Prometheus endpoint exposes all Micrometer metrics in the Prometheus text format at /actuator/prometheus. Configure Prometheus to scrape it on an interval.
# prometheus.yml — Prometheus scrape configuration
scrape_configs:
- job_name: 'order-service'
scrape_interval: 15s
metrics_path: /actuator/prometheus
static_configs:
- targets: ['order-service:8080']
# If behind Spring Security, add basic auth:
# basic_auth:
# username: prometheus
# password: secret
# Sample output at /actuator/prometheus
# HELP orders_created_total Number of orders successfully created
# TYPE orders_created_total counter
orders_created_total{application="order-service",region="us-east"} 1542.0
# HELP order_processing_duration_seconds Time taken to process an order
# TYPE order_processing_duration_seconds summary
order_processing_duration_seconds{application="order-service",quantile="0.5"} 0.045
order_processing_duration_seconds{application="order-service",quantile="0.95"} 0.112
order_processing_duration_seconds{application="order-service",quantile="0.99"} 0.287
order_processing_duration_seconds_count 1542.0
order_processing_duration_seconds_sum 72.3
Distributed tracing links all the work done across services for a single request into a trace. Each trace is a tree of spans — one per service call. Micrometer Tracing automatically creates spans for HTTP requests, WebClient calls, and Kafka messages, and propagates the trace context via HTTP headers (traceparent / X-B3-*).
// Tracing is automatic for HTTP — just add the dependency and configure Zipkin endpoint
// Trace/span IDs are automatically added to log lines via MDC:
// 2025-04-18 12:00:01 [order-service] [traceId=abc123,spanId=def456] INFO ...
// Manual span creation for custom operations
import io.micrometer.tracing.*;
@Service
public class InventoryClient {
private final Tracer tracer;
private final WebClient webClient;
public InventoryClient(Tracer tracer, WebClient.Builder builder) {
this.tracer = tracer;
this.webClient = builder.baseUrl("http://inventory-service").build();
}
public InventoryStatus checkStock(Long productId) {
// Create a child span for this outbound call
Span span = tracer.nextSpan()
.name("inventory.check-stock")
.tag("product.id", String.valueOf(productId))
.start();
try (Tracer.SpanInScope scope = tracer.withSpan(span)) {
return webClient.get()
.uri("/stock/{id}", productId)
.retrieve()
.bodyToMono(InventoryStatus.class)
.block();
} catch (Exception e) {
span.error(e);
throw e;
} finally {
span.end(); // always end the span
}
}
}
Spring Boot 3 introduced the @Observed annotation (backed by Micrometer's Observation API) that automatically creates a span and a timer metric for a method with a single annotation. Requires ObservedAspect.
import io.micrometer.observation.annotation.Observed;
// Register the AOP aspect
@Bean
public ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
return new ObservedAspect(observationRegistry);
}
@Service
public class ProductService {
// Creates both a trace span AND a timer metric automatically
@Observed(name = "product.search",
contextualName = "searching-products",
lowCardinalityKeyValues = {"feature", "search"})
public List<Product> search(ProductSearchRequest req) {
return productRepository.findAll(buildSpec(req));
}
// Fine-grained control using ObservationRegistry directly
private final ObservationRegistry observationRegistry;
public Order processOrder(CreateOrderRequest req) {
return Observation.createNotStarted("order.process", observationRegistry)
.lowCardinalityKeyValue("payment.method", req.paymentMethod())
.highCardinalityKeyValue("customer.id", req.customerId())
.observe(() -> {
// all code here is automatically timed and traced
return orderRepository.save(new Order(req));
});
}
}
Baggage lets you propagate key-value pairs alongside the trace context across service boundaries. Common uses: tenant ID, user ID, correlation ID. They appear automatically in MDC, so they show up in every log line for that request.
import io.micrometer.tracing.*;
@Component
public class TenantTracingFilter implements jakarta.servlet.Filter {
private final Tracer tracer;
public TenantTracingFilter(Tracer tracer) { this.tracer = tracer; }
@Override
public void doFilter(jakarta.servlet.ServletRequest req,
jakarta.servlet.ServletResponse res,
jakarta.servlet.FilterChain chain)
throws java.io.IOException, jakarta.servlet.ServletException {
String tenantId = ((jakarta.servlet.http.HttpServletRequest) req)
.getHeader("X-Tenant-ID");
if (tenantId != null) {
// Add tenantId to baggage — propagated to all downstream services
BaggageField.create("tenant.id").updateValue(tenantId);
}
chain.doFilter(req, res);
}
}
// application.yml — make baggage visible in logs via MDC
management:
tracing:
baggage:
remote-fields:
- tenant.id
correlation:
fields:
- tenant.id # adds tenant.id to MDC → appears in every log line
With Prometheus scraping and Grafana connected, you can import ready-made Spring Boot dashboards or build custom ones using the Prometheus metrics.
# docker-compose.yml — local observability stack
services:
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports: ["9090:9090"]
grafana:
image: grafana/grafana:latest
ports: ["3000:3000"]
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-data:/var/lib/grafana
zipkin:
image: openzipkin/zipkin:latest
ports: ["9411:9411"]
volumes:
grafana-data:
# Useful PromQL queries for Grafana panels
# Request rate per second (5m window)
rate(http_server_requests_seconds_count{application="order-service"}[5m])
# p99 latency
histogram_quantile(0.99,
rate(http_server_requests_seconds_bucket{application="order-service"}[5m]))
# Error rate percentage
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m])) * 100
# JVM heap usage
jvm_memory_used_bytes{area="heap", application="order-service"}
# HikariCP active connections
hikaricp_connections_active{application="order-service"}
Grafana dashboard ID 19004 (Spring Boot 3 Statistics) is a community dashboard that works out-of-the-box with Micrometer Prometheus metrics. Import it via Grafana → Dashboards → Import → enter the ID.