Suivi d’une application Spring avec Actuator

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, 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
management.server.base-path=/xxx

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
management.endpoints.web.exposure.include='*'
management.endpoints.web.exposure.exclude=health,beans

La liste des endpoints actifs par défaut dépend de l’applications. 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

  • /metrics

  • /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 du ConfigurableEnvironment

  • /scheduledtasks, /quartz : tâches planifiées

  • /loggers : liste et configuration des loggers

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

On peut le configurer pour avoir le détail.

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

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.

~$ curl http://localhost:9999/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 statistique 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 du endpoint s’appuie sur Micrometer, avec la publication des métriques sur un endpoint /actuator/metrics.

Grâce à Micrometer, les métriques peuvent être publiées sur un outil de monitoring externe comme Graphite, Prometheus ou Elastic.

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.

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
info.application.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 EnvironmentInfoContributor, GitInfoContributor, BuildInfoContributor et JavaInfoContributor.

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. Les valeurs possibles pour cette propriété sont:

  • ALWAYS, pour voir les valeurs,

  • NEVER, par défaut,

  • WHEN_AUTHORIZED, si l’authentification est activé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.

@Component
public class ActuatorSanitizingFunction implements SanitizingFunction {

    @Value("${application.actuator.keys-to-sanitize:}")
    private String[] excludedKeys;

    @Override
    public SanitizableData apply(SanitizableData data) {
        if (Arrays.stream(excludedKeys).anyMatch(excludedKey -> data.getKey().endsWith(excludedKey))) {
            return data.withValue(SanitizableData.SANITIZED_VALUE);
        }
        return data;
    }

}

1. Spring Boot 3
2. Spring Boot 2