Spring Boot Micrometer en Prometheus: Actuator-metrieken in productie

Integratie van Spring Boot Micrometer en Prometheus op Spring Boot 4: Actuator-toegangsmodel, scrapeconfiguratie, HTTP-metrieken, health-probes en alertregels.

Spring Boot Actuator monitoring met Micrometer en Prometheus

Spring Boot Actuator stelt endpoints beschikbaar voor health, metrieken en diagnostiek, en Micrometer is de metriek-facade daarachter. Met de dependency micrometer-registry-prometheus ontstaat een endpoint /actuator/prometheus dat Prometheus met een vast interval scrapet. De voorbeelden hieronder gaan uit van Spring Boot 4.1 (huidige GA-release, met 4.2 als release candidate), Micrometer 1.16 en de Prometheus Java client 1.x.

Het korte antwoord

De integratie van Spring Boot Micrometer met Prometheus kost drie stappen: spring-boot-starter-actuator en micrometer-registry-prometheus toevoegen, prometheus opnemen in management.endpoints.web.exposure.include en vervolgens een Prometheus-scrape_config naar /actuator/prometheus laten wijzen. JVM-, HTTP-, HikariCP- en systeemmetrieken worden gepubliceerd zonder extra code.

Spring Boot Micrometer opzetten: Actuator en de Prometheus-registry

Vereiste Maven-Dependencies

De Actuator-starter activeert de endpoints en haalt Micrometer binnen. De Prometheus-registry zet elke meter om naar het Prometheus-expositieformaat. De AspectJ-starter is alleen nodig voor de annotaties @Timed en @Counted.

xml
<!-- pom.xml -->
<!-- Actuator + Micrometer + Prometheus (Spring Boot 4.x) -->
<dependencies>
    <!-- Spring Boot Actuator: monitoring endpoints, includes Micrometer -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Micrometer Registry Prometheus (Prometheus Java client 1.x) -->
    <!-- Exposes metrics in Prometheus format at /actuator/prometheus -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

    <!-- AspectJ for @Timed and @Counted -->
    <!-- Spring Boot 4 renamed spring-boot-starter-aop to spring-boot-starter-aspectj -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aspectj</artifactId>
    </dependency>
</dependencies>

Oudere tutorials bevatten twee versievalkuilen. Sinds Micrometer 1.13 (Spring Boot 3.3) is micrometer-registry-prometheus gebouwd op de Prometheus Java client 1.x; de oude 0.x-code zit in de deprecated module micrometer-registry-prometheus-simpleclient, die de Micrometer-documentatie over Prometheus alleen als tijdelijke oplossing presenteert. En onder Spring Boot 4 wordt een pom.xml die nog spring-boot-starter-aop declareert niet meer opgelost: de migratiegids voor Spring Boot 4.0 hernoemt die naar spring-boot-starter-aspectj.

Configuratie van Actuator-Endpoints

Standaard is alleen het endpoint health via HTTP toegankelijk. Het endpoint /actuator/prometheus bestaat zodra de registry op het classpath staat, maar geeft 404 terug tot het aan de exposure-lijst is toegevoegd.

Spring Boot 3.4 heeft de aan/uit-schakelaars (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) vervangen door een toegangsmodel, en de oude properties zijn deprecated. Het opt-in-patroon zet de standaardtoegang op none en kent per endpoint read-only toe:

yaml
# application.yml
# Actuator configuration for production (Spring Boot 3.4+ access model)
management:
  endpoints:
    # Every endpoint is inaccessible unless granted below
    access:
      default: none
    web:
      exposure:
        # Endpoints exposed over HTTP
        # health, info, prometheus are the minimum for monitoring
        include: health,info,prometheus,metrics,env,loggers
      base-path: /actuator
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      show-components: when-authorized
    info:
      access: read-only
    prometheus:
      access: read-only
    metrics:
      access: read-only
    env:
      access: read-only
      # Mask sensitive values for anonymous callers
      show-values: when-authorized
    loggers:
      # unrestricted allows POST to change a log level at runtime
      access: unrestricted

