Contents

<!-- 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.

MetricDescription
http.server.requestsHTTP request count, latency, status by URI/method
jvm.memory.usedJVM heap and non-heap memory usage
jvm.gc.pauseGarbage collection pause time and count
process.cpu.usageProcess CPU utilisation (0.0–1.0)
hikaricp.connections.activeActive/idle/pending DB connection pool stats
spring.data.repository.invocationsSpring Data repository method call counts
cache.gets / cache.putsCache 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.