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 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.
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.
<!-- 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:
# 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: unrestrictedExposure 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:
// 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.
// 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.
// 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
}
}# application.yml
# Enables TimedAspect, CountedAspect and the other Micrometer annotation aspects
management:
observations:
annotations:
enabled: trueSinds 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.
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.
// 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:
# 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-authorizedDe 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:
# 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: 3Met 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.
# 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 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 * 100jvm_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:
// 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:
{
"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.
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.
# 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.
# 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:
// 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:
# 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: 1800000Timing 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:
<!-- 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.
// 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();
}
}// 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
# 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
| Symptoom | Oorzaak | Oplossing |
|---|---|---|
/actuator/prometheus geeft 404 | Endpoint niet in de exposure-lijst, of registry ontbreekt | prometheus 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 health | prometheus openzetten voor het netwerk van de scraper of basic auth configureren in de scrapeconfiguratie |
@Timed levert geen metriek op | management.observations.annotations.enabled staat standaard op false, of AspectJ ontbreekt | De property op true zetten en spring-boot-starter-aspectj toevoegen |
spring-boot-starter-aop niet gevonden na upgrade | Hernoemd in Spring Boot 4 | spring-boot-starter-aspectj gebruiken |
Import van Health of HealthIndicator faalt op Boot 4 | Package verhuisd naar org.springframework.boot.health.contributor | De imports bijwerken |
Deprecation-waarschuwingen op management.endpoint.*.enabled | Aan/uit-model vervangen in Spring Boot 3.4 | management.endpoints.access.default en management.endpoint.<id>.access gebruiken |
| Percentielen op het dashboard wijken sterk af van de werkelijkheid | Waarden van publishPercentiles gemiddeld over pods | publishPercentileHistogram() 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
CounterenTimerexporteert 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
- Spring Boot 4.0 Migration Guide:
spring-boot-starter-aophernoemd naarspring-boot-starter-aspectj, probes standaard actief, nieuwe modulespring-boot-health - Spring Boot 3.4 Release Notes:
enabled-by-defaultenendpoint.<id>.enableddeprecated ten gunste van het toegangsmodel - Referentie van de Spring Boot Actuator-endpoints: access-properties, package van
EndpointRequest, health indicators inorg.springframework.boot.health.contributor, probegroepen en extra paden - Referentie van de Spring Boot-metrieken:
DefaultServerRequestObservationConvention, HikariCP- enjdbc.connections-metrieken, deprecation van simpleclient - Referentie Spring Boot observability:
management.observations.annotations.enableden de AspectJ-vereiste - Micrometer-documentatie over Prometheus: registry op basis van de Prometheus Java client 1.x en de legacy module simpleclient
- datasource-micrometer op GitHub: 2.x voor Spring Boot 4, 1.x voor Spring Boot 3
- Grafana: verwijdering van AngularJS in Grafana 12: panelen Graph en Singlestat vervangen door Time series en Stat
- Spring Boot releases: 4.1.2 als huidige GA-release, 4.2.0-RC1 als pre-release
Wat te onthouden over Spring Boot Micrometer en Prometheus
- Drie stappen naar een werkende scrape: Actuator-starter,
micrometer-registry-prometheus,prometheusin de exposure-lijst - Toegangsmodel sinds 3.4:
management.endpoints.access.default: noneplusread-onlyper endpoint vervangt de deprecatedenabled-vlaggen - Verschuivingen in Spring Boot 4:
spring-boot-starter-aspectj, health-API inorg.springframework.boot.health.contributor,EndpointRequestinorg.springframework.boot.security.autoconfigure.actuate.web.servlet, probes standaard actief - Annotaties hebben een schakelaar nodig:
management.observations.annotations.enabled=true, geen handmatigeTimedAspect-bean - HTTP-tags via de Observation API:
DefaultServerRequestObservationConventionuitbreiden, labels met lage cardinaliteit houden - Histogrammen boven client-percentielen:
publishPercentileHistogram()ensum by (le)in elkehistogram_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.
Zie jij de bug in Spring Boot?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter 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
Delen
Gerelateerde artikelen

Gestructureerde logging in Spring Boot 2026: JSON-logs voor productie met Logback en OpenTelemetry
Volledige gids voor gestructureerde logging in Spring Boot 4.x. Native JSON-ondersteuning, OpenTelemetry starter, MDC tracing en ELK Stack integratie voor productie observability.

Freelance Java Spring Boot developer opdrachten 2026: waar contracten vinden, dagtarieven en voorbereiding
Freelance Java Spring Boot developer opdrachten 2026: actuele dagtarieven (mediaan 440 EUR), beste platforms voor projecten in Nederland en Europa, voorbereiding op technische interviews.

Java Spring Boot Developer Vacatures 2026: Waar Zoeken, Salarisranges, Hoe Voorbereiden
606 actieve Spring Boot posities in september 2026. Dit artikel analyseert salarisgegevens, remote-percentages en sollicitatievoorbereding voor Java Spring Boot ontwikkelaars.