Kodokon kodokon.com

Observabilidad: logs, healthcheck, graceful shutdown

Haz que tu servicio sea observable y se pueda detener limpiamente: logs en JSON, sondas de liveness/readiness y manejo de SIGTERM.

9 min · 3 preguntas

Abrir esta lección en Kodokon

En producción, un log está pensado para una máquina (Loki, Datadog, CloudWatch), no para un humano: emite un objeto JSON por línea (una línea = un evento) con un nivel, una marca de tiempo ISO y campos estructurados - nunca interpolaciones de texto libre imposibles de consultar. Un detalle poco conocido: escribir en stdout es síncrono hacia un archivo o un terminal, pero asíncrono hacia un pipe. Por eso un logger muy hablador puede bloquear el event loop según el destino - de eso se trata precisamente en bibliotecas como pino, que serializan rápido y pueden delegar la escritura.

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 estructurado mínimo, consultable tal cual.

Distingue dos sondas. Liveness responde a "¿hay que reiniciar el proceso?": debe permanecer trivial, sin ninguna dependencia externa. Readiness responde a "¿puedo recibir tráfico?": puede comprobar la base de datos y, sobre todo, cambiar a 503 durante el apagado, para que el balanceador de carga retire la instancia antes de que caiga. Confundir las dos hace que se reinicien procesos sanos en cuanto una dependencia tiene un hipo.

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 dinámico y graceful shutdown al recibir SIGTERM.

La secuencia de un graceful shutdown: el orquestador envía SIGTERM; tú pones readiness en 503; server.close() rechaza las nuevas conexiones y espera a que terminen las peticiones en curso; closeIdleConnections() cierra las conexiones keep-alive inactivas que, de lo contrario, mantendrían close() bloqueado indefinidamente. El timer marcado con unref() actúa como red de seguridad: mata el proceso al cabo de 10 segundos, sin impedir por sí mismo que el proceso salga antes.

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;
});
Último recurso: registrar y luego salir.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Cuál es la diferencia entre las sondas de liveness y readiness?
    • Ninguna: son dos nombres para la misma prueba
    • Liveness comprueba que el proceso se está ejecutando; readiness que puede aceptar tráfico, dependencias incluidas
    • Readiness reinicia el contenedor, liveness lo retira del balanceador de carga
  2. ¿Qué hace exactamente server.close()?
    • Corta inmediatamente todas las conexiones abiertas
    • Mata el proceso tras un retraso fijo de 30 segundos
    • Rechaza las nuevas conexiones y espera a que terminen las existentes
  3. ¿Qué debe hacer un handler de uncaughtException?
    • Registrar el error y luego terminar el proceso
    • Ignorar el error: Node se recupera solo
    • Reintentar automáticamente la operación fallida