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 commeenv,heapdumpouconfigpropspeuvent 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 suffixeHealthIndicator, donc la clé dans le JSON estpaymentGateway. withDetail(...)ajoute des détails supplémentaires, affichés uniquement sishow-detailsle permet.Health.down(e)met automatiquement le message d’erreur dansdetails.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,customerIdou 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 commechannel,statusouregion.
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/metricsretourne404: 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/prometheusest invisible : manquemicrometer-registry-prometheusou 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.