Exposure en toegang zijn twee aparte instellingen. exposure.include bepaalt of een endpoint via HTTP wordt gemapt; access bepaalt wat het endpoint toestaat zodra het gemapt is. Een propertiesbestand dat nog enabled: true gebruikt, blijft werken met een deprecation-waarschuwing, en dat is het moment om te migreren.

De optie show-details: when-authorized toont health-details enkel aan geauthenticeerde gebruikers met de juiste rol. De endpoints zelf worden beveiligd met een eigen SecurityFilterChain:

ActuatorSecurityConfig.javajava
// Securing Actuator endpoints (Spring Boot 4, Spring Security 7)
package com.example.monitoring.config;

import static org.springframework.security.web.access.IpAddressAuthorizationManager.hasIpAddress;

import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
public class ActuatorSecurityConfig {

    @Bean
    SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                // Health and info public for load balancers
                .requestMatchers(EndpointRequest.to("health", "info")).permitAll()
                // Prometheus accessible from the internal network only
                .requestMatchers(EndpointRequest.to("prometheus")).access(hasIpAddress("10.0.0.0/8"))
                // Other endpoints restricted to admins
                .anyRequest().hasRole("ACTUATOR_ADMIN")
            )
            .httpBasic(basic -> {})
            .build();
    }
}

EndpointRequest is in Spring Boot 4 verhuisd naar org.springframework.boot.security.autoconfigure.actuate.web.servlet, zoals de referentie van de Actuator-endpoints laat zien. De Java-DSL AuthorizedUrl van authorizeHttpRequests heeft geen methode hasIpAddress: de IP-regel loopt via IpAddressAuthorizationManager.hasIpAddress, beschikbaar sinds Spring Security 6.3.

Custom Metrieken met Micrometer

Applicatie-Counters, -Gauges en -Timers

Micrometer biedt verschillende metriektypes die passen bij uiteenlopende use cases. Counters meten cumulatieve gebeurtenissen, gauges meten momentwaarden en timers meten de duur van een operatie.

OrderMetricsService.javajava
// Custom business metrics service
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

@Service
public class OrderMetricsService {

    // Counter for orders created
    private final Counter ordersCreatedCounter;
    // Timer to measure processing duration
    private final Timer orderProcessingTimer;
    // Atomic value for pending orders gauge
    private final AtomicInteger pendingOrdersCount = new AtomicInteger(0);

    public OrderMetricsService(MeterRegistry registry) {
        // Exported as orders_created_total: the _total suffix is added by the registry
        this.ordersCreatedCounter = Counter.builder("orders.created")
            .description("Total number of orders created")
            .tag("channel", "web")
            .register(registry);

        // Timer with histogram buckets: percentiles computed server-side in PromQL
        this.orderProcessingTimer = Timer.builder("orders.processing.duration")
            .description("Order processing duration")
            .publishPercentileHistogram()
            .register(registry);

        // Gauge linked to atomic value
        // Read on each scrape
        Gauge.builder("orders.pending.count", pendingOrdersCount, AtomicInteger::get)
            .description("Number of orders pending processing")
            .register(registry);
    }

    public void recordOrderCreated() {
        ordersCreatedCounter.increment();
        pendingOrdersCount.incrementAndGet();
    }

    public void recordOrderProcessed(Runnable processingLogic) {
        // Measures execution duration
        orderProcessingTimer.record(processingLogic);
        pendingOrdersCount.decrementAndGet();
    }

    public <T> T recordOrderProcessedWithResult(Supplier<T> processingLogic) {
        return orderProcessingTimer.record(processingLogic);
    }
}

Met de Prometheus-registry tellen twee details in de naamgeving. Meternamen in Micrometer gebruiken punten en worden omgezet naar snake case (orders.processing.duration wordt orders_processing_duration_seconds). Counters krijgen automatisch het achtervoegsel _total, dus een counter orders.created.total noemen voegt alleen ruis toe.

