Spring Boot et Spring Cloud

Spring Boot Actuator et observabilité : health, métriques, traces et logs

Intermédiaire8 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

Vous saurez exposer exactement les endpoints de management dont un service a besoin, câbler les probes de liveness et de readiness, publier des métriques applicatives et métier vers Prometheus, propager le contexte de trace entre services et jusque dans les logs, et verrouiller l'ensemble pour que l'observabilité ne devienne pas une fuite d'informations.

Ce que vous allez apprendre

  • Activer et exposer les endpoints Actuator de façon sélective, et les sécuriser
  • Configurer des groupes de health et les probes Kubernetes de liveness et de readiness, et écrire un HealthIndicator personnalisé
  • Publier des métriques JVM, HTTP et métier via Micrometer vers Prometheus
  • Ajouter le tracing distribué avec Micrometer Tracing et corréler les traces avec les lignes de log
  • Expliquer l'Observation API et instrumenter avec elle un chemin de code critique

Un service que l'on ne peut pas observer est un service que l'on ne peut pas exploiter. Spring Boot Actuator transforme un jar en quelque chose qu'un opérateur peut interroger : est-il en bonne santé, que fait-il, à quelle vitesse, et quelle était la requête qui a échoué à 03:14 ? Cette page couvre les quatre piliers tels que Boot 3 les implémente : les endpoints de management, la santé et les probes, les métriques avec Micrometer, et les traces avec Micrometer Tracing et la corrélation des logs. Au passage, elle traite de ce que la plupart des tutoriels oublient : ne pas exposer tout cela sur l'Internet public.

Endpoints : activation, exposition et sécurisation

Ajoutez spring-boot-starter-actuator. Chaque endpoint a deux interrupteurs indépendants : activé (le bean existe-t-il ?) et exposé (est-il joignable en HTTP ou en JMX ?). Tous les endpoints sauf shutdown sont activés par défaut ; en HTTP, seul health est exposé. L'exposition se déclare sous forme de liste :

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,loggers
      base-path: /actuator          # valeur par défaut ; à changer si / est déjà pris par un préfixe d'API
  endpoint:
    health:
      show-details: when-authorized
    env:
      show-values: when-authorized  # Boot 3 masque par défaut toutes les valeurs d'env et de configprops
  server:
    port: 9090                      # optionnel : trafic de management sur un port séparé

Les endpoints à connaître :

EndpointCe qu'il vous apporteConseil d'exposition
healthÉtat agrégé de l'application et de ses dépendancesPublic sans les détails ; détails pour les utilisateurs authentifiés
infoVersion du build, commit git, informations personnaliséesExposable sans souci en interne
metrics / prometheusMeters Micrometer, en JSON ou au format texte PrometheusÀ exposer au seul scraper
loggersLire et modifier les niveaux de log à chaudAuthentifié
env, configpropsConfiguration effective et propriétés liéesAuthentifié, valeurs masquées
beans, conditions, mappingsIntrospection du contexte pour le débogageAuthentifié, ou en local uniquement
threaddump, heapdumpDiagnostic sous chargeAuthentifié ; les heap dumps contiennent des secrets
shutdownArrêt propre via POSTDésactivé sauf besoin réel

Faire tourner les endpoints de management sur un port séparé (management.server.port) est le durcissement le plus simple : le port principal est relié au load balancer, le port de management uniquement au réseau du cluster. Quand l'application utilise Spring Security, les endpoints du port principal sont protégés par la même filter chain, et les matchers EndpointRequest permettent d'écrire la règle avec précision :

@Bean
SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
    http.securityMatcher(EndpointRequest.toAnyEndpoint())
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
            .anyRequest().hasRole("OPS"))
        .httpBasic(Customizer.withDefaults());
    return http.build();
}

L'endpoint info reste vide tant que vous ne l'alimentez pas. Les plugins Maven et Gradle savent générer les informations de build (goal build-info du spring-boot-maven-plugin, springBoot { buildInfo() } côté Gradle), le plugin git-commit-id ajoute les données du commit, et tout bean InfoContributor ajoute des entrées personnalisées. Avec info.env.enabled=true, les propriétés info.* deviennent elles aussi des entrées.

Health et probes

health agrège les beans HealthIndicator. Boot en auto-configure un pour chaque dépendance qu'il connaît : le DataSource, Redis, RabbitMQ, Kafka, Elasticsearch, l'espace disque, et d'autres encore. Chacun renvoie UP, DOWN, OUT_OF_SERVICE ou UNKNOWN ; l'agrégat vaut DOWN dès qu'un contributeur l'est, et le statut HTTP est 503 pour DOWN et OUT_OF_SERVICE, 200 sinon.

