Spring Boot Micrometer et Prometheus : métriques Actuator en production

Intégration de Spring Boot Micrometer et Prometheus sur Spring Boot 4 : modèle d'accès Actuator, config de scraping, métriques HTTP, probes de santé et règles d'alerte.

Spring Boot Actuator monitoring avec Micrometer et Prometheus

Spring Boot Actuator expose des endpoints de santé, de métriques et de diagnostic, et Micrometer est la façade de métriques qui les alimente. L'ajout de la dépendance micrometer-registry-prometheus fournit un endpoint /actuator/prometheus que Prometheus scrape à intervalle fixe. Les exemples ci-dessous ciblent Spring Boot 4.1 (version GA courante, la 4.2 étant en release candidate), Micrometer 1.16 et le client Java Prometheus 1.x.

La réponse courte

L'intégration de Spring Boot Micrometer avec Prometheus tient en trois étapes : ajouter spring-boot-starter-actuator et micrometer-registry-prometheus, ajouter prometheus à management.endpoints.web.exposure.include, puis faire pointer un scrape_config Prometheus vers /actuator/prometheus. Les métriques JVM, HTTP, HikariCP et système sont publiées sans code supplémentaire.

Configurer Spring Boot Micrometer : Actuator et le registry Prometheus

Dépendances Maven requises

Le starter Actuator active les endpoints et embarque Micrometer. Le registry Prometheus formate chaque meter au format d'exposition Prometheus. Le starter AspectJ n'est nécessaire que pour les annotations @Timed et @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>

Deux pièges de version reviennent dans les anciens tutoriels. Depuis Micrometer 1.13 (Spring Boot 3.3), micrometer-registry-prometheus repose sur le client Java Prometheus 1.x ; l'ancien code 0.x vit dans le module déprécié micrometer-registry-prometheus-simpleclient, que la documentation Micrometer Prometheus présente uniquement comme une solution transitoire. Et sur Spring Boot 4, un pom.xml qui déclare encore spring-boot-starter-aop ne se résout plus : le guide de migration Spring Boot 4.0 le renomme en spring-boot-starter-aspectj.

Configuration des endpoints Actuator

Par défaut, seul l'endpoint health est exposé via HTTP. L'endpoint /actuator/prometheus existe dès que le registry est sur le classpath, mais il renvoie une 404 tant qu'il n'est pas ajouté à la liste d'exposition.

Spring Boot 3.4 a remplacé les interrupteurs on/off (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) par un modèle d'accès, et les anciennes propriétés sont dépréciées. Le schéma opt-in fixe l'accès par défaut à none et accorde read-only endpoint par endpoint :

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

Exposition et accès sont deux contrôles distincts. exposure.include décide si un endpoint est mappé en HTTP ; access décide de ce que l'endpoint autorise une fois mappé. Un fichier de propriétés qui utilise encore enabled: true continue de fonctionner avec un avertissement de dépréciation, et c'est le moment de migrer.

L'option show-details: when-authorized affiche les détails de santé uniquement aux utilisateurs authentifiés avec le rôle approprié. Les endpoints eux-mêmes sont sécurisés par une SecurityFilterChain dédiée :

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 a changé de package dans Spring Boot 4 pour org.springframework.boot.security.autoconfigure.actuate.web.servlet, comme le montre la référence des endpoints Actuator. Le DSL Java AuthorizedUrl de authorizeHttpRequests n'a pas de méthode hasIpAddress : la règle IP passe par IpAddressAuthorizationManager.hasIpAddress, disponible depuis Spring Security 6.3.

Métriques personnalisées avec Micrometer

Compteurs, jauges et timers applicatifs

Micrometer fournit plusieurs types de métriques adaptés à différents cas d'usage. Les compteurs mesurent les événements cumulatifs, les jauges les valeurs instantanées, et les timers la durée des opérations.

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);
    }
}

Deux détails de nommage comptent avec le registry Prometheus. Les noms de meters Micrometer utilisent des points et sont convertis en snake case (orders.processing.duration devient orders_processing_duration_seconds). Les compteurs reçoivent automatiquement le suffixe _total, donc nommer un compteur orders.created.total n'ajoute que du bruit.