publishPercentiles(0.5, 0.95, 0.99) berekent percentielen binnen elke JVM, en die waarden zijn niet over instanties heen te aggregeren. publishPercentileHistogram() publiceert buckets die histogram_quantile() over de hele vloot aggregeert, en dat is de optie om te kiezen achter Prometheus.

Annotaties @Timed en @Counted

Om boilerplate-code te vermijden, biedt Micrometer annotaties die methoden via AspectJ instrumenteren.

PaymentService.javajava
// Automatic instrumentation with annotations
package com.example.monitoring.service;

import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final PaymentValidator paymentValidator;

    public PaymentService(PaymentValidator paymentValidator) {
        this.paymentValidator = paymentValidator;
    }

    // @Timed creates a Timer
    // Measures each call and publishes count, sum, max and histogram buckets
    @Timed(
        value = "payment.process.duration",
        description = "Payment processing duration",
        histogram = true
    )
    public PaymentResult processPayment(PaymentRequest request) {
        // Call through another bean: the proxy intercepts it
        paymentValidator.validate(request);
        return executePayment(request);
    }

    // @Counted increments a counter on each call
    // Useful for discrete events
    @Counted(
        value = "payment.refunds",
        description = "Total number of refunds"
    )
    public void refundPayment(String transactionId) {
        // Refund logic
    }
}
yaml
# application.yml
# Enables TimedAspect, CountedAspect and the other Micrometer annotation aspects
management:
  observations:
    annotations:
      enabled: true

Sinds Spring Boot 3.2 hoeven de beans TimedAspect en CountedAspect niet meer met de hand te worden gedeclareerd: management.observations.annotations.enabled=true instellen met spring-boot-starter-aspectj op het classpath configureert ze automatisch. De property staat standaard op false, wat de meeste meldingen van het type "mijn @Timed doet niets" verklaart.

AOP-Beperking

De annotaties @Timed en @Counted werken alleen op public methoden van Spring-beans die via de proxy worden aangeroepen. Een aanroep van de ene methode naar de andere in dezelfde klasse omzeilt de proxy en wordt niet geïnstrumenteerd, en een annotatie op een private methode wordt genegeerd. Een Spring MVC-controller annoteren die al geïnstrumenteerd is, levert dubbele observations op.

Klaar om je Spring Boot gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Custom Health-Endpoints

Business Health Indicators

Health Indicators controleren de toestand van externe afhankelijkheden en kritieke business-componenten. Spring Boot levert standaard indicators voor databases, Redis en andere veelvoorkomende services. In Spring Boot 4 staat de health-API in een eigen module, spring-boot-health, en zijn Health en HealthIndicator verhuisd van org.springframework.boot.actuate.health naar org.springframework.boot.health.contributor.

PaymentGatewayHealthIndicator.javajava
// Health indicator for payment gateway (Spring Boot 4 packages)
package com.example.monitoring.health;

import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.time.Instant;

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final RestClient restClient;
    private final String gatewayHealthUrl;

    public PaymentGatewayHealthIndicator(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
        this.gatewayHealthUrl = "https://api.payment-gateway.com/health";
    }

    @Override
    public Health health() {
        Instant start = Instant.now();

        try {
            // Call gateway health endpoint
            var response = restClient.get()
                .uri(gatewayHealthUrl)
                .retrieve()
                .toBodilessEntity();

            Duration responseTime = Duration.between(start, Instant.now());

            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("responseTime", responseTime.toMillis() + "ms")
                    .withDetail("statusCode", response.getStatusCode().value())
                    .build();
            } else {
                return Health.down()
                    .withDetail("statusCode", response.getStatusCode().value())
                    .withDetail("reason", "Unexpected status code")
                    .build();
            }
        } catch (Exception e) {
            Duration responseTime = Duration.between(start, Instant.now());

            return Health.down()
                .withDetail("error", e.getClass().getSimpleName())
                .withDetail("message", e.getMessage())
                .withDetail("responseTime", responseTime.toMillis() + "ms")
                .build();
        }
    }
}

Deze indicator verschijnt in /actuator/health onder de naam paymentGateway: de beannaam zonder het achtervoegsel HealthIndicator.

Health-Groepen voor Kubernetes

