0089 — Métriques Prometheus des services applicatifs (/metrics via Effect)
Contexte
Section intitulée « Contexte »Le contrat d’interface avec le cluster (ADR 0033,
ligne « Métriques ») exige que chaque service applicatif expose /metrics et
déclare un ServiceMonitor, « aucune donnée personnelle dans les labels
(cardinalité + RGPD) ». Côté plateforme, Prometheus scrappe ces
ServiceMonitor (observabilité déléguée au cluster, ADR 0033).
Aujourd’hui aucun service atlas n’expose /metrics : le ServiceMonitor
côté cluster scrappe du vide (finding de l’audit de maturité 2026-06-15, issue
#400, priorité high). Le service services/crf (Hono) a déjà une instrumentation
OpenTelemetry de traces (telemetry.ts), opt-in et no-op safe, montée comme
Layer Effect — mais rien côté métriques.
Termes (charte 0052, R2) :
- métrique Prometheus : série temporelle numérique (compteur, jauge, histogramme) exposée en texte sur un endpoint HTTP, que Prometheus collecte périodiquement (scrape).
ServiceMonitor: ressource Kubernetes (CRD de l’opérateur Prometheus) qui déclare quel service scraper et où ; elle vit côté cluster, pas ici.- cardinalité : nombre de combinaisons de labels d’une métrique. Une étiquette à valeurs non bornées (identifiant, e-mail, URL brute) fait exploser la mémoire de Prometheus — et, si elle porte une donnée personnelle, viole le RGPD.
Décision
Section intitulée « Décision »Exposer /metrics sur services/crf en réutilisant le socle Effect/OTel
existant, pas une seconde pile d’observabilité.
-
Production des métriques via
Metricd’Effect. Le code applicatif déclare ses métriques avec l’APIMetricnative d’Effect (compteurs/jauges/histogrammes), homogène avec le runtime Effect du dépôt (ADR 0045). Pas deprom-clientparallèle. Première métrique livrée : un compteurcrf_http_requests_total{method,route,status}(middleware Hono), oùrouteest la route templatée (/api/v1/records/:id) — jamais l’URL réelle. -
Export via le pont OTel → Prometheus. Un
MeterProviderOpenTelemetry (@opentelemetry/sdk-metrics) équipé duPrometheusExporter(@opentelemetry/exporter-prometheus) — tous deux déjà au lockfile, alignés sur la version OTel des traces — sert le registre. Le pontMetricEffect → OTel est fourni par@effect/opentelemetry(déjà dépendance du service). -
Endpoint
/metricsporté par le service, pas un second port. LePrometheusExporterOTel démarre par défaut son propre serveur HTTP (port 9464) : on désactive ce serveur intégré et on branche le rendu du registre sur une route Hono/metricsdu service. Un seul port exposé, un seulService/ServiceMonitorà déclarer côté cluster. -
Opt-in et no-op safe, comme les traces. Les métriques ne démarrent que si activées par variable d’environnement (même convention que
telemetry.ts: un drapeau dédié, p. ex.OTEL_METRICS_ENABLED, et lesOTEL_*standard). Si désactivé,/metricsrépond404/503sans coût ni dépendance collecteur — le service tourne exactement comme avant. -
Garde-fou RGPD sur les labels (opposable). Aucune métrique ne porte en label une donnée à cardinalité non bornée ou personnelle : pas d’ID de projet, d’e-mail, de token, d’URL brute. Les labels autorisés sont bornés et non-identifiants (méthode HTTP, route templatée
/api/v1/records— jamais l’URL réelle, code de statut, nom logique du service). Vérifié en revue ; toute nouvelle métrique respecte cette règle.
Périmètre de la première itération : services/crf seul. L’app SvelteKit
apps/find-an-expert (autre runtime, route serveur SvelteKit) fera l’objet d’un
second incrément une fois le patron validé ici.
Alternatives écartées
Section intitulée « Alternatives écartées »-
prom-client(lib Prometheus dédiée) montée sur une route Hono. Plus direct, mais introduit une seconde pile d’observabilité à côté d’OTel/Effect, avec sa propre API de métriques et son propre registre — divergence des conventions et double maintenance. Écarté au profit de la continuité du socle. -
Laisser le
PrometheusExporterexposer son propre port (9464). Évite une route applicative, mais oblige à exposer deux ports et à déclarer unServiceMonitorpointant sur un port distinct du trafic applicatif — friction côté contrat cluster et côtéServicek8s. Écarté : un seul port,/metricssur le service. -
Livrer le
ServiceMonitordans cette PR. C’est un point de contact cluster (ADR 0033, garde-fou « même PR ») ; le manifesteServiceMonitoret leServicek8s relèvent du déploiement, côté cluster. Atlas livre la capacité (/metricsexposable) ; le câblage du scrape est tracé séparément. À cadrer avec le dépôtclusterquand le déploiement réel l’exige.
Accepted.
Conséquences
Section intitulée « Conséquences »- Capacité, pas garantie. Atlas rend
/metricsexposable ; l’activation effective (drapeau d’env) et le scrape (ServiceMonitor) sont décidés au déploiement — cohérent avec la posture « le code permet, le déployeur décide » (ADR 0035). - Test de non-régression. Le
/metricsactivé doit répondre200avec un corps au format d’exposition Prometheus, et503quand désactivé — couvert par des tests du service (unité + intégration viacreateApp). - Forçage du build au boot (piège
ManagedRuntime). Le runtime Effect construit sonLayerparesseusement, au premier effet exécuté ; or le pontMetrics.layerne lie le reader Prometheus à sonMeterProviderqu’à ce moment-là. Un scrape arrivant avant tout trafic verrait donc/metricsvide indéfiniment.makeCrfRuntimeforce donc le build une fois au démarrage (exécution d’Effect.void) quand les métriques sont actives. Le runtime est par ailleurs construit avecmakeRuntimeWithShutdownpour que le finalizer du reader tourne à l’arrêt (SIGTERM/SIGINT), symétrique avec les traces. - Met à jour l’ADR 0033 « même PR » le jour du
ServiceMonitor. Quand le scrape sera câblé, le point de contact cluster sera reflété dans l’ADR 0033 dans la même PR (garde-fou existant). - Incrément suivant :
find-an-expert. Même décision, runtime SvelteKit ; rouvre uniquement le point 3 (où monter la route/metricsdans SvelteKit). - Avance l’issue #400 (finding high «
/metricsPrometheus ») et la maturité CNCF/observabilité du contrat.
Voir aussi ADR 0033
(contrat cluster, exigence métriques) et le telemetry.ts du service crf
(patron opt-in no-op safe pour les traces, transposé ici aux métriques).