Haz que tu servicio sea observable y se pueda detener limpiamente: logs en JSON, sondas de liveness/readiness y manejo de SIGTERM.
Abrir esta lección en KodokonEn 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.
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 });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.
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);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.
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;
});