Spring Boot 4 activeert de liveness- en readiness-probes standaard, op elk platform en niet alleen wanneer Kubernetes wordt gedetecteerd. De groepen /actuator/health/liveness en /actuator/health/readiness bestaan zonder enige configuratie, en beans voor LivenessStateHealthIndicator of ReadinessStateHealthIndicator met de hand declareren is overbodig. Configuratie is alleen nodig om componenten aan een groep toe te voegen of om eigen groepen te definiëren:

yaml
# application.yml
# Health groups configuration for Kubernetes
management:
  endpoint:
    health:
      probes:
        # true by default in Spring Boot 4
        enabled: true
        # Also serve the groups at /livez and /readyz on the main server port
        add-additional-paths: true
      group:
        # Readiness: can the application receive traffic?
        readiness:
          include: readinessState,db
        # Custom group for critical dependencies
        critical:
          include: db,paymentGateway
          show-details: when-authorized

De liveness-groep hoort beperkt te blijven tot livenessState. Een database of externe gateway aan liveness toevoegen laat Kubernetes elke pod herstarten wanneer die afhankelijkheid wegvalt, waardoor een gedeeltelijke storing een volledige storing wordt. Externe afhankelijkheden horen in readiness, en alleen wanneer de applicatie zonder hen geen enkel request kan afhandelen.

De Kubernetes-probes verwijzen vervolgens naar dedicated endpoints:

yaml
# kubernetes-deployment.yml
# Kubernetes probes configuration
spec:
  containers:
    - name: order-service
      livenessProbe:
        httpGet:
          path: /livez
          port: 8080
        initialDelaySeconds: 30
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /readyz
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 3

Met add-additional-paths: true blijven de probes op de applicatiepoort, ook wanneer Actuator op een aparte managementpoort draait. Een kapotte hoofdserver wordt zo door de probe opgemerkt.

Spring Boot Prometheus-integratie en Grafana

Prometheus-Scrapingconfiguratie

Prometheus verzamelt metrieken door periodiek het endpoint /actuator/prometheus te bevragen. De scrapeconfiguratie definieert de targets en het metriekpad, omdat Prometheus standaard /metrics gebruikt.

yaml
# prometheus.yml
# Prometheus configuration for Spring Boot
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 10s
    static_configs:
      - targets:
          - 'order-service:9090'
          - 'payment-service:9090'
          - 'inventory-service:9090'
    # Relabeling to strip the port from the instance label
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
        regex: '([^:]+):\d+'
        replacement: '${1}'

  # Kubernetes service discovery
  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      # Only scrape pods with annotation
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)

De targets gebruiken poort 9090 omdat de productieconfiguratie verderop Actuator naar een aparte managementpoort verplaatst. Zonder management.server.port is het scrape-target de applicatiepoort.

Standaard gepubliceerde Spring Boot Actuator-metrieken

Actuator met Micrometer stelt JVM-, proces-, HikariCP- en HTTP-metrieken beschikbaar zonder code. Deze PromQL-queries dekken de metrieken die in productie als eerste worden bewaakt:

promql
# PromQL queries for JVM monitoring

# Heap memory usage, summed across heap pools
sum by (instance) (jvm_memory_used_bytes{area="heap"})

# Heap usage percentage
sum by (instance) (jvm_memory_used_bytes{area="heap"})
  / sum by (instance) (jvm_memory_max_bytes{area="heap"}) * 100

# Live threads
jvm_threads_live_threads

# Garbage collection: seconds spent paused per second
rate(jvm_gc_pause_seconds_sum[5m])

# GC pauses per minute
rate(jvm_gc_pause_seconds_count[1m]) * 60

# CPU used by the JVM process
process_cpu_usage

# Active database connections
hikaricp_connections_active

# Connection pool utilization
hikaricp_connections_active / hikaricp_connections_max * 100

jvm_memory_used_bytes{area="heap"} levert één reeks per geheugenpool (Eden, Survivor, Old Gen bij G1). Wie zonder sum by (instance) door het maximum deelt, krijgt verhoudingen per pool die er op een dashboard verkeerd uitzien.

