Spring Boot publie des informations et métriques sur le déploiement et le fonctionnement d’une application avec Actuator.
Configuration
Pour activer Actuator, on ajoute une dépendance vers son starter.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Actuator a nativement un ensemble de endpoints qu’il publie en Web ou en JMX. Par défaut, seul le endpoint Health est publié.
Web endpoints
Le endpoint racine /actuator
donne la liste des endpoints actifs, avec leur URL.
On l’appelle la page de découverte et elle peut être désactivée.
management:
endpoints:
web:
discovery:
enabled: false
Il faut noter que ce contexte est relatif au contexte racine de l’application défini par server.servlet.context-path
.
Il peut être modifié et cette modification est répercutée sur tous les endpoints.
management:
endpoints:
web:
base-path: /management
On peut aussi modifier le port. Dans ce cas, l’URL d’accès ne contient plus le contexte racine de l’application, mais celui du management qui est vide par défaut.
management:
server:
port: 9999
base-path: /actuator
Par défaut, seul le endpoint Health est publié, en version simplifiée. On peut publier explicitement chaque endpoint en Web, inclure l’ensemble de ceux qui sont actifs ou en exclure certains.
management:
endpoints:
web:
exposure:
include: metrics,info
#ou
include='*'
exclude=health,beans
La liste des endpoints actifs par défaut dépend de l’application. Les suivants sont systématiquement (ou souvent) présents pour connaître l’état de l’application :
-
/health : état de santé de l’application et de certains composants
-
/heapdump, /threaddump : génération d’un heap dump ou d’un thread dump
Les suivants sont systématiquement (ou souvent) présents pour le débogage d’un déploiement :
-
/info : informations statiques de l’application
-
/mappings : liste des endpoints applicatifs
-
/beans, /conditions, /configprops, /env : pour déboguer un déploiement, liste de composants Spring dans le contexte, évaluation des conditions pour l’auto-configuration, données utilisées pour les classes annotées avec
@ConfigurationProperties
et contenu duConfigurableEnvironment
-
/scheduledtasks, /quartz : tâches planifiées
-
/liquibase, /flyway : initialisations de la base de données par Liquibase ou Flyway
Les endpoints suivants ne sont publiés que dans certaines conditions.
-
/startup ; fournit les étapes de démarrage si l’application est configurée avec un
BufferingApplicationStartup
-
/shutdown : arrêt de l’application avec une requête POST ; doit être activé explicitement et CSRF désactivé
-
/httpexchanges[1], /httptrace[2] : échanges requête/réponse HTTP, s’il y a un bean `HttpExchangeRepository`[1] ou `HttpTraceRepository`[2]
-
/sessions : gestion des sessions pour une application stateful
-
/auditevents : événement d’audit, s’il y a un bean
AuditEventRepository
-
/integrationgraph : avec Spring Integration
Les chemins des différents endpoints peuvent aussi être reconfigurés.
Par exemple pour /health
:
management:
endpoints:
web:
path-mapping:
health: healthcheck
Chaque endpoint activé par défaut peut être désactivé dans la configuration.
Par exemple pour /health
:
management:
endpoint:
health:
enabled: false
A contrario, un endpoint désactivé par défaut, peut être activé.
management:
endpoint:
shutdown:
enabled: true
D’autres endpoints peuvent être ajoutés via des librairies ou développements personnalisés.
Health
Le endpoint /health
est activé par défaut.
Par défaut, ce endpoint ne donne qu’une information globale.
~$ curl http://localhost:9999/actuator/health
{"status":"UP"}
Détails
On peut le configurer pour avoir l’état des composants.
management:
endpoint:
health:
show-components: always
~$ curl http://localhost:9999/actuator/health
{
"status":"UP",
"components": {
"db":{
"status":"UP",
},
"diskSpace": {
"status":"UP",
},
"ping":{
"status":"UP"
}
}
}
Dans ces conditions, on peut aussi demander l’état d’un composant.
~$ curl http://localhost:9999/actuator/health/db
{
"status":"UP",
}
On peut aussi configurer le endpoint pour avoir plus de détails.
management:
endpoint:
health:
show-details: always
~$ curl http://localhost:9999/actuator/health
{
"status":"UP",
"components": {
"db":{
"status":"UP",
"details":{
"database":"PostgreSQL",
"validationQuery":"isValid()"
}
},
"diskSpace": {
"status":"UP",
"details": {
"total":502392610816,
"free":381957959680,
"threshold":10485760,
"exists":true
}
},
"ping":{
"status":"UP"
}
}
}
Kubernetes
Par ailleurs, le endpoint peut aussi publier des informations adaptées à Kubernetes.
management:
endpoint:
health:
probes:
enabled: true
~$ curl http://localhost:9999/actuator/health/liveness
{
"status": "UP"
}
~$ curl http://localhost:9999/actuator/health/readiness
{
"status": "UP"
}
Metrics
Le endpoint /metrics
est activé par défaut.
Sa racine fournit une liste de noms, sans valeur.
~$ curl http://localhost:9999/actuator/metrics
{
"names": [
"application.ready.time",
"application.started.time",
"disk.free",
"disk.total",
"executor.active",
"executor.completed",
"executor.pool.core",
"executor.pool.max",
"executor.pool.size",
...
]
}
La structure des noms est unidimensionnelle, à la façon de prometheus.
Les informations les plus couramment utilisées concernent la ou les datasource(s) (hikaricp.connections.*
), la mémoire (jvm.memory.*
), le ramasse-miettes (jvm.gc.*
) ou la CPU (process.cpu.usage
).
Chaque nom peut ensuite être utilisé pour accéder à une métrique unitaire.
~$ curl http://localhost:9999/actuator/metrics/jvm.memory.used
{
"name": "jvm.memory.used",
"description": "The amount of used memory",
"baseUnit": "bytes",
"measurements": [
{
"statistic": "VALUE",
"value": 383617384
}
],
"availableTags": [
{
"tag": "area",
"values": [
"heap",
"nonheap"
]
},
{
"tag": "id",
"values": [
"G1 Survivor Space",
"Compressed Class Space",
"Metaspace",
"CodeCache",
"G1 Old Gen",
"G1 Eden Space"
]
}
]
}
Comme on le voit dans cet exemple, les métriques sont organisées via des tags, qu’on peut utiliser en paramètre des requêtes.
~$ curl http://localhost:9999/actuator/metrics/jvm.memory.used?tag=area:heap
{
"name": "jvm.memory.used",
"description": "The amount of used memory",
"baseUnit": "bytes",
"measurements": [
{
"statistic": "VALUE",
"value": 132698240
}
],
"availableTags": [
{
"tag": "id",
"values": [
"G1 Survivor Space",
"G1 Old Gen",
"G1 Eden Space"
]
}
]
}
Métriques Spring MVC
La métrique http.server.requests
des requêtes traitées par Spring MVC est activée par défaut.
Elle peut être désactivée dans la configuration.
management:
metrics:
web:
server:
request:
autotime:
enabled: false
Sans tag, elle fournit des statistique globales, avec le nombre de requêtes traitées, le temps total de traitement et la durée de traitement maximal pour une requête.
~$ curl http://localhost:9999/actuator/metrics/http.server.requests
{
"name": "http.server.requests",
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 754
},
{
"statistic": "TOTAL_TIME",
"value": 134.09375702
},
{
"statistic": "MAX",
"value": 0.76434196
}
]
}
On peut ensuite accéder à des données plus précises via les tags method
, uri
et status
.
~$ curl http://localhost:9999/actuator/metrics/http.server.requests?tag=uri:/secured/roles&tag=method:GET
{
"name": "http.server.requests",
"description": null,
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 56
},
{
"statistic": "TOTAL_TIME",
"value": 5.121863713
},
{
"statistic": "MAX",
"value": 0.143918815
}
]
}
C’est pas vraiment pratique pour détecter les requêtes lentes, mais on a pas mal de détails.
Métriques Spring Data
La métrique spring.data.repository.invocations
des repositories de Spring Data est activée par défaut.
Elle peut être désactivée dans la configuration.
management.metrics.data.repository.autotime.enabled=false
Son fonctionnement est similaire à celui de Spring MVC, avec une vue globale.
~$ curl http://localhost:9999/actuator/metrics/spring.data.repository.invocations
{
"name": "spring.data.repository.invocations",
"description": null,
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 293
},
{
"statistic": "TOTAL_TIME",
"value": 2.293597305
},
{
"statistic": "MAX",
"value": 0
}
]
}
On retrouve aussi des tags pour entrer dans les détails.
~$ curl http://localhost:9999/actuator/metrics/spring.data.repository.invocations?tag=repository:ClientRepository'&'method=findAll
Statistiques Hibernate
Les statistiques d’Hibernate sont désactivées par défaut. Pour les avoir dans les métriques, il faut les activer et ajouter l’extension Micrometer.
Pour activer les statistiques Hibernate, sans les publier dans actuator:
spring:
jpa:
properties:
hibernate:
generate_statistics: true
Puis pour les publier, il faut ajouter une dépendance vers hibernate-micrometer
.
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-micrometer</artifactId>
<version>${hibernate.version}</version>
</dependency>
Micrometer
La mécanique interne des métriques de l'actuator s’appuie sur Micrometer, avec la publication sur le endpoint /actuator/metrics
.
Grâce à Micrometer, il est aussi possible d’ajouter des métriques personnalisées.
Depuis n’importe quel bean, en injectant MeterRegistry
, on peut ajouter les métriques de son choix.
private final Counter createClientCallCounter;
public ClientService(MeterRegistry meterRegistry) {
Counter createClientCallCounter = Counter.builder("jtips.client.create")
.description("Nombre de création de Client")
.register(meterRegistry);
}
public Client create(Client client) {
createClientCallCounter.increment();
...
}
L’autre solution, plus modulaire, c’est de publier un bean qui implémente MeterBinder
et qui publie un ensemble de meters.
Grâce à Micrometer, les métriques peuvent être publiées sur de nombreux outils de monitoring externes comme Graphite, Prometheus ou Elastic.
JMX
Les métriques peuvent être publiées en JMX, en plus du endpoint.
Pour l’activer', il faut ajouter une dépendance vers io.micrometer:micrometer-registry-jmx
.
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-jmx</artifactId>
<version>${micrometer.version}</version>
</dependency>
Par défaut, les informations sont dans le domaine metrics
, ce qui peut être modifié.
management:
jmx:
metrics:
export:
domain: info.jtips
Prometheus
Les métriques peuvent aussi être publiées au format Prometheus.
Il suffit d’ajouter une dépendance vers io.micrometer:micrometer-registry-prometheus
pour activer le endpoint /prometheus
.
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.11.3</version>
</dependency>
Les données exposées ne sont pas les mêmes que pour le endpoint /metrics
.
Elles viennent d’un registre spécifique.
Dump
Le endpoint /threaddump
publie un export de l’ensemble des threads au format JSON.
On peut demander une réponse au format traditionnel avec l’en-tête "Accept": "text/plain"
.
curl http://127.0.0.1:9999/actuator/threaddump --header 'Accept: text/plain'
Le endpoint /heapdump
publie un export de la mémoire au format binaire.
curl http://127.0.0.1:9999/actuator/heapdump -o heap.dump
Info
Le endpoint /info
publie des informations statiques de l’application.
Les informations sont publiées par l’intermédiaire de contributeurs qui sont tous désactivés par défaut.
De ce fait, sans autre configuration, le résultat reste vide.
Le contributeur env
publie l’ensemble des propriétés info.*
.
Il était activé par défaut jusqu’à Spring Boot 2.5 et a été désactivé dans les versions suivantes (attention aux migrations).
management:
info:
env:
enabled: true
info:
application:
name: JTips examples
version: 1.2.3
~$ curl http://localhost:9999/actuator/info
{
"application": {
"name": "JTips examples",
"version": "1.2.3"
}
}
Le contributeur java
publie des informations du runtime Java.
management.info.java.enabled=true
~$ curl http://localhost:9999/actuator/info
{
"java": {
"vendor": "Private Build",
"version": "17.0.5",
"runtime": {
"name": "OpenJDK Runtime Environment",
"version": "17.0.5"
},
"jvm": {
"name": "OpenJDK 64-Bit Server VM",
"vendor": "Private Build",
"version": "17.0.5"
}
}
}
Le contributeur build
publie les informations du fichier /META-INF/build-info.properties
généré au moment du build.
Ça se configure avec Maven (ou gradle).
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Ce contributeur est activé par défaut et publie les informations à condition que le fichier soit présent.
~$ curl http://localhost:9999/actuator/info
{
"build": {
"artifact": "spring-boot-example",
"name": "JTips examples with Spring Boot",
"time": 1672075975.823000000,
"version": "1.2.3",
"group": "info.jtips"
}
}
Le contributeur git
publie les informations présentes dans le fichier git.properties
généré au moment du build.
<build>
<plugins>
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
<version>5.0.0</version>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
<configuration>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
</configuration>
</plugin>
</plugins>
</build>
Ce contributeur est activé par défaut et publie les informations à condition que le fichier soit présent.
~$ curl http://localhost:9999/actuator/info
{
"git": {
"branch": "main",
"commit": {
"id": "0cff7b1",
"time": 1671009439.000000000
}
}
}
Les informations publiées par ce endpoint sont disponibles sous forme de beans, de type |
Il est possible d’ajouter des informations personnalisées en publiant un bean de type InfoContributor
.
Env
Depuis Spring Boot 3, les valeurs fournies par le endpoint /env
sont cachées.
Pour les afficher, il faut configurer la propriété management.endpoint.env.show-values
avec pour valeurs possibles:
-
ALWAYS
, pour voir les valeurs, -
NEVER
, par défaut, -
WHEN_AUTHORIZED
, si la requête est authentifiée.
Il y a le même comportement pour /configprops
, et la propriété management.endpoint.configprops.show-values
équivalente.
Lorsqu’on décide d’afficher les valeurs, on se retrouve avec des mots de passe ou des secrets qu’on ne souhaiterait pas montrer.
Pour choisir quelles valeurs doivent être cachées malgré tout, il faut créer un bean qui implémente SanitizingFunction
.
@Configuration
public class ActuatorConfiguration {
@Value("${application.actuator.keys-to-sanitize:}")
private String[] excludedKeys;
@Bean
public SanitizingFunction actuatorSanitizingFunction {
return data -> {
if (Arrays.stream(excludedKeys)
.anyMatch(excludedKey -> data.getKey().equals(excludedKey))) {
return data.withValue(SanitizableData.SANITIZED_VALUE);
}
return data;
};
}
}
HttpExchanges
Le endpoint /httpexchanges
de Spring Boot 3 remplace /httptrace de Spring Boot 2.
Il permet de consulter le détail des requêtes HTTP reçues par l’application.
Dans les deux cas, le endpoint n’est pas activé par défaut.
Il faut déclarer un bean de type HttpExchangeRepository
.
@Configuration
public class ActuatorConfiguration {
@Bean
public HttpExchangeRepository httpExchangeRepository() {
return new InMemoryHttpExchangeRepository();
}
...
}
InMemoryHttpExchangeRepository
est la seule implémentation fournie pas Spring.
Elle conserve les 100 denières requêtes en mémoire.
AuditEvents
Le endpoint /auditevents
permet de consulter les événements d’authentification.
Il s’active de la même façon que /httpexchanges, en déclarant un bean de type AuditEventRepository
.
@Configuration
public class ActuatorConfiguration {
...
@Bean
public AuditEventRepository auditEventRepository() {
return new InMemoryAuditEventRepository();
}
}
InMemoryAuditEventRepository
est la seule implémentation fournie pas Spring.
Elle conserve les 100 deniers événements en mémoire.
Loggers
Le endpoint /loggers
publie les détails de tous les loggers de l’application.
Avec une requête GET, on peut obtenir la configuration d’un logger unique.
~$ curl http://localhost:8081/actuator/loggers/com.meshimer.cfws
{
"configuredLevel": "INFO",
"effectiveLevel": "INFO"
}
Avec une requête POST, on peut changer la configuration d’un logger, à chaud.
~$ curl --request POST http://localhost:8081/actuator/loggers/info.jtips \
--data '{ "configuredLevel": "DEBUG" }'
Après cette requête, les logs de debug sont écrits, jusqu’au prochain redémarrage. Pour l’annuler, on peut envoyer la requête suivante:
~$ curl --request POST http://localhost:8081/actuator/loggers/info.jtips \
--data '{}'
Si le système de logs est configuré avec une sortie fichier (propriété logging.file.name
), on peut en demander le contenu sur le endpoint /logfile
.
~$ curl --header 'Accept: text/plain' http://localhost:8081/actuator/logfile
2023-12-08T21:25:48.866Z INFO 632981 --- [restartedMain] info.jtips.Application : Starting Application using Java 17 with PID 632981
2023-12-08T21:25:48.868Z INFO 632981 --- [restartedMain] info.jtips.Application : The following 1 profile is active: "local"
2023-12-08T21:25:49.738Z INFO 632981 --- [restartedMain] o.s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-12-08T21:25:50.058Z INFO 632981 --- [restartedMain] o.s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 310 ms. Found 65 JPA repository interfaces.
...
Endpoint personnalisé
On peut aussi créer nos propres endpoints.
Technique
Pour ça, il faut créer un bean annoté avec @Endpoint
, avec au moins une opération, annotatée avec @ReadOperation
, @WriteOperation
ou @DeleteOperation
.
@Component
@Endpoint(id = "echo")
public class EchoEndpoint {
@ReadOperation
public String echo(@Selector String input) {
return "Hello " + input;
}
}
L’annotation @Selector
permet de gérer des path parameters, alors qu’un parameter de méthode sans annotation gère un paramètre de requête.
Comme pour un endpoint applicatif, on peut affiner ses capacités.
Par exemple, on peut choisir le type de réponse dans l’annotation @ReadOperation
, faire une opération par type accepté.
Plutôt que de retourner un objet simple, on peut retourner un WebEndpointResponse<?>
, en choisissant le status code.
Par contre, on ne peut pas modifier les headers de la réponse.
@Component
@Endpoint(id = "echo")
public class EchoEndpoint {
@ReadOperation(produces = "text/plain")
public WebEndpointResponse<String> echoSimple(@Selector String input) {
return new WebEndpointResponse<>("Hello " + input);
}
@ReadOperation(produces = "application/json")
public WebEndpointResponse<Message> echoJson(@Selector String input) {
return new WebEndpointResponse<>(new Message("Hello", input));
}
public record Message(String hi, String dest) {}
}
Exemple
J’ai utilisé cette technique pour créer un endpoint de métriques qui utilise le registre Prometheus et qui est capable d’exposer les données au format Prometheus, en JSON ou en CSV.
/
* An @Endpoint for exposing aggregated metrics from Prometheus registry.
*
* Can be requested in JSON format (default), in CSV (text/csv) or in Prometheus format ("text/plain").
*/
@Component
@Endpoint(id = "metrix")
public class MetrixEndpoint {
private final PrometheusScrapeEndpoint prometheusScrapeEndpoint;
private final CollectorRegistry collectorRegistry;
public MetrixEndpoint(PrometheusScrapeEndpoint prometheusScrapeEndpoint, CollectorRegistry collectorRegistry) {
this.prometheusScrapeEndpoint = prometheusScrapeEndpoint;
this.collectorRegistry = collectorRegistry;
}
/
* JSON format, default
*/
@ReadOperation()
public List json(@Nullable Set includedNames) {
Enumeration samples = (includedNames != null)
? this.collectorRegistry.filteredMetricFamilySamples(includedNames)
: this.collectorRegistry.metricFamilySamples();
return Collections.list(samples);
}
/
* Prometheus format ("text/plain")
*/
@ReadOperation(producesFrom = TextOutputFormat.class)
public WebEndpointResponse prometheus(TextOutputFormat format, @Nullable Set includedNames) {
return prometheusScrapeEndpoint.scrape(format, includedNames);
}
/
* CSV format ("text/csv")
*/
@ReadOperation(produces = "text/csv")
public String csv(@Nullable Set includedNames) {
return json(includedNames).stream()
.flatMap(metric -> metric.samples.stream().map(sample -> new MetricSample(metric, sample)))
.map(MetricSample::toCsv)
.collect(Collectors.joining());
}
@ReadOperation
public Collector.MetricFamilySamples single(@Selector String requiredMetricName) {
return this.collectorRegistry.filteredMetricFamilySamples(Set.of(requiredMetricName)).nextElement();
}
private record MetricSample(Collector.MetricFamilySamples metric, Collector.MetricFamilySamples.Sample sample) {
String toCsv() {
Stream labels = StreamUtils.zip(
sample.labelNames.stream(),
sample.labelValues.stream(),
(name, value) -> String.format("%s=\"%s\"", name, value));
return "%s;%s;%s;%s%n"
.formatted(sample.name, labels.collect(Collectors.joining(",")), sample.value, metric.unit);
}
}
}
Références
Les exemples de cette page ont été testés avec Spring Boot 2.7 et Spring Boot 3.1.