Kodokon kodokon.com

Observabilité : logs, healthcheck, arrêt propre

Rendez votre service observable et arrêtable proprement : logs JSON, sondes liveness/readiness et gestion de SIGTERM.

9 min · 3 questions

Ouvrir cette leçon dans Kodokon

En production, un log est destiné à une machine (Loki, Datadog, CloudWatch), pas à un humain : émettez du JSON par ligne (une ligne = un événement) avec un niveau, un horodatage ISO et des champs structurés - jamais d'interpolations libres impossibles à requêter. Détail méconnu : l'écriture sur stdout est synchrone vers un fichier ou un terminal, mais asynchrone vers un pipe. Un logger bavard peut donc bloquer l'event loop selon la destination - c'est la raison d'être de bibliothèques comme pino, qui sérialisent vite et peuvent déporter l'écriture.

JAVASCRIPT
function log(level, message, fields = {}) {
  const entry = {
    level,
    message,
    time: new Date().toISOString(),
    pid: process.pid,
    ...fields,
  };
  process.stdout.write(JSON.stringify(entry) + "\n");
}

log("info", "server started", { port: 3000 });
log("error", "db unreachable", { retryInMs: 5000 });
Un logger structuré minimal, requêtable tel quel.

Distinguez deux sondes. La liveness répond à « le processus doit-il être redémarré ? » : elle doit rester triviale, sans dépendance externe. La readiness répond à « puis-je recevoir du trafic ? » : elle peut vérifier la base de données, et surtout passer à 503 pendant l'arrêt, pour que le load balancer retire l'instance avant la coupure. Confondre les deux fait redémarrer des processus sains dès qu'une dépendance tousse.

JAVASCRIPT
import { createServer } from "node:http";

let ready = true;

const server = createServer((req, res) => {
  if (req.url === "/healthz") {
    res.writeHead(ready ? 200 : 503);
    res.end(ready ? "ok" : "draining");
    return;
  }
  res.end("hello");
});

server.listen(3000);

function shutdown() {
  ready = false;
  server.closeIdleConnections();
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(1), 10_000).unref();
}

process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
Readiness dynamique et arrêt propre sur SIGTERM.

Le déroulé d'un arrêt propre : l'orchestrateur envoie SIGTERM ; vous basculez la readiness à 503 ; server.close() refuse les nouvelles connexions et attend la fin des requêtes en cours ; closeIdleConnections() ferme les connexions keep-alive inactives qui, sinon, retiendraient close() indéfiniment. Le timer marqué unref() sert de filet de sécurité : il tue le processus après 10 secondes, sans empêcher à lui seul le processus de se terminer plus tôt.

JAVASCRIPT
process.on("uncaughtException", (err) => {
  const entry = {
    level: "fatal",
    message: err.message,
    stack: err.stack,
    time: new Date().toISOString(),
  };
  process.stderr.write(JSON.stringify(entry) + "\n");
  process.exit(1);
});

process.on("unhandledRejection", (reason) => {
  throw reason;
});
Dernier recours : journaliser puis sortir.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Quelle est la différence entre les sondes liveness et readiness ?
    • Aucune : ce sont deux noms pour le même test
    • La liveness vérifie que le processus tourne ; la readiness qu'il peut accepter du trafic, dépendances comprises
    • La readiness redémarre le conteneur, la liveness le retire du load balancer
  2. Que fait exactement server.close() ?
    • Il coupe immédiatement toutes les connexions ouvertes
    • Il tue le processus après un délai fixe de 30 secondes
    • Il refuse les nouvelles connexions et attend la fin des connexions existantes
  3. Que doit faire un gestionnaire uncaughtException ?
    • Journaliser l'erreur puis terminer le processus
    • Ignorer l'erreur : Node récupère de lui-même
    • Relancer automatiquement l'opération échouée