Een MeterBinder-bean voegt gauges op applicatieniveau toe die de standaardmetrieken niet afdekken:

CustomJvmMetrics.javajava
// Additional JVM metrics
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.stereotype.Component;

import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;

@Component
public class CustomJvmMetrics implements MeterBinder {

    @Override
    public void bindTo(MeterRegistry registry) {
        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();

        // System load average
        Gauge.builder("system.load.average", osBean, OperatingSystemMXBean::getSystemLoadAverage)
            .description("System load average over 1 minute")
            .register(registry);

        // Available processors count
        Gauge.builder("system.cpu.count", osBean, OperatingSystemMXBean::getAvailableProcessors)
            .description("Number of available processors")
            .register(registry);

        // Application uptime
        Gauge.builder("application.uptime.seconds",
                ManagementFactory.getRuntimeMXBean(),
                bean -> bean.getUptime() / 1000.0)
            .description("Application uptime in seconds")
            .register(registry);
    }
}

system.cpu.count en process.uptime worden al gepubliceerd door de ingebouwde binders van Micrometer. Het voorbeeld toont het patroon, en een echte binder registreert alleen wat de standaardmetrieken missen.

Kant-en-Klare Grafana-Dashboards

Grafana biedt communitydashboards voor Spring Boot die werken met de standaard Actuator-metrieken. Een zelfgeschreven dashboard gebruikt de paneltypes timeseries en stat:

json
{
  "annotations": {
    "list": []
  },
  "panels": [
    {
      "title": "Request Rate",
      "type": "timeseries",
      "targets": [
        {
          "expr": "sum by (method, uri, status) (rate(http_server_requests_seconds_count{application=\"$application\"}[5m]))",
          "legendFormat": "{{method}} {{uri}} - {{status}}"
        }
      ]
    },
    {
      "title": "Response Time P99",
      "type": "timeseries",
      "targets": [
        {
          "expr": "histogram_quantile(0.99, sum by (le, method, uri) (rate(http_server_requests_seconds_bucket{application=\"$application\"}[5m])))",
          "legendFormat": "{{method}} {{uri}}"
        }
      ]
    },
    {
      "title": "Error Rate",
      "type": "stat",
      "targets": [
        {
          "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{application=\"$application\"}[5m])) * 100"
        }
      ]
    }
  ]
}

Veel dashboards die uit oudere tutorials zijn gekopieerd, gebruiken nog de paneltypes graph en singlestat. Singlestat is in Grafana 8 verwijderd, en Grafana 12 heeft de ondersteuning voor AngularJS volledig geschrapt, dus die panelen worden pas weergegeven na migratie naar timeseries en stat.

Grafana-Import

Een communitydashboard importeren: Grafana > Dashboards > New > Import, daarna ID 4701 (JVM Micrometer) of 12900 (SpringBoot APM) invoeren. Controleer na de import de paneltypes: oudere revisies van communitydashboards kunnen nog Angular-panelen bevatten die Grafana 12 moet migreren.

Alerting met Prometheus

Essentiële Alertregels

De alertregels van Prometheus genereren meldingen zodra metrieken kritieke drempels overschrijden.

