Aller au contenu
Outils medium

Micrometer : Métriques JVM multi-backend

21 min de lecture

logo micrometer

Micrometer est la façade de métriques des applications JVM : comme SLF4J unifie le logging, Micrometer unifie les métriques. Vous instrumentez une seule fois et vous exportez vers Prometheus, Datadog, InfluxDB ou OTLP, sans changer votre code.

Ce guide s'adresse aux profils intermédiaires et avancés en écosystème Java/Spring. Vous irez de votre premier endpoint /actuator/prometheus jusqu'à une configuration prête pour la production : métriques métier, tags, cardinalité, histogrammes et export OTLP.

  • Exposer un endpoint /actuator/prometheus depuis Spring Boot.
  • Créer des métriques métier : Counter, Gauge, Timer.
  • Maîtriser les tags et éviter l'explosion de cardinalité.
  • Configurer histogrammes et percentiles pour les SLO.
  • Exporter en OTLP vers un OpenTelemetry Collector.

Sans façade, chaque bibliothèque de métriques lie votre code à un backend précis. Passer de Prometheus à Datadog imposerait de réécrire l'instrumentation. Micrometer casse ce couplage : votre code appelle une API neutre, et le choix du backend se règle par une simple dépendance. C'est pourquoi Spring Boot l'a adopté nativement.

QuestionRéponse
C'est quoi ?Une façade de métriques "vendor-neutral" pour la JVM
IntégrationNatif dans Spring Boot depuis 2.0 (via Actuator)
BackendsPrometheus, Datadog, CloudWatch, InfluxDB, OTLP...
FrameworksSpring Boot, Quarkus, Micronaut

Micrometer n'est pas un backend, c'est une API plus un Registry. Le schéma se lit de gauche à droite : votre code ne connaît que l'API, le MeterRegistry choisi par vos dépendances décide du format et de la destination. Retenez surtout le point de bascule : tant que vous ne changez que le registry, aucune ligne de code métier ne bouge.

Architecture Micrometer : de votre code aux backends

ComposantRôle
Micrometer APICe que votre code appelle (Counter, Timer, Gauge)
MeterRegistryL'implémentation qui décide envoyer (Prometheus, OTLP...)
BindersMétriques automatiques : JVM, HTTP, pools, caches
ActuatorExposition des endpoints (Spring Boot)
Observation APIInstrumentation unifiée métriques + traces (Spring Boot 3)

C'est le cas d'usage le plus courant. En 3 étapes, vous avez des métriques dans Prometheus.

  1. Ajoutez les dépendances

    pom.xml
    <!-- Actuator (inclut Micrometer) -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- Registry Prometheus -->
    <dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
  2. Exposez l'endpoint Prometheus

    Par défaut, /actuator/prometheus n'est pas exposé. Ajoutez :

    application.yml
    management:
    endpoints:
    web:
    exposure:
    include: health,info,prometheus,metrics
    prometheus:
    metrics:
    export:
    enabled: true
  3. Configurez le scrape Prometheus

    prometheus.yml
    scrape_configs:
    - job_name: 'spring-app'
    metrics_path: '/actuator/prometheus'
    static_configs:
    - targets: ['localhost:8080']

Vérification :

Fenêtre de terminal
# L'endpoint doit répondre
curl http://localhost:8080/actuator/prometheus
# Vous verrez des métriques comme :
# jvm_memory_used_bytes{...}
# http_server_requests_seconds_count{...}

Les binders fournissent gratuitement les métriques d'infrastructure : mémoire JVM, garbage collector, requêtes HTTP, pools de connexions. Elles répondent à « le service va-t-il bien ? », jamais à « combien de commandes ont été validées ? ». Les trois instruments qui suivent couvrent l'essentiel du besoin métier : Counter pour ce qui s'accumule, Gauge pour ce qui varie dans les deux sens, Timer pour ce qui dure.

Un Counter ne fait que monter, jamais descendre : il compte des événements passés. Dans l'exemple, notez que les deux compteurs sont créés une seule fois dans le constructeur, pas à chaque appel. Créer un Counter dans une méthode appelée en boucle fonctionne quand même, car le registry dédoublonne par nom et tags, mais vous payez une recherche à chaque passage. Le suffixe .total est une convention Prometheus, Micrometer le traduit en orders_created_total.

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
@Service
public class OrderService {
private final Counter orderCounter;
private final Counter errorCounter;
public OrderService(MeterRegistry registry) {
this.orderCounter = Counter.builder("orders.created.total")
.description("Total orders created")
.tag("channel", "web")
.register(registry);
this.errorCounter = Counter.builder("orders.errors.total")
.description("Total order errors")
.tag("type", "validation")
.register(registry);
}
public void createOrder(Order order) {
try {
// ... création
orderCounter.increment();
} catch (ValidationException e) {
errorCounter.increment();
throw e;
}
}
}

