Сделай свой сервис наблюдаемым и корректно останавливаемым: JSON-логи, пробы liveness/readiness и обработка SIGTERM.
Открыть этот урок в KodokonВ продакшене лог предназначен для машины (Loki, Datadog, CloudWatch), а не для человека: выводи один JSON-объект на строку (одна строка = одно событие) с уровнем, ISO-меткой времени и структурированными полями - никаких свободных интерполяций, по которым невозможно делать запросы. Малоизвестная деталь: запись в stdout синхронна при выводе в файл или терминал, но асинхронна при выводе в пайп. Так что болтливый логгер может заблокировать цикл событий в зависимости от назначения - в этом весь смысл библиотек вроде pino, которые быстро сериализуют и умеют выносить запись в сторону.
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 });Различай две пробы. Liveness отвечает на вопрос "нужно ли перезапустить процесс?": она должна оставаться тривиальной, без внешних зависимостей. Readiness отвечает на вопрос "могу ли я принимать трафик?": она может проверять базу данных и, главное, переключаться на 503 во время остановки, чтобы балансировщик убрал экземпляр раньше, чем тот отключится. Путаница между ними приводит к перезапуску здоровых процессов при малейшей икоте зависимости.
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);Последовательность корректной остановки: оркестратор шлёт SIGTERM; ты переводишь readiness на 503; server.close() отказывается от новых соединений и ждёт завершения текущих запросов; closeIdleConnections() закрывает простаивающие keep-alive соединения, которые иначе держали бы close() бесконечно. Таймер, помеченный unref(), служит страховкой: он убивает процесс через 10 секунд, при этом сам по себе не мешая процессу завершиться раньше.
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;
});