yaml
# alerting-rules.yml
# Alert rules for Spring Boot applications
groups:
  - name: spring-boot-alerts
    rules:
      # Alert if application is down
      - alert: ApplicationDown
        expr: up{job="spring-boot-apps"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Application {{ $labels.instance }} is down"
          description: "{{ $labels.instance }} has been down for more than 1 minute"

      # Alert on HTTP error rate
      - alert: HighErrorRate
        expr: |
          sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (application)
          /
          sum(rate(http_server_requests_seconds_count[5m])) by (application)
          > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High error rate on {{ $labels.application }}"
          description: "Error rate is {{ $value | humanizePercentage }}"

      # Alert on P99 latency, aggregated per application
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            sum by (le, application) (rate(http_server_requests_seconds_bucket[5m]))
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High latency on {{ $labels.application }}"
          description: "P99 latency is {{ $value | humanizeDuration }}"

      # Heap memory alert
      - alert: HighHeapUsage
        expr: |
          sum by (instance) (jvm_memory_used_bytes{area="heap"})
          / sum by (instance) (jvm_memory_max_bytes{area="heap"})
          > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High heap memory usage on {{ $labels.instance }}"
          description: "Heap usage is at {{ $value | humanizePercentage }}"

      # Database connection pool exhausted alert
      - alert: DatabaseConnectionPoolExhausted
        expr: |
          hikaricp_connections_active
          / hikaricp_connections_max
          > 0.9
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Database connection pool nearly exhausted"
          description: "{{ $value | humanizePercentage }} of connections in use"

      # Excessive GC alert
      - alert: HighGCPause
        expr: |
          rate(jvm_gc_pause_seconds_sum[5m])
          / rate(jvm_gc_pause_seconds_count[5m])
          > 0.5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High GC pause time"
          description: "Average GC pause is {{ $value | humanizeDuration }}"

Deze alerts dekken de meest voorkomende productieproblemen af: beschikbaarheid, prestaties en resources. De regel HighLatency heeft sum by (le, ...) nodig: zonder die aggregatie berekent histogram_quantile één P99 per reeks (per URI, methode en status), en één traag endpoint met drie aanroepen laat de alert afgaan.

HTTP-metrieken en databasemetrieken in Spring Boot

Automatische Instrumentatie van HTTP-Requests

Spring Boot instrumenteert elk binnenkomend Spring MVC- en WebFlux-request via de Observation API en publiceert de timer http.server.requests, met de tags method, uri, status, outcome en exception.

yaml
# application.yml
# HTTP metrics configuration
management:
  metrics:
    distribution:
      # Histogram buckets for server-side percentiles
      percentiles-histogram:
        http.server.requests: true
      # SLO buckets: count requests faster than each boundary
      slo:
        http.server.requests: 100ms, 500ms, 1s, 2s
    tags:
      # Common tags added to every meter
      application: ${spring.application.name}
      environment: ${spring.profiles.active:default}

De interfaces WebMvcTagsContributor en WebMvcTagsProvider die in veel voorbeelden opduiken, zijn in Spring Boot 3.0 verwijderd. Eigen tags worden nu toegevoegd door DefaultServerRequestObservationConvention uit te breiden, zoals de referentie van de Spring Boot-metrieken documenteert:

TenantObservationConvention.javajava
// Adds custom tags to http.server.requests (Spring Boot 3.0+)
package com.example.monitoring.config;

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;

@Component
public class TenantObservationConvention extends DefaultServerRequestObservationConvention {

    @Override
    public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
        HttpServletRequest request = context.getCarrier();
        String userId = request.getHeader("X-User-Id");
        String tenantId = request.getHeader("X-Tenant-Id");

        // Keep the default tags and add two bounded ones
        return super.getLowCardinalityKeyValues(context).and(
            KeyValue.of("user.type", userId != null ? "authenticated" : "anonymous"),
            KeyValue.of("tenant", tenantId != null ? tenantId : "default")
        );
    }
}

Alleen key-values met lage cardinaliteit worden Prometheus-labels. Een tag tenant is acceptabel bij enkele tientallen tenants en wordt een cardinaliteitsprobleem bij duizenden. Waarden met hoge cardinaliteit (gebruikers-ID, order-ID) horen in getHighCardinalityKeyValues, dat alleen traces voedt. De tracingkant van de Observation API wordt uitgewerkt in Spring Boot observability met OpenTelemetry.

HikariCP- en SQL-Querymetrieken

De poolmetrieken van HikariCP worden automatisch beschikbaar gesteld met het voorvoegsel hikaricp, met de poolnaam als tag, naast de generieke gauges jdbc.connections. Een ingestelde poolnaam maakt de tag leesbaar:

yaml
# application.yml
# HikariCP pool configuration
spring:
  datasource:
    hikari:
      # Becomes the "pool" tag on hikaricp_* metrics
      pool-name: OrderServicePool
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