Un Gauge mesure un état qui monte et descend : taille de file, connexions ouvertes, threads actifs. Le piège tient à la mémoire : Micrometer conserve une référence faible vers l'objet observé. Si votre taskQueue est ramassée par le garbage collector, la métrique renvoie NaN et disparaît des graphiques. Gardez donc toujours une référence forte, ici le champ de la classe, et ne construisez jamais un gauge sur une variable locale.

import io.micrometer.core.instrument.Gauge;
@Service
public class QueueService {
private final Queue<Task> taskQueue = new ConcurrentLinkedQueue<>();
public QueueService(MeterRegistry registry) {
Gauge.builder("queue.size", taskQueue, Queue::size)
.description("Current queue size")
.tag("queue", "orders")
.register(registry);
}
}

Un Timer mesure une durée et compte les appels dans le même instrument : vous obtenez d'un coup le débit et la latence. L'appel record() prend un bloc de code et le chronomètre, y compris quand il lève une exception, ce qui évite d'oublier un stop(). Attention à la ligne publishPercentiles : ces percentiles sont calculés dans la JVM, ils ne s'additionnent pas entre plusieurs instances. Le chapitre sur les histogrammes détaille la parade.

import io.micrometer.core.instrument.Timer;
@Service
public class PaymentService {
private final Timer paymentTimer;
public PaymentService(MeterRegistry registry) {
this.paymentTimer = Timer.builder("payment.duration")
.description("Payment processing time")
.publishPercentiles(0.5, 0.95, 0.99) // p50, p95, p99
.register(registry);
}
public PaymentResult processPayment(Payment payment) {
return paymentTimer.record(() -> {
// ... traitement
return new PaymentResult();
});
}
}

L'annotation @Timed évite d'écrire le chronométrage à la main, au prix d'un proxy AOP : elle ne fonctionne que sur un bean Spring appelé depuis l'extérieur. Un appel interne à la même classe contourne le proxy et ne produit aucune mesure, c'est la cause numéro un des métriques fantômes. Elle réclame en plus un bean TimedAspect, absent par défaut.

import io.micrometer.core.annotation.Timed;
@Service
public class UserService {
@Timed(value = "user.create.duration",
description = "Time to create a user",
percentiles = {0.5, 0.95, 0.99})
public User createUser(CreateUserRequest request) {
// ...
}
}

Les tags permettent de filtrer et grouper. Mais attention à la cardinalité.

Un tag statique est figé à la création de l'instrument : une seule série temporelle en sortie. Un tag dynamique est fourni à chaque appel, donc le nombre de séries dépend des valeurs réellement rencontrées à l'exécution. Regardez la troisième ligne de l'exemple dynamique : le code range le statut HTTP par classe (2xx, 4xx, 5xx) au lieu de publier le code exact. C'est ce genre de regroupement volontaire qui garde la cardinalité sous contrôle.

// Tags statiques (à la création)
Counter.builder("http.requests.total")
.tag("application", "order-service")
.tag("environment", "production")
.register(registry);
// Tags dynamiques (à l'appel)
public void recordRequest(String method, String route, int status) {
registry.counter("http.requests.total",
"method", method, // GET, POST, PUT...
"route", route, // /api/orders/{id}
"status", String.valueOf(status / 100) + "xx" // 2xx, 4xx, 5xx
).increment();
}

Les tags communs sont appliqués par le registry à toutes les métriques, y compris celles produites par les binders JVM et HTTP que vous n'écrivez pas. C'est le seul moyen fiable de distinguer deux services ou deux environnements dans un Prometheus partagé. Déclarez-les via un MeterRegistryCustomizer plutôt que de les répéter instrument par instrument, sinon la moindre métrique oubliée devient impossible à filtrer côté requête.

@Bean
public MeterRegistryCustomizer<MeterRegistry> commonTags() {
return registry -> registry.config()
.commonTags("application", "order-service")
.commonTags("environment", "production");
}