publishPercentiles(0.5, 0.95, 0.99) calcule les percentiles dans chaque JVM, et ces valeurs ne peuvent pas être agrégées entre instances. publishPercentileHistogram() publie des buckets que histogram_quantile() agrège sur l'ensemble de la flotte : c'est l'option à retenir derrière Prometheus.

Annotations @Timed et @Counted

Pour éviter le code boilerplate, Micrometer propose des annotations qui instrumentent les méthodes via AspectJ.

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

Depuis Spring Boot 3.2, déclarer à la main des beans TimedAspect et CountedAspect n'est plus nécessaire : fixer management.observations.annotations.enabled=true avec spring-boot-starter-aspectj sur le classpath les auto-configure. La propriété vaut false par défaut, ce qui explique la plupart des signalements du type "mon @Timed ne fait rien".

Limitation AOP

Les annotations @Timed et @Counted ne fonctionnent que sur les méthodes publiques de beans Spring appelées via le proxy. Un appel d'une méthode à une autre dans la même classe contourne le proxy et n'est pas instrumenté, et une annotation sur une méthode privée est ignorée. Annoter un contrôleur Spring MVC déjà instrumenté produit des observations en double.

Prêt à réussir tes entretiens Spring Boot ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Endpoints de santé personnalisés

Health Indicators métier

Les Health Indicators permettent de vérifier l'état de dépendances externes et de composants métier critiques. Spring Boot fournit des indicateurs par défaut pour la base de données, Redis, et d'autres services courants. Dans Spring Boot 4, l'API de santé vit dans son propre module spring-boot-health, et Health et HealthIndicator sont passés de org.springframework.boot.actuate.health à 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();
        }
    }
}

Cet indicateur apparaît dans /actuator/health sous le nom paymentGateway : le nom du bean sans le suffixe HealthIndicator.

Health Groups pour Kubernetes

Spring Boot 4 active les probes liveness et readiness par défaut, sur toutes les plateformes et plus seulement quand il détecte Kubernetes. Les groupes /actuator/health/liveness et /actuator/health/readiness existent sans aucune configuration, et déclarer à la main des beans LivenessStateHealthIndicator ou ReadinessStateHealthIndicator est inutile. La configuration ne sert qu'à ajouter des composants à un groupe ou à définir des groupes personnalisés :

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

Le groupe liveness doit rester limité à livenessState. Ajouter une base de données ou une gateway externe à la liveness pousse Kubernetes à redémarrer tous les pods quand cette dépendance tombe, ce qui transforme une panne partielle en panne totale. Les dépendances externes ont leur place dans la readiness, et seulement quand l'application ne peut servir aucune requête sans elles.

Les probes Kubernetes pointent alors vers les endpoints dédiés :

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

Avec add-additional-paths: true, les probes restent sur le port applicatif même quand Actuator tourne sur un port de management séparé, si bien qu'un serveur principal défaillant est détecté par la probe.

Intégration Spring Boot Prometheus et Grafana

Configuration du scraping Prometheus

Prometheus collecte les métriques en interrogeant périodiquement l'endpoint /actuator/prometheus. La configuration de scraping définit les cibles et le chemin des métriques, puisque Prometheus utilise /metrics par défaut.

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: (.+)

Les cibles utilisent le port 9090 parce que la configuration de production plus bas déplace Actuator sur un port de management séparé. Sans management.server.port, la cible de scraping est le port applicatif.

Métriques Spring Boot Actuator publiées par défaut

Actuator avec Micrometer expose les métriques JVM, processus, HikariCP et HTTP sans aucun code. Ces requêtes PromQL couvrent celles que l'on surveille en premier en production :

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"} renvoie une série par pool mémoire (Eden, Survivor, Old Gen avec G1). La diviser par le maximum sans sum by (instance) produit des ratios par pool qui paraissent faux sur un dashboard.

Un bean MeterBinder ajoute des jauges applicatives que les métriques par défaut ne couvrent pas :

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 et process.uptime sont déjà publiés par les binders intégrés de Micrometer. L'exemple montre le pattern, et un vrai binder ne doit enregistrer que ce qui manque aux métriques par défaut.

Dashboards Grafana prêts à l'emploi

Grafana propose des dashboards communautaires pour Spring Boot qui fonctionnent sur les métriques Actuator standard. Un dashboard écrit à la main utilise les types de panneaux timeseries et 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"
        }
      ]
    }
  ]
}

Beaucoup de dashboards copiés depuis d'anciens tutoriels utilisent encore les types de panneaux graph et singlestat. Singlestat a été supprimé dans Grafana 8, et Grafana 12 a retiré complètement le support d'AngularJS : ces panneaux ne s'affichent qu'après migration vers timeseries et stat.

Import Grafana

Pour importer un dashboard communautaire : Grafana > Dashboards > New > Import, puis saisir l'ID 4701 (JVM Micrometer) ou 12900 (SpringBoot APM). Vérifier les types de panneaux après l'import : d'anciennes révisions des dashboards communautaires peuvent encore contenir des panneaux Angular que Grafana 12 doit migrer.

Alerting avec Prometheus

Règles d'alerte essentielles

Les règles d'alerte Prometheus déclenchent des notifications lorsque les métriques dépassent des seuils critiques.

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 }}"

Ces alertes couvrent les problèmes les plus courants en production : disponibilité, performance, et ressources. La règle HighLatency a besoin de sum by (le, ...) : sans lui, histogram_quantile calcule un P99 par série (par URI, méthode et statut), et un seul endpoint lent appelé trois fois déclenche l'alerte.

Métriques HTTP et base de données dans Spring Boot

Instrumentation automatique des requêtes HTTP

Spring Boot instrumente chaque requête entrante Spring MVC et WebFlux via l'Observation API et publie le timer http.server.requests, tagué avec method, uri, status, outcome et 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}

Les interfaces WebMvcTagsContributor et WebMvcTagsProvider que l'on trouve dans de nombreux exemples ont été supprimées dans Spring Boot 3.0. Les tags personnalisés s'ajoutent désormais en étendant DefaultServerRequestObservationConvention, comme le documente la référence des métriques Spring Boot :

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")
        );
    }
}

Seules les key-values à faible cardinalité deviennent des labels Prometheus. Un tag tenant reste acceptable avec quelques dizaines de tenants et devient un problème de cardinalité avec des milliers. Les valeurs à forte cardinalité (ID utilisateur, ID de commande) relèvent de getHighCardinalityKeyValues, qui n'alimente que les traces. Le volet tracing de l'Observation API est détaillé dans l'observabilité Spring Boot avec OpenTelemetry.

Métriques HikariCP et requêtes SQL

Les métriques du pool HikariCP sont exposées automatiquement avec le préfixe hikaricp, taguées par nom de pool, à côté des jauges génériques jdbc.connections. Définir le nom du pool rend le tag lisible :

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

Le timing au niveau des requêtes n'est pas couvert par Spring Boot. Le projet datasource-micrometer enveloppe la DataSource et crée une observation par connexion, requête et result set, ce qui produit à la fois des métriques et des spans de trace. Sa ligne 2.x cible Spring Boot 4, la ligne 1.x 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>

Cela remplace l'approche manuelle qui enveloppait la DataSource dans un ProxyDataSourceBuilder avec un listener écrit à la main, qui contournait l'auto-configuration DataSource de Spring Boot et devait être resynchronisée à chaque montée de version de Boot. Les requêtes lentes signalent le plus souvent un pattern N+1, traité dans les solutions au problème N+1 de Spring Data JPA.

Bonnes pratiques pour la production

Cardinalité des métriques

Une cardinalité excessive dégrade les performances de Prometheus. Chaque combinaison unique de tags crée une série temporelle distincte.

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));
    }
}

Un Timer avec des buckets d'histogramme répond mieux aux questions de latence qu'un compteur avec un tag bucket fait maison : PromQL peut calculer n'importe quel percentile à partir des buckets, et les bornes des buckets restent cohérentes d'un service à l'autre.

Configuration production-ready

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}

Avec access.default: none, des endpoints comme env, beans, configprops et mappings sont inaccessibles sans avoir à les lister un par un. management.prometheus.metrics.export.enabled vaut true par défaut, et un réglage step n'a aucun effet sur Prometheus : c'est un registry en mode pull, et l'intervalle de scraping se règle dans prometheus.yml.

Erreurs fréquentes avec Actuator et Prometheus

SymptômeCauseCorrection
/actuator/prometheus renvoie une 404Endpoint absent de la liste d'exposition, ou registry manquantAjouter prometheus à management.endpoints.web.exposure.include et vérifier que micrometer-registry-prometheus est sur le classpath
La cible Prometheus affiche "server returned HTTP status 401"Spring Security protège tous les endpoints sauf healthOuvrir prometheus au réseau du scraper ou configurer le basic auth dans la config de scraping
@Timed ne produit aucune métriquemanagement.observations.annotations.enabled vaut false par défaut, ou AspectJ est absentPasser la propriété à true et ajouter spring-boot-starter-aspectj
spring-boot-starter-aop introuvable après la montée de versionRenommé dans Spring Boot 4Utiliser spring-boot-starter-aspectj
L'import de Health ou HealthIndicator échoue sur Boot 4Package déplacé vers org.springframework.boot.health.contributorMettre à jour les imports
Avertissements de dépréciation sur management.endpoint.*.enabledModèle on/off remplacé dans Spring Boot 3.4Utiliser management.endpoints.access.default et management.endpoint.<id>.access
Les percentiles du dashboard s'écartent fortement de la réalitéValeurs publishPercentiles moyennées entre podsUtiliser publishPercentileHistogram() et histogram_quantile()

Questions d'entretien sur Spring Boot Micrometer

Actuator revient dans la plupart des entretiens Spring Boot pour les postes confirmés et seniors. Les réponses qui distinguent un profil confirmé d'un junior :

  • Différence entre liveness et readiness : la liveness indique à Kubernetes de redémarrer le pod, la readiness lui indique de cesser d'y router du trafic. Un candidat qui place la base de données dans le groupe liveness ne l'a jamais vécu pendant une panne.
  • Pourquoi Micrometer et pas directement le client Prometheus : Micrometer est une façade. Le même code Counter et Timer exporte vers Prometheus, Datadog ou OTLP en changeant une dépendance, et l'instrumentation propre à Spring (HTTP, pool JDBC, caches) est écrite contre elle.
  • Percentiles côté client contre histogrammes : les percentiles côté client ne peuvent pas être agrégés entre instances, les histogrammes si. C'est la question piège la plus fréquente sur le sujet.
  • Ce que signifie la cardinalité et comment elle casse Prometheus : une série temporelle par combinaison unique de labels ; un tag ID utilisateur multiplie la consommation mémoire par le nombre d'utilisateurs.
  • Comment sécuriser Actuator : limiter l'exposition, fixer les niveaux d'accès, faire tourner le management sur un port séparé et le protéger avec une SecurityFilterChain dédiée.

D'autres questions de ce type sont rassemblées dans le module questions d'entretien Spring Boot Actuator.

Sources

Ce qu'il faut retenir de Spring Boot Micrometer et Prometheus

  • Trois étapes pour un scraping fonctionnel : starter Actuator, micrometer-registry-prometheus, prometheus dans la liste d'exposition
  • Modèle d'accès depuis la 3.4 : management.endpoints.access.default: none plus read-only par endpoint remplace les flags enabled dépréciés
  • Changements de Spring Boot 4 : spring-boot-starter-aspectj, API de santé dans org.springframework.boot.health.contributor, EndpointRequest dans org.springframework.boot.security.autoconfigure.actuate.web.servlet, probes actives par défaut
  • Les annotations demandent un interrupteur : management.observations.annotations.enabled=true, pas de bean TimedAspect manuel
  • Tags HTTP via l'Observation API : étendre DefaultServerRequestObservationConvention, garder des labels à faible cardinalité
  • Histogrammes plutôt que percentiles client : publishPercentileHistogram() et sum by (le) dans chaque histogram_quantile
  • La liveness reste locale : les dépendances externes vont dans la readiness ou un groupe personnalisé, jamais dans la liveness

Ces réglages donnent à un service Spring Boot 4 des métriques qui s'agrègent correctement entre pods, des endpoints qui ne laissent rien fuiter vers le réseau public, et des alertes qui se déclenchent sur de vrais incidents plutôt que sur une URI lente isolée.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Défi du jour

Tu saurais repérer le bug en Spring Boot ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 26 octobre 2026

Tags

#spring boot actuator
#micrometer
#prometheus
#monitoring
#observabilité

Partager

Articles similaires