Un indicateur personnalisé tient en un petit bean :

@Component
class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentGatewayClient gateway;

    PaymentGatewayHealthIndicator(PaymentGatewayClient gateway) {
        this.gateway = gateway;
    }

    @Override
    public Health health() {
        try {
            Duration latency = gateway.ping();
            return Health.up().withDetail("latency", latency.toMillis() + "ms").build();
        } catch (Exception ex) {
            return Health.down(ex).build();       // le message de l'exception n'apparaît qu'avec show-details
        }
    }
}

Le nom du bean, amputé du suffixe HealthIndicator, devient la clé du contributeur (paymentGateway). Gardez les indicateurs peu coûteux : ils s'exécutent à chaque probe, et les orchestrateurs sondent souvent. Mettez le résultat en cache quelques secondes si la vérification implique un appel réseau.

Liveness et readiness

Kubernetes distingue deux questions, et votre configuration de health devrait en faire autant. Boot expose deux groupes de health, liveness et readiness, sur /actuator/health/liveness et /actuator/health/readiness. Ils sont activés automatiquement quand l'application détecte qu'elle tourne sur Kubernetes (via la variable d'environnement KUBERNETES_SERVICE_HOST), et peuvent être forcés avec management.endpoint.health.probes.enabled=true.

Par défaut, le groupe liveness ne contient que le LivenessStateHealthIndicator, qui reflète le LivenessState propre à l'application et ne touche à aucun système externe. Le groupe readiness ne contient que le ReadinessStateHealthIndicator, qui passe à REFUSING_TRAFFIC pendant le démarrage et l'arrêt. Les dépendances qui comptent, vous les ajoutez explicitement à la readiness :

management:
  endpoint:
    health:
      group:
        readiness:
          include: readinessState,db,paymentGateway
        liveness:
          include: livenessState

Ne mettez pas la base de données dans la liveness. Si elle disparaît, redémarrer tous les pods ne la fera pas revenir ; cela ne fera qu'ajouter des tempêtes de reconnexions. La readiness est le bon levier : le pod cesse de recevoir du trafic et revient dans le circuit quand la dépendance est rétablie.

Le fragment de déploiement correspondant :

livenessProbe:
  httpGet: { path: /actuator/health/liveness, port: 9090 }
  initialDelaySeconds: 10
  periodSeconds: 10
readinessProbe:
  httpGet: { path: /actuator/health/readiness, port: 9090 }
  periodSeconds: 5

Le code applicatif peut basculer ces états via AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC), par exemple pendant le préchauffage d'un cache. Avec server.shutdown=graceful, Boot commence par marquer la readiness comme refusant le trafic, attend la fin des requêtes en cours, puis s'arrête, ce qui s'accorde naturellement avec les rolling updates.

Métriques avec Micrometer

Les métriques d'Actuator reposent sur Micrometer, une façade indépendante des fournisseurs, à la manière de SLF4J pour les logs. Boot enregistre des meters pour la JVM (mémoire, garbage collection, threads, chargement de classes), le système (CPU, descripteurs de fichiers), les requêtes du serveur HTTP, les appels RestClient/WebClient, les pools de DataSource, les caches, les executors, et bien d'autres. GET /actuator/metrics/http.server.requests affiche le meter avec ses tags ; affinez avec ?tag=uri:/api/orders&tag=status:500.

Pour les envoyer quelque part, ajoutez un registry. Pour Prometheus :

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
  <scope>runtime</scope>
</dependency>

puis exposez prometheus ; le scraper lit /actuator/prometheus au format texte d'exposition. Les registries pour OpenTelemetry (OTLP), Datadog, New Relic, Graphite, InfluxDB et d'autres suivent le même principe et peuvent cohabiter.

C'est avec les métriques métier que l'observabilité se rentabilise. Injectez le MeterRegistry :

@Service
class CheckoutService {

    private final Counter ordersPlaced;
    private final Timer paymentTimer;

    CheckoutService(MeterRegistry registry) {
        this.ordersPlaced = Counter.builder("shop.orders.placed")
                .description("Orders successfully placed")
                .register(registry);
        this.paymentTimer = Timer.builder("shop.payment.duration")
                .publishPercentileHistogram()       // permet à Prometheus de calculer les quantiles
                .register(registry);
    }

    Order checkout(Cart cart) {
        Payment payment = paymentTimer.record(() -> gateway.charge(cart.total()));
        Order order = orders.create(cart, payment);
        ordersPlaced.increment();
        return order;
    }
}