La cardinalité d'une métrique, c'est le nombre de combinaisons de tags distinctes qu'elle produit. Le tableau ci-dessous oppose systématiquement un identifiant unique à la catégorie qui porte la même information utile pour une alerte. Lisez la colonne de droite comme la question à se poser avant d'ajouter un tag : « combien de valeurs différentes cette expression peut-elle prendre sur un mois ? ». Au-delà de quelques dizaines, remplacez la valeur brute par une classe.

❌ Mauvais✅ CorrectPourquoi
tag("user_id", userId)tag("user_tier", "premium")user_id = millions de valeurs
tag("request_id", reqId)(pas de tag)Chaque requête = nouvelle série
tag("path", "/api/orders/123")tag("route", "/api/orders/{id}")Path avec ID = cardinalité infinie

Un histogramme découpe les durées en tranches et compte combien d'observations tombent dans chacune. Il coûte des séries temporelles supplémentaires, donc réservez-le aux métriques qui portent un SLO ou qui servent au diagnostic de latence. Le tableau associe un besoin précis à la méthode du builder qui y répond : commencez par identifier votre besoin dans la colonne de gauche, la colonne de droite donne l'appel exact.

BesoinConfiguration
Distribution de latence (p50, p95, p99)publishPercentiles(0.5, 0.95, 0.99)
SLO buckets (< 100ms, < 500ms, < 1s)serviceLevelObjectives(100, 500, 1000)
Agrégation serveur-side (Prometheus)publishPercentileHistogram()
Timer.builder("http.server.requests")
.publishPercentiles(0.5, 0.95, 0.99) // Client-side percentiles
.publishPercentileHistogram() // Histogram pour agrégation
.serviceLevelObjectives( // SLO buckets
Duration.ofMillis(100),
Duration.ofMillis(500),
Duration.ofSeconds(1)
)
.register(registry);

Chaque option a un prix, et ce prix se multiplie avec les tags. Le tableau donne un ordre de grandeur par instrument, avant multiplication : c'est la dernière ligne qui doit vous arrêter, puisqu'elle montre que le facteur des tags s'applique à tout ce qui précède. Comparez ce total au nombre de séries que votre Prometheus encaisse déjà avant d'activer les histogrammes sur une métrique très étiquetée.

ConfigurationSéries générées
Counter simple1
Timer avec 3 percentiles~4 (count, sum, p50, p95, p99)
Timer avec histogram (14 buckets)~17
Tout × 5 tags × 4 valeurs chacun× 1024

La propriété management.metrics.enable.<préfixe> agit sur des familles entières de métriques, sans toucher au code. C'est le levier le moins coûteux quand un binder inonde votre backend : couper logback et tomcat retire des dizaines de séries d'un coup. La correspondance se fait par préfixe hiérarchique, donc jvm: false désactive aussi jvm.gc.* et jvm.memory.*.

application.yml
management:
metrics:
enable:
jvm: true
process: true
http: true
logback: false # Désactiver
tomcat: false # Désactiver

Quand le filtrage par préfixe ne suffit pas, un MeterFilter décide instrument par instrument. Les trois réponses possibles comptent : DENY supprime la métrique, ACCEPT la garde définitivement, NEUTRAL laisse la main aux filtres suivants. Renvoyez toujours NEUTRAL par défaut comme dans l'exemple, sinon votre filtre court-circuite tous ceux déclarés après lui. Un MeterFilter sert aussi à réécrire des tags ou à plafonner la cardinalité avec MeterFilter.maximumAllowableTags.

@Bean
public MeterFilter customFilter() {
return new MeterFilter() {
@Override
public MeterFilterReply accept(Meter.Id id) {
// Exclure les métriques internes
if (id.getName().startsWith("jvm.gc.")) {
return MeterFilterReply.DENY;
}
return MeterFilterReply.NEUTRAL;
}
};
}

En Kubernetes, vous ne listez plus les cibles à la main. Le Prometheus Operator lit des objets ServiceMonitor et génère la configuration de scrape à votre place. Deux champs conditionnent le résultat : le selector doit correspondre aux labels du Service (pas ceux du Pod), et le port désigne le nom du port du Service, pas son numéro. Une erreur sur l'un des deux se traduit par une cible absente de /targets, sans message d'erreur.

servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: order-service
namespace: monitoring
spec:
selector:
matchLabels:
app: order-service
namespaceSelector:
matchNames:
- production
endpoints:
- port: http
path: /actuator/prometheus
interval: 30s

Si vous standardisez sur OTLP (plateforme OTel), Micrometer peut exporter directement vers le Collector.

