
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Exposer un endpoint
/actuator/prometheusdepuis 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.
Pourquoi Micrometer ?
Section intitulée « Pourquoi Micrometer ? »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.
| Question | Réponse |
|---|---|
| C'est quoi ? | Une façade de métriques "vendor-neutral" pour la JVM |
| Intégration | Natif dans Spring Boot depuis 2.0 (via Actuator) |
| Backends | Prometheus, Datadog, CloudWatch, InfluxDB, OTLP... |
| Frameworks | Spring Boot, Quarkus, Micronaut |
Modèle mental : comment Micrometer s'emboîte
Section intitulée « Modèle mental : comment Micrometer s'emboîte »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.
| Composant | Rôle |
|---|---|
| Micrometer API | Ce que votre code appelle (Counter, Timer, Gauge) |
| MeterRegistry | L'implémentation qui décide où envoyer (Prometheus, OTLP...) |
| Binders | Métriques automatiques : JVM, HTTP, pools, caches |
| Actuator | Exposition des endpoints (Spring Boot) |
| Observation API | Instrumentation unifiée métriques + traces (Spring Boot 3) |
Quickstart : Spring Boot → Prometheus (10 min)
Section intitulée « Quickstart : Spring Boot → Prometheus (10 min) »C'est le cas d'usage le plus courant. En 3 étapes, vous avez des métriques dans Prometheus.
-
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> -
Exposez l'endpoint Prometheus
Par défaut,
/actuator/prometheusn'est pas exposé. Ajoutez :application.yml management:endpoints:web:exposure:include: health,info,prometheus,metricsprometheus:metrics:export:enabled: true -
Configurez le scrape Prometheus
prometheus.yml scrape_configs:- job_name: 'spring-app'metrics_path: '/actuator/prometheus'static_configs:- targets: ['localhost:8080']
Vérification :
# L'endpoint doit répondrecurl http://localhost:8080/actuator/prometheus
# Vous verrez des métriques comme :# jvm_memory_used_bytes{...}# http_server_requests_seconds_count{...}Créer des métriques métier
Section intitulée « Créer des métriques métier »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.
Counter : événements qui s'accumulent
Section intitulée « Counter : événements qui s'accumulent »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;
@Servicepublic 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; } }}Gauge : valeur instantanée
Section intitulée « Gauge : valeur instantané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;
@Servicepublic 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); }}Timer : durée et nombre d'appels
Section intitulée « Timer : durée et nombre d'appels »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;
@Servicepublic 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(); }); }}Annotation @Timed
Section intitulée « Annotation @Timed »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;
@Servicepublic 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) { // ... }}Tags : les dimensions de vos métriques
Section intitulée « Tags : les dimensions de vos métriques »Les tags permettent de filtrer et grouper. Mais attention à la cardinalité.
Tags statiques ou dynamiques
Section intitulée « Tags statiques ou dynamiques »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();}Tags communs à toute l'application
Section intitulée « Tags communs à toute l'application »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.
@Beanpublic MeterRegistryCustomizer<MeterRegistry> commonTags() { return registry -> registry.config() .commonTags("application", "order-service") .commonTags("environment", "production");}Anti-pattern : haute cardinalité
Section intitulée « Anti-pattern : haute cardinalité »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 | ✅ Correct | Pourquoi |
|---|---|---|
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 |
Histogrammes et percentiles
Section intitulée « Histogrammes et percentiles »Quand les activer ?
Section intitulée « Quand les activer ? »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.
| Besoin | Configuration |
|---|---|
| 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);Impact sur le nombre de séries
Section intitulée « Impact sur le nombre de séries »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.
| Configuration | Séries générées |
|---|---|
| Counter simple | 1 |
| Timer avec 3 percentiles | ~4 (count, sum, p50, p95, p99) |
| Timer avec histogram (14 buckets) | ~17 |
| Tout × 5 tags × 4 valeurs chacun | × 1024 |
Filtrage des métriques
Section intitulée « Filtrage des métriques »Activer ou désactiver par préfixe
Section intitulée « Activer ou désactiver par préfixe »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.*.
management: metrics: enable: jvm: true process: true http: true logback: false # Désactiver tomcat: false # DésactiverMeterFilter, le filtrage programmatique
Section intitulée « MeterFilter, le filtrage programmatique »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.
@Beanpublic 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; } };}Kubernetes : ServiceMonitor
Section intitulée « Kubernetes : ServiceMonitor »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.
apiVersion: monitoring.coreos.com/v1kind: ServiceMonitormetadata: name: order-service namespace: monitoringspec: selector: matchLabels: app: order-service namespaceSelector: matchNames: - production endpoints: - port: http path: /actuator/prometheus interval: 30sOTLP Registry : Micrometer → OTel Collector
Section intitulée « OTLP Registry : Micrometer → OTel Collector »Si vous standardisez sur OTLP (plateforme OTel), Micrometer peut exporter directement vers le Collector.
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-otlp</artifactId></dependency>management: otlp: metrics: export: enabled: true url: http://otel-collector:4318/v1/metrics step: 1mArchitecture résultante :
App (Micrometer) → OTLP → OTel Collector → Prometheus/Mimir/Datadog...Spring Boot 3 : Observation API + Tracing
Section intitulée « Spring Boot 3 : Observation API + Tracing »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;
@Servicepublic 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); }); }}Activer le tracing
Section intitulée « Activer le tracing »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.
<!-- 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>management: tracing: sampling: probability: 0.1 # 10% en prod otlp: tracing: endpoint: http://otel-collector:4318/v1/tracesAnti-patterns
Section intitulée « Anti-patterns »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-pattern | Conséquence | Solution |
|---|---|---|
| Tags à haute cardinalité | OOM, coûts explosifs | Utiliser des catégories (tier, status_class) |
| Pas de tags communs | Répétition, inconsistance | MeterRegistryCustomizer |
| Histogrammes partout | Explosion de séries | Activer uniquement sur les métriques RED |
| Percentiles sans histogram | Non agrégeables en cluster | Activer publishPercentileHistogram() |
| Versions explicites avec Spring | Conflits de versions | Laisser le BOM gérer |
Dépannage
Section intitulée « Dépannage »Checklist « je ne vois pas mes métriques »
Section intitulée « Checklist « je ne vois pas mes métriques » »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.
-
L'endpoint répond-il ?
Fenêtre de terminal curl http://localhost:8080/actuator/prometheus# 404 → endpoint non exposé -
L'endpoint est-il exposé ?
management.endpoints.web.exposure.include: prometheus -
Le registry est-il présent ?
Vérifiez la dépendance
micrometer-registry-prometheus. -
Les métriques sont-elles créées ?
Fenêtre de terminal curl http://localhost:8080/actuator/metrics# Liste toutes les métriques disponibles -
Prometheus scrape-t-il ?
Vérifiez
http://prometheus:9090/targets.
Erreurs courantes
Section intitulée « Erreurs courantes »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ôme | Cause probable | Solution |
|---|---|---|
/actuator/prometheus 404 | Endpoint non exposé | Ajouter à exposure.include |
| Métrique absente | Counter jamais incrémenté | Vérifier que le code est appelé |
| OOM côté Prometheus | Haute cardinalité | Auditer les tags |
| @Timed sans effet | TimedAspect absent | Ajouter le bean |
| Versions incompatibles | Version explicite | Utiliser le BOM Spring Boot |
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »/actuator/prometheus que Prometheus vient scraper. Les deux sont complémentaires, pas concurrents.management.endpoints.web.exposure.include=prometheus. Prometheus scrape ensuite /actuator/prometheus, qui publie les métriques JVM et HTTP automatiquement.À retenir
Section intitulée « À retenir »- 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/prometheusn'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