Français
Supervision
Une instance Mankomail indique son état par deux points d’entrée HTTP et par ses journaux. Cette page décrit les deux, ce qui mérite d’être surveillé, comment dimensionner la base, et ce que fait l’instance d’elle-même après un redémarrage ou une coupure.
Les sondes de santé
Chaque processus applicatif, quel que soit son APP_ROLE, répond à deux points d’entrée sur son port HTTP. Aucun ne demande d’authentification.
| Point d’entrée | Question à laquelle il répond | Interroge la base |
|---|---|---|
/healthz | Le processus est-il vivant ? | Non |
/readyz | Le processus peut-il servir des requêtes ? | Oui |
/healthz répond toujours 200 tant que le processus tourne :
json
{ "status": "ok", "brand": "Mankomail", "version": "1.4.0", "uptimeSeconds": 5321.4 }brand est la valeur de BRAND_NAME, version celle d’APP_VERSION, et uptimeSeconds le temps écoulé depuis le démarrage du processus.
/readyz fait deux vérifications, dans l’ordre : la base répond à une requête, puis chaque migration de l’image a été appliquée. Il répond 200 quand les deux passent :
json
{
"status": "ready",
"checks": [
{ "name": "database", "ok": true },
{ "name": "migrations", "ok": true }
]
}et 503 sinon, avec la raison dans detail :
json
{
"status": "not_ready",
"checks": [
{ "name": "database", "ok": false, "detail": "connect ECONNREFUSED 172.18.0.2:5432" },
{ "name": "migrations", "ok": false, "detail": "database unreachable" }
]
}Quand la base est joignable mais que des migrations manquent, la vérification migrations porte pending: suivi des noms de fichier des migrations manquantes.
Quelle sonde utiliser, et où
- Sonde de vivacité (health check Docker,
livenessProbeKubernetes, superviseur de processus) :/healthz. Une sonde qui interrogerait la base ferait redémarrer en boucle des processus sains pendant une courte coupure de la base. - Sonde de disponibilité et répartiteur de charge (
readinessProbeKubernetes, vérifications amont d’un proxy) :/readyz. - Vérification de disponibilité externe :
<PUBLIC_BASE_URL>/readyz, depuis l’extérieur du serveur, pour vérifier aussi le reverse proxy et le certificat.
L’image déclare un HEALTHCHECK Docker sur /healthz : toutes les 30 secondes, délai de 5 secondes, état « unhealthy » après 3 échecs, avec une période de démarrage de 20 secondes. docker compose ps affiche le résultat.
Le champ detail de /readyz peut contenir des messages d’erreur de la base et des noms de fichiers de migration. Si vous préférez ne pas les exposer publiquement, réservez /readyz à vos adresses de supervision au niveau du reverse proxy, et interrogez-le localement sur 127.0.0.1:<APP_PORT>.
Les journaux
L’application écrit ses journaux sur la sortie standard, un objet JSON par ligne. Avec le fichier Compose de référence, lisez-les avec :
bash
docker compose -f compose.reference.yaml logs -f appChaque ligne porte :
| Champ | Contenu |
|---|---|
level | fatal, error, warn, info, debug ou trace |
time | Horodatage ISO 8601 |
role | L’APP_ROLE du processus |
version | L’APP_VERSION du processus |
msg | Le message |
module | La partie de l’application qui a écrit la ligne, quand elle en a une |
Les requêtes HTTP sont journalisées au niveau info, avec un identifiant de requête (reqId) commun à la ligne de la requête et à celle de la réponse.
LOG_LEVELfixe le niveau minimal :fatal,error,warn,info(défaut),debug,traceousilent.LOG_FORMATacceptejson(un objet JSON par ligne, pour les collecteurs de journaux) etpretty(lignes lisibles, pour le développement).- Les secrets et les données personnelles sont masqués : en-têtes d’autorisation, cookies, mots de passe, jetons, clés d’API, ainsi que l’objet, le corps et les destinataires des mails sont remplacés par
[redacted]. - Une erreur fatale au démarrage, comme une migration en échec ou une base injoignable, est écrite en texte simple, en commençant par
fatal: failed to start, suivie de l’erreur.
Pour filtrer les lignes JSON, utilisez jq :
bash
docker compose -f compose.reference.yaml logs --no-log-prefix app \
| jq -c 'select(.level == "error" or .level == "fatal")'Le fichier Compose de référence garde au plus cinq fichiers de 10 Mo de journaux par conteneur. Pour les conserver plus longtemps, envoyez-les vers un système de journaux (Loki, Elasticsearch, un serveur syslog) avec un pilote de journalisation Docker ou un collecteur.
Les lignes de journal à connaître
| Ligne de journal | Niveau | Ce qu’elle signifie |
|---|---|---|
configuration: … | warn | Une variable a une valeur invalide, et sa valeur par défaut est utilisée à la place. |
ENCRYPTION_KEY is not set / ENCRYPTION_KEY is invalid | warn | L’enregistrement des secrets est désactivé. Voir La clé de chiffrement. |
database schema is up to date | info | Les migrations sont appliquées. |
postgres is not accepting connections yet, retrying | warn | La base n’est pas encore prête au démarrage. Normal pendant quelques secondes. |
expired job leases reclaimed (a worker died holding them) | warn | Des tâches tenues par un processus mort ont été rendues à la file. Attendu après un crash ou un arrêt forcé. |
claim failed, scheduler tick failed | error | Les boucles de fond ne joignent pas la base. Répété chaque seconde pendant une coupure de la base. |
postgres pool: connection lost while in use | warn | Une connexion à la base a été coupée pendant une requête. |
instance maintenance done | info | L’entretien horaire a tourné. |
APP_ROLE=api: this process runs no worker | warn | Ce processus ne fait aucun travail de fond : assurez-vous qu’un processus worker tourne ailleurs. |
Quelques lignes error pendant un redémarrage de la base sont attendues. Les mêmes lignes répétées pendant des minutes, alors que PostgreSQL est debout, ne le sont pas.
Les métriques
La version actuelle n’expose aucun point de métriques : pas d’export Prometheus ni OpenTelemetry. Surveillez l’instance par les sondes de santé, les journaux, et quelques mesures que vous collectez vous-même.
| Mesure | Comment l’obtenir | Pourquoi |
|---|---|---|
État de /readyz | Vérification HTTP externe | La disponibilité de l’instance. |
| Espace disque libre | Votre supervision système | Le stockage objet et PostgreSQL grossissent avec les boîtes. Un disque plein arrête tout. |
| Taille de PostgreSQL | SELECT pg_size_pretty(pg_database_size(current_database())); | Elle guide le dimensionnement de la base, ci-dessous. |
| Âge de la plus ancienne tâche prête | Requête SQL ci-dessous | Le meilleur signe d’un travail de fond bloqué. |
| Tâches qui ont épuisé leurs tentatives | Requête SQL ci-dessous | Du travail qui demande un regard humain. |
La file de tâches vit dans la table jobs. Cette requête donne son état :
sql
SELECT
count(*) FILTER (WHERE dead_at IS NULL AND claimed_by IS NULL AND run_at <= now()) AS ready,
count(*) FILTER (WHERE dead_at IS NULL AND claimed_by IS NOT NULL) AS running,
count(*) FILTER (WHERE dead_at IS NOT NULL) AS dead,
now() - min(run_at) FILTER (WHERE dead_at IS NULL AND claimed_by IS NULL AND run_at <= now())
AS oldest_ready_age
FROM jobs;Lancez-la avec :
bash
docker compose -f compose.reference.yaml exec postgres \
sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'oldest_ready_agereste à quelques secondes sur une instance saine. Plusieurs minutes signifient qu’aucun processus ne prend de travail : vérifiez qu’un processusworkeroualltourne et que ses journaux ne montrent pas declaim failed.deadcompte les tâches qui ont épuisé leurs tentatives. Elles sont gardées 30 jours, puis supprimées par l’entretien. Un nombre qui ne cesse de croître mérite un regard sur leurkindet leurlast_error.
Le schéma de la base est interne et peut changer d’une version à l’autre : vérifiez ces requêtes après une mise à jour.
Les échecs de workflows ne relèvent pas de l’exploitation : les membres les voient sur la page Activité et dans leurs notifications. Voir Erreurs et nouvelles tentatives.
Le dimensionnement
Les tests de charge du chemin d’ingestion montrent qu’un seul processus applicatif absorbe bien plus de mails entrants que n’en reçoit une organisation de cent boîtes : les workers de fond ne sont pas la première limite. La mémoire de PostgreSQL l’est.
- Un message coûte environ 1,7 Ko dans PostgreSQL, index compris. Les corps de mails et les pièces jointes sont dans le stockage objet, de très loin le premier consommateur de disque.
- L’image PostgreSQL démarre avec
shared_buffers=128MB, quelle que soit la mémoire du conteneur. Vers 50 000 messages, les données utiles ne tiennent plus dans ce cache, et la lecture des fils anciens commence à toucher le disque.
| Messages en base | Taille de PostgreSQL | shared_buffers | Mémoire du conteneur PostgreSQL |
|---|---|---|---|
| 50 000 | ~110 Mo | 256 Mo | 2 Go (défaut) |
| 500 000 | ~1,1 Go | 1 Go | 4 Go |
| 5 000 000 | ~11 Go | 4 Go | 16 Go |
Réglez les paramètres de PostgreSQL dans votre fichier Compose, et augmentez POSTGRES_MEMORY en conséquence :
yaml
services:
postgres:
command: >
postgres -c shared_buffers=1GB -c effective_cache_size=3GB
-c work_mem=16MB -c max_wal_size=4GBUn max_wal_size plus grand espace les points de contrôle forcés, qui provoquent des écritures lentes ponctuelles pendant une grosse synchronisation initiale.
| Instance | Boîtes | Configuration conseillée |
|---|---|---|
| Évaluation | 1 à 5 | Le fichier Compose de référence tel quel. |
| Équipe | 10 à 50 | shared_buffers=512MB, max_wal_size=4GB, conteneur PostgreSQL à 4 Go. Sauvegarde quotidienne. |
| Grande instance | 100 et plus | PostgreSQL hors du fichier Compose (service managé ou machine dédiée), processus api et worker séparés, stockage objet externe. Gardez DATABASE_POOL_MAX × nombre de processus sous max_connections. |
DATABASE_STATEMENT_TIMEOUT_MS (30 secondes par défaut) annule toute requête qui dure plus longtemps, pour qu’une requête qui dérape ne puisse pas garder une connexion indéfiniment. Les migrations et les sauvegardes n’en héritent pas.
Redémarrages et reprise
L’instance est faite pour être arrêtée et tuée. Tout son état vit dans PostgreSQL : un redémarrage ne perd aucun travail.
Arrêt propre. À la réception de SIGTERM ou SIGINT, le processus cesse d’accepter des requêtes, arrête son planificateur, laisse les tâches qu’il tient se terminer pendant 30 secondes au plus, ferme ses connexions à la base, et écrit shutdown complete. Laissez au conteneur au moins 40 secondes pour s’arrêter : voir Le travail en cours pendant une mise à jour.
Crash ou arrêt forcé. Chaque tâche tenue par un processus a un bail de 60 secondes, renouvelé tant qu’elle tourne. Quand un processus meurt, ses baux expirent, et un processus qui fait tourner le travail de fond rend les tâches à la file (expired job leases reclaimed). Une tâche qui échoue sans cesse finit parmi les tâches mortes au lieu de tuer les processus l’un après l’autre.
Démarrage avant la base. Au démarrage, l’application attend PostgreSQL jusqu’à 60 secondes, avec un délai croissant entre les tentatives. Un mot de passe faux, un rôle inconnu ou une base inexistante arrêtent le démarrage tout de suite, avec le message du serveur : attendre n’y changerait rien. Après 60 secondes sans base, le processus s’arrête sur la dernière erreur de PostgreSQL, et Docker le redémarre.
Coupure de la base en cours de route. Un redémarrage, un crash de PostgreSQL ou une coupure réseau n’arrêtent pas l’application :
| Pendant la coupure | Au retour de la base | |
|---|---|---|
/healthz | 200 | 200 |
/readyz | 503, avec l’erreur de PostgreSQL dans detail | 200 dès la première connexion acceptée, sans redémarrer l’application |
| Requêtes HTTP qui ont besoin de la base | Échouent avec une erreur | Fonctionnent de nouveau |
| Travail de fond | claim failed et scheduler tick failed, chaque seconde | Reprend seul ; les tâches interrompues sont rendues à la file à l’expiration de leur bail |
Si /readyz reste à 503 alors que PostgreSQL accepte les connexions, lisez son detail : il pointe le plus souvent un problème d’authentification ou de migration. Un conteneur applicatif qui redémarre après chaque coupure de la base n’est pas un comportement attendu.