Les tags ajoutent des dimensions (.tag("channel", "web")), et chaque valeur de tag distincte crée une nouvelle série temporelle : ne taguez jamais avec des valeurs non bornées comme des identifiants client ou du texte libre. @Timed sur une méthode fonctionne dès que TimedAspect est déclaré comme bean et que l'AOP est dans le classpath. Des tags communs à tous les meters (management.metrics.tags.application=shop, region=eu-west-1) rendent les dashboards filtrables d'un service à l'autre.

Un mot sur les percentiles : publishPercentiles les calcule dans l'application, et ils ne peuvent pas être agrégés entre instances ; publishPercentileHistogram publie des buckets pour que le système de monitoring calcule les quantiles sur l'ensemble de la flotte. Préférez l'histogramme pour tout ce que vous comptez agréger.

Tracing et Observation API

Les métriques vous disent que le 99e percentile a bondi ; les traces vous disent quel appel en aval en est responsable. Spring Boot 3 a remplacé Spring Cloud Sleuth par Micrometer Tracing, une façade au-dessus d'une implémentation de tracer. Choisissez un bridge et un exporter :

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
management:
  tracing:
    sampling:
      probability: 0.1              # 0.1 par défaut ; 1.0 en développement
  otlp:
    tracing:
      endpoint: http://otel-collector:4318/v1/traces

Une fois cela en place, les requêtes HTTP entrantes, les appels RestClient/WebClient, JDBC (via la bibliothèque datasource-micrometer) et les échanges Kafka et RabbitMQ produisent des spans, et l'en-tête W3C traceparent propage le contexte entre services (B3 est disponible via management.tracing.propagation.type). micrometer-tracing-bridge-brave avec Zipkin constitue la combinaison alternative.

Le concept qui unifie le tout est l'Observation API, introduite dans Micrometer 1.10. Une Observation est une unité de travail qui, une fois démarrée puis arrêtée, produit un timer et un span et tout ce que font par ailleurs les handlers enregistrés. Instrumenter une seule fois votre propre chemin critique vous donne les deux signaux :

Observation.createNotStarted("shop.checkout", observationRegistry)
        .lowCardinalityKeyValue("channel", cart.channel())   // devient un tag de métrique et un attribut de span
        .highCardinalityKeyValue("cart.id", cart.id())      // span uniquement, jamais un tag de métrique
        .observe(() -> checkoutService.checkout(cart));

@Observed sur une méthode fait la même chose de façon déclarative dès que ObservedAspect est enregistré. La distinction entre faible et forte cardinalité inscrit directement dans l'API la règle de tagging vue dans la section sur les métriques.

Corréler logs et traces

Quand le tracing est actif, Boot 3 place traceId et spanId dans le MDC de logging et les inclut dans le pattern console par défaut sous la forme [app,traceId,spanId]. Copier un trace id depuis une ligne de log vers votre interface de tracing, ou depuis une trace vers votre recherche de logs, boucle la boucle entre les deux systèmes. Si vous personnalisez logging.pattern.console, conservez-y %X{traceId} et %X{spanId}, ou utilisez logging.pattern.correlation (Boot 3.1+) pour ne modifier que ce fragment.

Le logging structuré achève le travail : Boot 3.4 sait écrire des lignes JSON au format Elastic Common Schema ou Logstash avec logging.structured.format.console=ecs, si bien qu'une plateforme de logs indexe les trace ids et les champs personnalisés sans parsing par regex. Renvoyer le trace id aux clients dans les réponses d'erreur, comme le suggère le guide sur la gestion des erreurs, transforme un ticket de support en une simple recherche.

Erreurs courantes

Exposer * et l'oublier. Le management.endpoints.web.exposure.include=* d'un tutoriel rend env, heapdump et threaddump accessibles à quiconque trouve /actuator. Énumérez les endpoints dont vous avez besoin et placez le reste derrière une authentification ou un port séparé.

Mettre des dépendances externes dans la probe de liveness. Une panne de base de données devient une boucle de redémarrages sur toute la flotte. La liveness reste interne ; c'est la readiness qui porte les dépendances.

Des health checks coûteux. Un indicateur qui lance une requête sur une grosse table, ou appelle une API tierce sans timeout, fait de la probe elle-même un problème de charge. Donnez un timeout à chaque vérification et mettez les résultats en cache.

Des tags à forte cardinalité. Un tag userId sur http.server.requests crée une série temporelle par utilisateur et peut épuiser le registry comme le backend de monitoring. Réservez les valeurs à forte cardinalité aux spans.

Échantillonner à 100 % en production sans planifier la capacité. Les traces sont volumineuses. Partez de la probabilité par défaut et augmentez-la pour certains services ou pendant les incidents, ou utilisez le tail-based sampling dans le collector.

Croire que les métriques apparaissent sans registry. L'endpoint metrics affiche les meters, mais rien n'est exporté tant qu'aucune dépendance de registry n'est ajoutée ; l'endpoint prometheus n'existe pas sans micrometer-registry-prometheus.

