Spring Boot Actuator : health check et metrics

1. Aperçu

Un service en production doit répondre à deux questions : « est-il encore vivant ? » et « comment se comporte-t-il ? ». Spring Boot Actuator répond aux deux via des endpoints prêts à l’emploi, principalement /actuator/health et /actuator/metrics.

Dans cet article, vous allez activer Actuator, écrire un HealthIndicator personnalisé et configurer les probes liveness/readiness pour Kubernetes. Puis vous enregistrerez des métriques métier avec Micrometer, les exporter vers Prometheus et écrire des tests pour les deux parties.

2. Installation

Vous avez besoin d’ajouter le starter Actuator. Cet article utilise également spring-boot-starter-web pour que les endpoints soient servis via HTTP :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Démarrez l’application puis testez :

curl http://localhost:8080/actuator
{"_links":{"self":{...},"health":{...},"health-path":{...}}}

Par défaut, Spring Boot 3.x expose uniquement l’endpoint health via HTTP. Les autres endpoints existent toujours mais ne sont pas accessibles de l’extérieur. C’est un choix de sécurité volontaire.

2.1. Exposer des endpoints supplémentaires

Vous listez clairement les endpoints que vous souhaitez exposer dans application.properties :

management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always

La première ligne expose les trois endpoints health, info et metrics. La deuxième ligne permet à /actuator/health de retourner les détails de chaque composant au lieu d’un simple statut global.

Attention : Évitez d’utiliser management.endpoints.web.exposure.include=* en production. Des endpoints comme env, heapdump ou configprops peuvent exposer des mots de passe, des configurations internes et des données en mémoire.

3. Health check

3.1. Endpoint par défaut

Avec la configuration ci-dessus, appeler /actuator/health retournera quelque chose comme :

{
  "status": "UP",
  "components": {
    "diskSpace": {
      "status": "UP",
      "details": { "total": 499963174912, "free": 213456789504, "threshold": 10485760 }
    },
    "ping": { "status": "UP" }
  }
}

Spring Boot enregistre automatiquement les HealthIndicator en fonction du classpath. S’il y a une DataSource, vous verrez aussi db. S’il y a Redis, vous verrez redis, et ainsi de suite pour les autres composants. Le statut global est agrégé : si un seul composant est DOWN, l’ensemble est DOWN, et le statut HTTP retourné est 503.

3.2. Écrire un HealthIndicator personnalisé

Supposons que votre service de commandes dépend d’une passerelle de paiement externe. D’abord, nous avons un client simple :

public interface PaymentGatewayClient {
    boolean ping();
}