Timing per query valt buiten Spring Boot. Het project datasource-micrometer omhult de DataSource en maakt een observation per connectie, query en result set, wat zowel metrieken als trace-spans oplevert. De 2.x-lijn is bedoeld voor Spring Boot 4, de 1.x-lijn voor Spring Boot 3:

xml
<!-- pom.xml -->
<!-- JDBC observations: 2.x for Spring Boot 4, 1.x for Spring Boot 3 -->
<dependency>
    <groupId>net.ttddyy.observation</groupId>
    <artifactId>datasource-micrometer-spring-boot</artifactId>
    <version>2.0.1</version>
</dependency>

Dit vervangt de handmatige aanpak waarbij de DataSource in een ProxyDataSourceBuilder met een zelfgeschreven listener werd gewikkeld. Die aanpak omzeilde de DataSource-autoconfiguratie van Spring Boot en moest bij elke Boot-upgrade worden bijgewerkt. Trage queries wijzen meestal op een N+1-patroon, behandeld in oplossingen voor N+1-queries in Spring Data JPA.

Best Practices voor Productie

Cardinaliteit van Metrieken

Een te hoge cardinaliteit verslechtert de prestaties van Prometheus. Elke unieke combinatie van tags creëert een aparte tijdreeks.

AntiPatternHighCardinality.javajava
// AVOID: unbounded cardinality
package com.example.monitoring.antipattern;

@Service
public class AntiPatternHighCardinality {

    private final MeterRegistry registry;

    // BAD: userId creates one series per user
    public void trackUserAction(String userId, String action) {
        Counter.builder("user.actions")
            .tag("userId", userId)  // Millions of possible values
            .tag("action", action)
            .register(registry)
            .increment();
    }
}
GoodPracticeCardinality.javajava
// Controlled cardinality
package com.example.monitoring.bestpractice;

@Service
public class GoodPracticeCardinality {

    private final MeterRegistry registry;

    // GOOD: user category instead of ID
    public void trackUserAction(User user, String action) {
        Counter.builder("user.actions")
            .tag("userType", user.getSubscriptionType())  // FREE, PREMIUM, ENTERPRISE
            .tag("action", action)
            .register(registry)
            .increment();
    }

    // GOOD: let a timer histogram bucket durations
    public void trackResponseTime(long responseTimeMs) {
        Timer.builder("response.time")
            .publishPercentileHistogram()
            .register(registry)
            .record(Duration.ofMillis(responseTimeMs));
    }
}

Een Timer met histogrambuckets beantwoordt latentievragen beter dan een counter met een zelfgemaakte bucket-tag: PromQL kan elk percentiel uit de buckets berekenen, en de bucketgrenzen blijven consistent tussen services.

Productieklare Configuratie

yaml
# application-production.yml
# Production configuration (Spring Boot 4)
management:
  server:
    # Separate port for management endpoints, not exposed publicly
    port: 9090
  endpoints:
    access:
      default: none
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      probes:
        add-additional-paths: true
    info:
      access: read-only
    prometheus:
      access: read-only
  observations:
    annotations:
      enabled: true
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
      minimum-expected-value:
        http.server.requests: 1ms
      maximum-expected-value:
        http.server.requests: 30s
    tags:
      application: ${spring.application.name}
      environment: production
      version: ${app.version:unknown}

Met access.default: none zijn endpoints zoals env, beans, configprops en mappings ontoegankelijk zonder ze één voor één op te sommen. management.prometheus.metrics.export.enabled staat standaard op true, en een step-instelling heeft geen effect op Prometheus: het is een pull-gebaseerde registry, en het scrape-interval wordt in prometheus.yml ingesteld.

Veelvoorkomende fouten met Actuator en Prometheus