Points clés à retenir

  • Les endpoints Actuator sont activés par défaut mais seul health est exposé en HTTP ; exposez une liste explicite, sécurisez le reste et envisagez un port de management séparé.
  • La liveness ne doit refléter que l'état propre du processus ; la readiness inclut les dépendances dont une requête a besoin, afin qu'une défaillance détourne le trafic au lieu de redémarrer les pods.
  • Micrometer fournit d'emblée les métriques JVM, HTTP et de pools ; ajoutez un registry (Prometheus, OTLP, autres) pour les exporter, et quelques meters métier bien tagués pour leur donner du sens.
  • Micrometer Tracing remplace Sleuth dans Boot 3 ; choisissez un bridge (OpenTelemetry ou Brave) et un exporter, réglez la probabilité d'échantillonnage, et le contexte se propage automatiquement.
  • L'Observation API instrumente un chemin de code une seule fois et produit à la fois un timer et un span ; gardez les valeurs à forte cardinalité hors des tags de métriques.
  • Les trace ids dans le MDC et les logs JSON structurés relient logs, métriques et traces en un seul parcours d'investigation.

Sources et lectures complémentaires

Questions fréquentes

Quels endpoints Actuator sont exposés en HTTP par défaut ?
Uniquement health. Tous les autres endpoints (info, metrics, env, beans, loggers, threaddump, etc.) sont activés mais ne sont pas exposés sur le web tant que vous ne les listez pas dans management.endpoints.web.exposure.include. En JMX, l'exposition par défaut est elle aussi limitée à health depuis Boot 2.2. Ce choix prudent s'explique par le fait que des endpoints comme env et heapdump révèlent la configuration et le contenu de la mémoire.
Quelle différence entre liveness et readiness ?
La liveness répond à la question « ce processus est-il cassé sans espoir de retour ? » : un échec de la probe pousse Kubernetes à redémarrer le conteneur, elle ne doit donc dépendre d'aucun système externe. La readiness répond à « cette instance peut-elle recevoir du trafic maintenant ? » : un échec retire seulement le pod des endpoints du Service, c'est donc le bon endroit pour refléter une connexion perdue à la base de données ou une phase de préchauffage.
Ai-je encore besoin de Spring Cloud Sleuth pour le tracing ?
Non. Spring Cloud Sleuth est arrivé en fin de vie avec Spring Boot 2.x. Dans Boot 3, le tracing est fourni par Micrometer Tracing, avec des bridges vers OpenTelemetry ou Brave, et auto-configuré par Actuator. L'API a changé (Observation et Tracer au lieu des types Span de Sleuth), mais le préfixe de propriétés management.tracing.* couvre l'échantillonnage et la propagation.
Comment changer un niveau de log en production sans redémarrer ?
Exposez l'endpoint loggers et envoyez un POST sur /actuator/loggers/com.example.shop avec un corps JSON comme {"configuredLevel": "DEBUG"}. Le changement est immédiat et perdu au redémarrage. Protégez l'endpoint par une authentification, car il peut générer assez de volume de logs pour affecter le service.
Configuration Spring Boot : properties, YAML, profils et @ConfigurationPropertiesLa configuration externalisée de Spring Boot 3 : priorité des sources, YAML et profils, @ConfigurationProperties typé, config import et secrets.Débutant10 min de lecturePackager et déployer Spring Boot 3 : jar exécutable, conteneurs et images nativesPackager un service Spring Boot 3 en jar exécutable, construire une image en couches ou par buildpacks, régler la JVM en conteneur et arrêter proprement.Intermédiaire10 min de lectureSpring Cloud pour les microservices : Config Server, discovery, gateway et résilienceCe que Spring Cloud apporte à Spring Boot 3 : configuration centralisée, service discovery, load balancing côté client, API gateway et circuit breakers.Avancé9 min de lectureDevOpsLes bases de Kubernetes : architecture, Pods, Deployments et ServicesComment est construit un cluster Kubernetes, ce qu'est vraiment un Pod, et comment Deployments, Services, ConfigMaps et labels font tourner une application.Intermédiaire10 min de lectureDonnéesCentraliser ses logs avec Elasticsearch, Logstash et KibanaLogs structurés, Beats et Elastic Agent, ingest pipelines et grok, index templates, rétention ILM, dashboards Kibana et dimensionnement du cluster.Intermédiaire9 min de lectureArchitecturePerformance applicative : latence, percentiles, profilage et la JVMUne méthode fondée sur la mesure pour rendre une application rapide : loi de Little, percentiles, profilage, GC, accès à la base de données et cache.Avancé12 min de lecture