Ensuite, écrivez un indicateur qui vérifie ce client :

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentGatewayClient client;

    public PaymentGatewayHealthIndicator(PaymentGatewayClient client) {
        this.client = client;
    }

    @Override
    public Health health() {
        try {
            if (client.ping()) {
                return Health.up().withDetail("provider", "VNPay").build();
            }
            return Health.down().withDetail("reason", "ping returned false").build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

Quelques points à noter :

  • Le nom du bean est paymentGatewayHealthIndicator. Spring Boot supprime le suffixe HealthIndicator, donc la clé dans le JSON est paymentGateway.
  • withDetail(...) ajoute des détails supplémentaires, affichés uniquement si show-details le permet.
  • Health.down(e) met automatiquement le message d’erreur dans details.error.

Le résultat quand la passerelle de paiement échoue :

{
  "status": "DOWN",
  "components": {
    "diskSpace": { "status": "UP", ... },
    "paymentGateway": {
      "status": "DOWN",
      "details": { "error": "java.net.ConnectException: Connection refused" }
    },
    "ping": { "status": "UP" }
  }
}

Attention : La méthode health() s’exécute à chaque appel de l’endpoint. Si elle fait un appel réseau sans timeout, une dépendance lente figera /actuator/health. Vous devriez toujours définir un court timeout (par exemple 1–2 secondes) pour le client utilisé dans health check.

3.3. Liveness et readiness probe

Kubernetes distingue deux types de probes :

  • Liveness : l’application est-elle gelée ? En cas d’échec, le pod est redémarré.
  • Readiness : l’application est-elle prête à recevoir les demandes ? En cas d’échec, le pod est retiré de l’équilibreur de charge mais n’est pas redémarré.

Spring Boot active automatiquement ces deux groupes quand il détecte qu’il s’exécute sur Kubernetes. Quand vous exécutez localement, vous l’activez manuellement :

management.endpoint.health.probes.enabled=true
management.endpoint.health.group.readiness.include=readinessState,paymentGateway

La première ligne crée les deux endpoints /actuator/health/liveness et /actuator/health/readiness. La deuxième ligne ajoute paymentGateway au groupe readiness. Quand la passerelle de paiement échoue, le pod cesse simplement de recevoir du trafic au lieu d’être inutilement redémarré.

curl http://localhost:8080/actuator/health/liveness
{"status":"UP"}

La configuration correspondante dans le manifeste Kubernetes :

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080

Attention : N’incluez pas les dépendances externes (base de données, API de paiement) dans le groupe liveness. Quand la base de données échoue, tous les pods échouent la sonde liveness et redémarrent simultanément. Le redémarrage ne résout pas la base de données et ajoute une charge lors du démarrage.

4. Métriques avec Micrometer

Actuator utilise Micrometer comme couche d’abstraction pour les métriques. Sans configuration supplémentaire, vous disposez déjà de métriques JVM, CPU, requêtes HTTP, pool de connexions et bien d’autres.

curl http://localhost:8080/actuator/metrics
{"names":["application.ready.time","http.server.requests","jvm.gc.pause","jvm.memory.used","process.cpu.usage", ...]}

Pour voir les détails d’une métrique, vous ajoutez son nom à l’URL. Vous pouvez filtrer par tag en utilisant le paramètre tag :

curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/orders"

4.1. Counter pour le nombre de commandes

Les métriques système ne suffisent pas, vous avez souvent besoin de métriques métier. L’exemple ci-dessous compte le nombre de commandes créées, ventilé par canal de vente :

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

@Service
public class OrderService {

    private final MeterRegistry registry;

    public OrderService(MeterRegistry registry) {
        this.registry = registry;
    }

    public Order createOrder(Customer customer, String channel) {
        Order order = new Order(customer.id(), channel);
        // ... sauvegarder la commande
        Counter.builder("orders.created")
                .description("Nombre de commandes créées")
                .tag("channel", channel)
                .register(registry)
                .increment();
        return order;
    }
}
public record Customer(long id, String name) {}
public record Order(long customerId, String channel) {}

MeterRegistry est automatiquement configuré et injecté par Spring Boot. Appeler register(registry) plusieurs fois avec le même nom et les mêmes tags est sûr, car Micrometer retourne le compteur existant. Après avoir créé trois commandes via le canal web :

curl http://localhost:8080/actuator/metrics/orders.created
{
  "name": "orders.created",
  "description": "Nombre de commandes créées",
  "measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
  "availableTags": [ { "tag": "channel", "values": ["web"] } ]
}

4.2. Timer pour le temps de traitement des paiements

Timer à la fois compte les appels et mesure le temps total et le temps maximum :

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

@Service
public class PaymentService {

    private final Timer paymentTimer;

    public PaymentService(MeterRegistry registry) {
        this.paymentTimer = Timer.builder("payment.process")
                .description("Temps de traitement du paiement")
                .register(registry);
    }

    public boolean pay(Order order) {
        return paymentTimer.record(() -> {
            // ... appeler la passerelle de paiement
            return true;
        });
    }
}

Le Timer est créé une fois dans le constructeur puis réutilisé, car il n’y a pas de tags changeants à chaque appel. record(Supplier) mesure le temps d’exécution de la lambda et retourne son résultat. L’endpoint /actuator/metrics/payment.process affichera trois statistiques : COUNT, TOTAL_TIME et MAX.

4.3. Tag commun pour toutes les métriques

Quand plusieurs services poussent des métriques vers un endroit central, vous devez attacher le nom de l’application à chaque métrique :

management.metrics.tags.application=order-service

Attention : N’utilisez pas de valeurs avec un grand nombre ou illimitées comme tags, comme orderId, customerId ou email. Chaque combinaison de tags crée une série temporelle distincte. Des millions de commandes créent des millions de séries, ce qui gonfle la mémoire de l’application et du système de monitoring. Les tags doivent seulement contenir de petits ensembles de valeurs fixes comme channel, status ou region.

5. Export vers Prometheus

L’endpoint /actuator/metrics convient pour une consultation rapide. Pour le stockage à long terme et les graphiques, vous avez besoin d’un système comme Prometheus. Ajoutez le registre correspondant :

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

Exposez ensuite l’endpoint prometheus :

management.endpoints.web.exposure.include=health,info,metrics,prometheus
curl http://localhost:8080/actuator/prometheus | grep -E "orders|payment"
# HELP orders_created_total Nombre de commandes créées
# TYPE orders_created_total counter
orders_created_total{application="order-service",channel="web"} 3.0
payment_process_seconds_count{application="order-service"} 5.0
payment_process_seconds_sum{application="order-service"} 0.412
payment_process_seconds_max{application="order-service"} 0.153

Micrometer renomme automatiquement selon les conventions de Prometheus : les points deviennent des traits de soulignement, les compteurs ont le suffixe _total, les timers sont en secondes. Le reste est la configuration de Prometheus pour scraper le chemin /actuator/prometheus.

6. Tests

6.1. Tester HealthIndicator

HealthIndicator est une classe ordinaire, donc vous pouvez la tester sans démarrer le contexte Spring :

import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;

import static org.assertj.core.api.Assertions.assertThat;

class PaymentGatewayHealthIndicatorTest {

    @Test
    void whenPingSucceeds_thenStatusIsUp() {
        var indicator = new PaymentGatewayHealthIndicator(() -> true);

        Health health = indicator.health();

        assertThat(health.getStatus()).isEqualTo(Status.UP);
        assertThat(health.getDetails()).containsEntry("provider", "VNPay");
    }

    @Test
    void whenPingThrows_thenStatusIsDownWithError() {
        var indicator = new PaymentGatewayHealthIndicator(() -> {
            throw new IllegalStateException("timeout");
        });

        Health health = indicator.health();

        assertThat(health.getStatus()).isEqualTo(Status.DOWN);
        assertThat(health.getDetails().get("error").toString()).contains("timeout");
    }
}

Puisque PaymentGatewayClient n’a qu’une seule méthode, vous passez une lambda à la place d’un mock.

6.2. Tester les métriques

Micrometer fournit SimpleMeterRegistry qui stocke les métriques en mémoire, très utile pour les tests unitaires :

import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class OrderServiceTest {

    @Test
    void whenOrdersCreated_thenCounterIncrementsPerChannel() {
        var registry = new SimpleMeterRegistry();
        var service = new OrderService(registry);
        var customer = new Customer(1L, "An");

        service.createOrder(customer, "web");
        service.createOrder(customer, "web");
        service.createOrder(customer, "mobile");

        assertThat(registry.get("orders.created").tag("channel", "web").counter().count())
                .isEqualTo(2.0);
        assertThat(registry.get("orders.created").tag("channel", "mobile").counter().count())
                .isEqualTo(1.0);
    }
}

Si la métrique n’existe pas, registry.get(...) lève MeterNotFoundException. De cette façon, le test peut aussi détecter les erreurs de typage du nom de la métrique.

7. Sécurité et erreurs courantes

Port d’administration séparé. Vous pouvez faire fonctionner Actuator sur un port distinct, exposé uniquement au réseau interne :

management.server.port=8081

L’application sert sur 8080, tandis que /actuator/** n’est disponible que sur 8081. Vous bloquez simplement ce port au firewall ou à l’ingress. Dans Kubernetes, pensez à changer le port dans la configuration de la sonde en 8081.

Limiter les détails de la santé. En production, utilisez show-details=when-authorized avec Spring Security à la place de always. Les utilisateurs anonymes ne verront que {"status":"UP"}.

Erreurs courantes :

  • Appeler /actuator/metrics retourne 404 : vous n’avez pas ajouté metrics à exposure.include.
  • Health check échoue aléatoirement : un indicateur appelle le réseau sans timeout, ou le temps de réponse dépasse le timeout de la sonde.
  • Pod redémarre continuellement quand la base de données est instable : la base de données est dans le groupe liveness.
  • La mémoire augmente progressivement : la métrique utilise des tags avec un nombre illimité de valeurs.
  • L’endpoint /actuator/prometheus est invisible : manque micrometer-registry-prometheus ou l’endpoint n’est pas exposé.

8. Conclusion

Spring Boot Actuator vous donne health check et métriques presque gratuitement. Votre travail est de bien configurer : exposer uniquement les endpoints nécessaires, écrire des HealthIndicator avec timeout pour les dépendances critiques, et séparer liveness de readiness pour que Kubernetes réagisse judicieusement.

Avec les métriques, Micrometer vous aide à enregistrer Counter et Timer métier en seulement quelques lignes, puis exporter vers Prometheus avec une simple dépendance. Gardez le nombre de valeurs des tags petit et écrivez des tests avec SimpleMeterRegistry pour que les métriques ne se cassent silencieusement après chaque refactorisation.

spring-bootactuatormicrometermonitoringprometheus