infra / monitoring-observabilite
Exporters personnalisés et instrumentation avancée
Explication
Ce que vous allez apprendre
- Écrire un exporter Prometheus personnalisé pour un système sans client natif
- Utiliser le pattern "Collector" pour calculer des métriques fraîches à chaque scrape
- Choisir des buckets d'Histogram adaptés à l'ordre de grandeur réel de la donnée
- Comprendre le rôle de blackbox_exporter pour sonder des endpoints externes
- Reconnaître le pattern "exporter proxy" via
relabel_configs
Dans quel contexte ?
L'entreprise utilise un système de facturation legacy, vieux de dix ans, sans aucune bibliothèque Prometheus disponible pour son langage. Il expose seulement une API REST maison. Pourtant, l'équipe veut suivre la profondeur de sa file d'attente de traitement et l'expiration de sa licence dans les mêmes dashboards Grafana que le reste de l'infrastructure moderne.
D'abord, il faut désacraliser ce qu'est réellement un "exporter"
Après node_exporter et cAdvisor (leçons précédentes), un exporter peut sembler être un outil complexe et spécialisé. En réalité, ce n'est rien de plus qu'un serveur HTTP qui répond au format texte attendu par Prometheus sur un endpoint /metrics — n'importe quel langage capable de faire tourner un serveur HTTP peut en écrire un.
C'est exactement ce que fait le premier exemple de cette leçon : un script Python qui interroge l'API REST du système legacy toutes les 30 secondes, transforme le résultat en Gauges, et les expose via start_http_server.
| Approche | Fraîcheur des données | Complexité |
|---|---|---|
Boucle en arrière-plan (while True: collect(); sleep(30)) | Rafraîchie toutes les 30s, même sans scrape | Simple |
Pattern Collector (.collect()) | Recalculée à CHAQUE scrape Prometheus | Légèrement plus structuré |
Prérequis
Cette leçon suppose une bonne maîtrise de la leçon sur l'écriture de métriques applicatives (Counter, Gauge, Histogram) ; elle en réutilise directement les concepts, mais hors du contexte d'une API web classique.
Une fois l'exporter basique compris, un problème de fraîcheur des données se pose
La boucle while True du premier exemple actualise les valeurs toutes les 30 secondes, indépendamment du rythme de scraping réel de Prometheus. Si Prometheus scrape toutes les 15 secondes, il récupère parfois une valeur "périmée" de quelques secondes ; si Prometheus scrape moins souvent, la donnée est inutilement recalculée en pure perte entre deux scrapes.
Voici comment on résout ce problème : le pattern Collector
Une classe avec une méthode .collect(), enregistrée auprès du REGISTRY de Prometheus, n'est appelée qu'au moment PRÉCIS où Prometheus vient scraper — jamais avant, jamais après. C'est plus simple à raisonner pour des sources de données lentes ou coûteuses à interroger, puisqu'il n'y a plus de fréquence à synchroniser manuellement entre deux systèmes.
Piège fréquent
Réutiliser les buckets par défaut d'un Histogram pour un job batch qui dure plusieurs minutes produit un histogramme complètement inutilisable : toutes les observations tombent dans le dernier bucket, rendant tout calcul de percentile grossier voire faux. Les buckets doivent toujours être pensés pour l'ordre de grandeur réel de la donnée mesurée (millisecondes pour une API, minutes pour un batch).
Il reste un dernier cas particulier, différent de tout ce qui précède : surveiller des systèmes EXTERNES
Astuce
blackbox_exporter ne surveille rien en interne : il sonde activement des endpoints externes (un site public, une API tierce) pour vérifier leur disponibilité, leur code de statut HTTP ou même l'expiration de leur certificat TLS. C'est le seul exporter de ce cours qui teste depuis l'EXTÉRIEUR, comme le ferait un vrai utilisateur.
Son fonctionnement particulier passe par relabel_configs, qui redirige la cible réelle du scrape vers l'exporter lui-même, tout en transmettant l'URL à sonder comme simple paramètre — un pattern qu'on appelle "exporter proxy".
Après cette leçon technique, la suite du cours change complètement d'angle : comment garder toute cette stack de monitoring elle-même fiable et disponible, même en cas de panne d'un de ses composants.
Commandes & code
Exporters personnalisés et instrumentation avancée
# Exporter custom en Python : exposer des métriques venant d'une source SANS client Prometheus natif
# (ex : interroger une API tierce, un fichier, une base legacy)
from prometheus_client import start_http_server, Gauge
import time
import requests
QUEUE_DEPTH = Gauge("legacy_queue_depth", "Profondeur de la file d'attente legacy", ["queue_name"])
LICENSE_DAYS_LEFT = Gauge("license_days_remaining", "Jours restants avant expiration de licence")
def collect():
resp = requests.get("http://legacy-system/api/queues").json()
for queue in resp["queues"]:
QUEUE_DEPTH.labels(queue_name=queue["name"]).set(queue["depth"])
license_info = requests.get("http://legacy-system/api/license").json()
LICENSE_DAYS_LEFT.set(license_info["days_remaining"])
if __name__ == "__main__":
start_http_server(9200) # expose /metrics sur le port 9200, comme un exporter standard
while True:
collect()
time.sleep(30)# Collector custom (classe) : pattern plus propre pour des métriques calculées à la volée à chaque scrape
from prometheus_client.core import GaugeMetricFamily, REGISTRY
from prometheus_client import start_http_server
class DatabaseConnectionCollector:
def collect(self):
metric = GaugeMetricFamily(
"db_connections_active", "Connexions actives par base", labels=["database"]
)
for db_name, count in get_active_connections_from_pool().items():
metric.add_metric([db_name], count)
yield metric
REGISTRY.register(DatabaseConnectionCollector())
start_http_server(9201)
# -> collect() est appelé à CHAQUE scrape Prometheus, pas en arrière-plan : toujours une valeur fraîche# Histogram custom avec buckets adaptés au domaine métier (pas les buckets par défaut, souvent inadaptés)
from prometheus_client import Histogram
# Buckets par défaut de prometheus_client (secondes) : mal adaptés à une latence de batch de plusieurs minutes
BATCH_DURATION = Histogram(
"batch_job_duration_seconds", "Durée des jobs batch",
buckets=[1, 5, 15, 30, 60, 120, 300, 600, 1800, 3600] # buckets pensés pour des minutes/heures, pas des ms
)
# Pour des latences API sub-seconde, l'inverse : des buckets fins en dessous de 100ms
API_LATENCY = Histogram(
"api_call_duration_seconds", "Latence des appels API",
buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5]
)# blackbox_exporter : sonde des endpoints EXTERNES (uptime, TLS, latence DNS) — pas juste des métriques internes
# blackbox.yml
modules:
http_2xx:
prober: http
timeout: 5s
http:
valid_status_codes: [200]
method: GET
tcp_connect:
prober: tcp
timeout: 5s# prometheus.yml : scraper via blackbox_exporter (pattern "proxy" de scraping)
scrape_configs:
- job_name: "blackbox_http"
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://example.com
- https://api.example.com/health
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115 # remplace la cible réelle par l'exporter qui fait la sondeRésumé
- Un exporter custom n'est qu'un serveur HTTP exposant
/metricsau format texte Prometheus : aucune magie, juste une convention. - Le pattern "Collector" (classe avec
.collect()) calcule les valeurs à CHAQUE scrape plutôt qu'en tâche de fond : plus simple à raisonner pour des sources lentes. - Les buckets d'un Histogram doivent être choisis selon l'ordre de grandeur réel de la donnée (ms pour de l'API, minutes pour du batch) : les défauts ne conviennent pas à tout.
- blackbox_exporter illustre le pattern "exporter proxy" : Prometheus scrape l'exporter, qui lui-même sonde une cible externe, via
relabel_configs.
Exercices pratiques
Mission : brancher le système de facturation legacy
Objectif : Écrire un exporter Prometheus custom pour un système sans client natif, avec des buckets d'Histogram adaptés à l'ordre de grandeur réel de la donnée.
Contexte
Le système de facturation legacy expose une API REST maison, sans aucune bibliothèque Prometheus. L'équipe veut suivre la profondeur de sa file d'attente ET la durée de ses jobs de facturation batch, qui durent typiquement entre 2 et 40 minutes — bien au-delà des buckets par défaut pensés pour des latences web.