pom.xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-otlp</artifactId>
</dependency>
application.yml
management:
otlp:
metrics:
export:
enabled: true
url: http://otel-collector:4318/v1/metrics
step: 1m

Architecture résultante :

App (Micrometer) → OTLP → OTel Collector → Prometheus/Mimir/Datadog...

Spring Boot 3 introduit l'Observation API : vous instrumentez une fois, vous obtenez métriques + traces.

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
@Service
public class OrderService {
private final ObservationRegistry observationRegistry;
public Order processOrder(OrderRequest request) {
return Observation.createNotStarted("order.process", observationRegistry)
.lowCardinalityKeyValue("channel", "web")
.observe(() -> {
// Ce code est tracé ET mesuré
return doProcessOrder(request);
});
}
}

Les métriques de l'Observation API fonctionnent sans rien de plus, mais les traces exigent deux dépendances distinctes : le pont micrometer-tracing-bridge-otel, qui traduit les observations en spans OpenTelemetry, et un exporteur qui les envoie sur le réseau. Sans l'exporteur, les spans sont créés puis jetés, et vous cherchez longtemps pourquoi Tempo reste vide. Le taux d'échantillonnage à 0.1 conserve 10 % des traces, valeur de départ raisonnable en production.

pom.xml
<!-- Bridge vers OpenTelemetry -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
application.yml
management:
tracing:
sampling:
probability: 0.1 # 10% en prod
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces

Ces cinq erreurs reviennent dans presque toutes les instrumentations reprises en cours de route. Les deux premières lignes touchent la cardinalité et se paient en facture de stockage ; les deux suivantes concernent l'exploitabilité des données, une métrique non agrégeable étant inutilisable dès que le service tourne en plusieurs répliques. Traitez le tableau comme une revue de code : chaque ligne correspond à une chose à chercher dans votre projet.

Anti-patternConséquenceSolution
Tags à haute cardinalitéOOM, coûts explosifsUtiliser des catégories (tier, status_class)
Pas de tags communsRépétition, inconsistanceMeterRegistryCustomizer
Histogrammes partoutExplosion de sériesActiver uniquement sur les métriques RED
Percentiles sans histogramNon agrégeables en clusterActiver publishPercentileHistogram()
Versions explicites avec SpringConflits de versionsLaisser le BOM gérer

Suivez ces cinq contrôles dans l'ordre : ils remontent la chaîne depuis l'application jusqu'au scrape, et chacun élimine une cause entière. L'étape 4 est la plus révélatrice, car /actuator/metrics liste les métriques connues du registry : si la vôtre n'y figure pas, le problème est dans le code, pas dans la configuration ni dans Prometheus.

  1. L'endpoint répond-il ?

    Fenêtre de terminal
    curl http://localhost:8080/actuator/prometheus
    # 404 → endpoint non exposé
  2. L'endpoint est-il exposé ?

    management.endpoints.web.exposure.include: prometheus
  3. Le registry est-il présent ?

    Vérifiez la dépendance micrometer-registry-prometheus.

  4. Les métriques sont-elles créées ?

    Fenêtre de terminal
    curl http://localhost:8080/actuator/metrics
    # Liste toutes les métriques disponibles
  5. Prometheus scrape-t-il ?

    Vérifiez http://prometheus:9090/targets.

Ce tableau part du symptôme observable, celui que vous avez sous les yeux, pour remonter à la cause. Il complète la checklist : la checklist sert quand rien ne remonte du tout, le tableau quand une métrique précise manque ou se comporte mal. La colonne « cause probable » liste la cause la plus fréquente, pas la seule ; vérifiez-la avant de creuser plus loin.

SymptômeCause probableSolution
/actuator/prometheus 404Endpoint non exposéAjouter à exposure.include
Métrique absenteCounter jamais incrémentéVérifier que le code est appelé
OOM côté PrometheusHaute cardinalitéAuditer les tags
@Timed sans effetTimedAspect absentAjouter le bean
Versions incompatiblesVersion expliciteUtiliser le BOM Spring Boot
  • Micrometer = SLF4J des métriques, une API, plusieurs backends
  • Spring Boot Actuator inclut Micrometer, ajoutez juste le registry (Prometheus, OTLP...)
  • Exposez l'endpoint, par défaut /actuator/prometheus n'est pas accessible
  • Cardinalité = coût, jamais d'IDs uniques en tags
  • OTLP Registry pour unifier avec OTel Collector
  • Observation API (Spring Boot 3) = métriques + traces en une instrumentation

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn