feat(monitoring): supervise l'API, la base et l'hôte avec Prometheus et Grafana

L'API exposait /metrics, mais aucun collecteur ne le lisait : monitoring/ ne contenait que des
.gitkeep.

Sous le profil Compose `monitoring` : prometheus, alertmanager, grafana, postgres-exporter,
node-exporter et cadvisor. Tous ont un mem_limit, pour environ 700 Mo au total sur la VM de 8 Go,
et leurs interfaces n'écoutent que sur 127.0.0.1. Le profil est actif en prod via
COMPOSE_PROFILES, donc à chaque déploiement, et se lance à la demande ailleurs
(make monitoring-up).

- Neuf règles d'alerte (API, base, hôte, cibles). Chacune a un cas dans les tests joués par
  `promtool test rules`, en CI comme par make monitoring-check.
- Alertmanager route les alertes par courriel vers Mailpit ; un critical masque le warning de la
  même cible.
- Grafana est provisionné : sources Prometheus et TimescaleDB, et trois tableaux de bord (API,
  données et dérive du modèle, infrastructure).
- Le rôle PostgreSQL `supervision` est en lecture seule sur les seules tables métier
  (db/roles/supervision.sql), posé par make db-ensure-supervision et par stack-up quand le
  profil est actif.
- Le jeton de /metrics passe à Prometheus en secret Compose (APP_METRICS_TOKEN) ;
  provision-host.sh génère ce secret et les deux autres.

Backend :
- un APP_METRICS_TOKEN vide vaut absent ;
- les sondes de santé ne comptent plus dans les métriques ;
- seaux de latence fins autour de 500 ms ;
- un registre Prometheus par application, sans quoi toute application créée après la première
  (dans les tests) ne mesurait rien.

Réf : #26
This commit is contained in:
Johan LEROY
2026-09-23 09:41:12 +02:00
parent 2ed9e1cee4
commit dc952d13aa
28 changed files with 2364 additions and 19 deletions
+114
View File
@@ -107,6 +107,7 @@ services:
APP_SMTP_PORT: "1025"
APP_SMTP_USE_TLS: "false"
APP_SMTP_FROM_ADDRESS: ${APP_SMTP_FROM_ADDRESS:-no-reply@enervision.fr}
APP_METRICS_TOKEN: ${APP_METRICS_TOKEN:-}
ports:
- "${BACKEND_PORT:-8000}:8000"
restart: unless-stopped
@@ -193,6 +194,111 @@ services:
airflow-init:
condition: service_completed_successfully
# Profil `monitoring` : actif en prod par COMPOSE_PROFILES, à la demande ailleurs (ADR 0016).
# Aucun `depends_on` : `make monitoring-up` démarre en `--no-deps`, sans jamais recréer `db`.
prometheus:
image: prom/prometheus:v3.14.0
profiles: ["monitoring"]
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=15d
- --storage.tsdb.retention.size=1GB
volumes:
- ./monitoring/prometheus:/etc/prometheus:ro
- prometheus_data:/prometheus
secrets:
- metrics_token
ports:
- "127.0.0.1:${PROMETHEUS_PORT:-9090}:9090"
mem_limit: 512m
restart: unless-stopped
alertmanager:
image: prom/alertmanager:v0.34.1
profiles: ["monitoring"]
command:
- --config.file=/etc/alertmanager/alertmanager.yml
- --storage.path=/alertmanager
volumes:
- ./monitoring/alertmanager:/etc/alertmanager:ro
- alertmanager_data:/alertmanager
ports:
- "127.0.0.1:${ALERTMANAGER_PORT:-9093}:9093"
mem_limit: 64m
restart: unless-stopped
# Sans mot de passe, Grafana créerait un compte admin/admin : le conteneur refuse de démarrer.
grafana:
image: grafana/grafana:13.2.2
profiles: ["monitoring"]
entrypoint:
- /bin/sh
- -c
- ': "$${GF_SECURITY_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD manquant dans .env}" && exec /run.sh'
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-}
GF_USERS_ALLOW_SIGN_UP: "false"
GF_AUTH_ANONYMOUS_ENABLED: "false"
GF_ANALYTICS_REPORTING_ENABLED: "false"
GF_ANALYTICS_CHECK_FOR_UPDATES: "false"
GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES: "false"
GF_NEWS_NEWS_FEED_ENABLED: "false"
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /etc/grafana/dashboards/api.json
POSTGRES_DB: ${POSTGRES_DB:-enervision}
SUPERVISION_DB_PASSWORD: ${SUPERVISION_DB_PASSWORD:-}
volumes:
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
- ./monitoring/grafana/dashboards:/etc/grafana/dashboards:ro
- grafana_data:/var/lib/grafana
ports:
- "127.0.0.1:${GRAFANA_PORT:-3001}:3000"
mem_limit: 256m
restart: unless-stopped
postgres-exporter:
image: prometheuscommunity/postgres-exporter:v0.20.1
profiles: ["monitoring"]
environment:
DATA_SOURCE_URI: db:5432/${POSTGRES_DB:-enervision}?sslmode=disable
DATA_SOURCE_USER: supervision
DATA_SOURCE_PASS: ${SUPERVISION_DB_PASSWORD:-}
mem_limit: 64m
restart: unless-stopped
node-exporter:
image: prom/node-exporter:v1.12.1
profiles: ["monitoring"]
command:
- --path.rootfs=/host
pid: host
volumes:
- /:/host:ro,rslave
mem_limit: 64m
restart: unless-stopped
# Contrainte : cAdvisor lit les cgroups de tous les conteneurs de l'hôte, d'où `privileged` et
# ses montages en lecture seule. Aucun port publié : seul Prometheus le joint.
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.55.1
profiles: ["monitoring"]
privileged: true
devices:
- /dev/kmsg
command:
- --docker_only=true
- --housekeeping_interval=30s
- --store_container_labels=false
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
mem_limit: 160m
restart: unless-stopped
# Pourquoi : sur le réseau du projet, k6 joint `backend:8000` sans passer par nginx, dont la
# limite par adresse (20 req/s) fausserait la mesure de l'API. `make load-*` le lance (ADR 0015).
k6:
@@ -214,3 +320,11 @@ volumes:
pgdata:
airflow_logs:
airflow_ml_state:
prometheus_data:
alertmanager_data:
grafana_data:
# Vide tant qu'APP_METRICS_TOKEN n'est pas posé : l'API n'exige alors aucun jeton.
secrets:
metrics_token:
environment: APP_METRICS_TOKEN