SymptoomOorzaakOplossing
/actuator/prometheus geeft 404Endpoint niet in de exposure-lijst, of registry ontbreektprometheus toevoegen aan management.endpoints.web.exposure.include en controleren of micrometer-registry-prometheus op het classpath staat
Prometheus-target toont "server returned HTTP status 401"Spring Security beschermt elk endpoint behalve healthprometheus openzetten voor het netwerk van de scraper of basic auth configureren in de scrapeconfiguratie
@Timed levert geen metriek opmanagement.observations.annotations.enabled staat standaard op false, of AspectJ ontbreektDe property op true zetten en spring-boot-starter-aspectj toevoegen
spring-boot-starter-aop niet gevonden na upgradeHernoemd in Spring Boot 4spring-boot-starter-aspectj gebruiken
Import van Health of HealthIndicator faalt op Boot 4Package verhuisd naar org.springframework.boot.health.contributorDe imports bijwerken
Deprecation-waarschuwingen op management.endpoint.*.enabledAan/uit-model vervangen in Spring Boot 3.4management.endpoints.access.default en management.endpoint.<id>.access gebruiken
Percentielen op het dashboard wijken sterk af van de werkelijkheidWaarden van publishPercentiles gemiddeld over podspublishPercentileHistogram() en histogram_quantile() gebruiken

Sollicitatievragen over Spring Boot Micrometer

Actuator komt aan bod in de meeste Spring Boot-sollicitatiegesprekken voor medior- en seniorfuncties. De antwoorden die een ervaren profiel van een junior onderscheiden:

  • Verschil tussen liveness en readiness: liveness vertelt Kubernetes de pod te herstarten, readiness vertelt het geen verkeer meer te routeren. Een kandidaat die de database in de liveness-groep zet, heeft dat nog nooit door een storing zien gaan.
  • Waarom Micrometer en niet rechtstreeks de Prometheus-client: Micrometer is een facade. Dezelfde code met Counter en Timer exporteert naar Prometheus, Datadog of OTLP door één dependency te wisselen, en de instrumentatie van Spring zelf (HTTP, JDBC-pool, caches) is tegen Micrometer geschreven.
  • Client-side percentielen of histogrammen: client-side percentielen zijn niet over instanties te aggregeren, histogrammen wel. Dit is de meest gestelde strikvraag over het onderwerp.
  • Wat cardinaliteit betekent en hoe het Prometheus onderuit haalt: één tijdreeks per unieke labelcombinatie; een tag met de gebruikers-ID vermenigvuldigt het geheugengebruik met het aantal gebruikers.
  • Hoe Actuator wordt beveiligd: exposure beperken, toegangsniveaus instellen, management op een aparte poort draaien en beschermen met een eigen SecurityFilterChain.

Meer vragen van dit type staan in de module Spring Boot Actuator sollicitatievragen.

Bronnen

Wat te onthouden over Spring Boot Micrometer en Prometheus

  • Drie stappen naar een werkende scrape: Actuator-starter, micrometer-registry-prometheus, prometheus in de exposure-lijst
  • Toegangsmodel sinds 3.4: management.endpoints.access.default: none plus read-only per endpoint vervangt de deprecated enabled-vlaggen
  • Verschuivingen in Spring Boot 4: spring-boot-starter-aspectj, health-API in org.springframework.boot.health.contributor, EndpointRequest in org.springframework.boot.security.autoconfigure.actuate.web.servlet, probes standaard actief
  • Annotaties hebben een schakelaar nodig: management.observations.annotations.enabled=true, geen handmatige TimedAspect-bean
  • HTTP-tags via de Observation API: DefaultServerRequestObservationConvention uitbreiden, labels met lage cardinaliteit houden
  • Histogrammen boven client-percentielen: publishPercentileHistogram() en sum by (le) in elke histogram_quantile
  • Liveness blijft lokaal: externe afhankelijkheden gaan naar readiness of een eigen groep, nooit naar liveness

Met deze standaarden levert een Spring Boot 4-service metrieken die correct over pods aggregeren, endpoints die niets aan het publieke netwerk prijsgeven en alerts die afgaan bij echte incidenten in plaats van bij één trage URI.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Dagelijkse challenge

Zie jij de bug in Spring Boot?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter van SharpSkill

Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.

Bijgewerkt op 26 oktober 2026

Tags

#spring boot actuator
#micrometer
#prometheus
#monitoring
#observability

Delen

Gerelateerde artikelen