Метрики в Node.js: рабочие приёмы сбора без потери прод
Как снимать метрики с Node.js-сервиса через prom-client, не блокируя event loop, что именно мерить помимо CPU и почему ваш RPS-график врёт, если считать его в обработчике.

Ваш сервис на Node.js держит нагрузку, пока в один момент отвечает 504 на здоровых с виду инстансах. CPU показывает 40%, память в норме, а запросы висят. Проблема почти всегда в event loop: он заблокирован, и никакая метрика CPU этого не покажет. Разберём, какие метрики реально ловят такие ситуации, как собирать их через prom-client без просадки производительности и где типовая обвязка врёт.
Работаем с Node.js 20.x и 22.x, prom-client версии 15.x — это актуальная линейка на момент написания. Экспорт метрик — в формате Prometheus, потому что это де-факто стандарт и его понимают Grafana, VictoriaMetrics и большинство managed-решений.
Что мерить: четыре группы
Начинать со сбора «всего подряд» — верный способ утонуть в 400 рядах, из которых половина никому не нужна. Полезные метрики Node.js делятся на четыре группы, и приоритет между ними неочевиден.
- Event loop lag — задержка цикла событий. Главная метрика, которую забывают. Именно она объясняет «CPU свободен, а всё висит».
- RED по HTTP — Rate (запросы в секунду), Errors (доля 5xx), Duration (латентность по перцентилям). Базовый набор для любого HTTP-сервиса.
- Процесс и рантайм — heap, RSS, количество хендлов, работа GC. Ловят утечки и давление на сборщик мусора.
- Бизнес-метрики — то, что уникально для вашего сервиса: количество обработанных заказов, размер очереди, число активных соединений с БД.
Если из всего списка снимать только одно — снимайте event loop lag. Это единственная метрика, у которой в Node.js нет аналога в стандартном системном мониторинге, и именно она первой сигналит о синхронной блокировке.
Базовый экспорт через prom-client
Библиотека prom-client даёт реестр метрик и готовый сборщик дефолтных метрик процесса. Минимальная рабочая конфигурация выглядит так.
// Node.js 20+, prom-client 15.x, express 4.x
const express = require('express');
const client = require('prom-client');
const app = express();
const register = new client.Registry();
// Дефолтные метрики процесса: heap, RSS, GC, event loop lag
client.collectDefaultMetrics({
register,
// считать GC и lag; префикс отделяет метрики приложения
prefix: 'app_',
});
// Гистограмма длительности HTTP-запросов
const httpDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Длительность HTTP-запросов в секундах',
labelNames: ['method', 'route', 'status'],
// границы бакетов под ожидаемую латентность сервиса
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
registers: [register],
});
// Middleware: замеряем каждый запрос
app.use((req, res, next) => {
const end = httpDuration.startTimer({ method: req.method });
res.on('finish', () => {
// route берём из совпавшего маршрута, а не из req.path
const route = req.route ? req.route.path : 'unknown';
end({ route, status: res.statusCode });
});
next();
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
app.listen(3000);Ключевой момент — метка route. Если вместо совпавшего маршрута класть туда req.path, то запросы /users/1, /users/2 и /users/3 породят три разных ряда метрик. На сервисе с идентификаторами в URL это взрывает кардинальность: Prometheus начинает хранить миллионы временных рядов, и хранилище раздувается. Всегда используйте шаблон маршрута (/users/:id), а не конкретный путь.
Event loop lag: две реализации
collectDefaultMetrics уже отдаёт метрику лага, но по умолчанию она снимается грубым таймером. Для честного измерения в Node.js есть модуль perf_hooks с monitorEventLoopDelay — он использует гистограмму на уровне libuv и не искажает результат под нагрузкой.
const { monitorEventLoopDelay } = require('perf_hooks');
const client = require('prom-client');
// resolution — шаг сэмплирования в миллисекундах
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
const eventLoopP99 = new client.Gauge({
name: 'app_event_loop_lag_p99_seconds',
help: 'p99 задержки event loop за интервал',
registers: [register],
collect() {
// percentile отдаёт наносекунды -> переводим в секунды
this.set(histogram.percentile(99) / 1e9);
histogram.reset();
},
});Функция collect у Gauge вызывается в момент скрейпа /metrics, поэтому вы получаете p99 лага за интервал между скрейпами без своего таймера. Значение выше 50–100 мс под нагрузкой означает, что в цикле есть синхронная работа: JSON-парсинг гигантских тел, синхронная криптография, JSON.stringify на больших объектах.
Что ломается на практике
Скрейп метрик сам блокирует loop
Метод register.metrics() в prom-client 15.x асинхронный и возвращает промис — это не случайно. При тысячах рядов сериализация занимает заметное время. Если у вас старый код с синхронным register.metrics() без await, при большом числе меток скрейп сам создаёт лаг. Всегда await и следите за кардинальностью.
Кардинальность из пользовательских данных
Самая частая авария — метка с неограниченным множеством значений: user id, email, полный URL с query. Каждое уникальное значение метки — отдельный временной ряд. Правило простое: в метках только то, у чего конечное и небольшое число значений (метод, маршрут, код статуса, имя очереди).
Метрики в кластере и воркерах
Если сервис запущен через cluster или PM2 в режиме нескольких инстансов, каждый воркер держит свой реестр. Скрейп попадает в случайный воркер и отдаёт метрики только его. Для агрегации есть AggregatorRegistry в prom-client, но проще собирать метрики с каждого инстанса отдельно (по своему порту или через service discovery) и суммировать на стороне Prometheus.
Гистограмма без нужных бакетов
Дефолтные бакеты гистограммы рассчитаны на секунды. Если ваша латентность — единицы миллисекунд, все значения свалятся в первый бакет, и p95/p99 посчитать будет не из чего. Границы бакетов задавайте под реальный диапазон латентности сервиса.
Counter, Gauge или Histogram
Выбор типа метрики определяет, что вы сможете посчитать потом. Ошибка здесь необратима: сменить тип на живой метрике без потери истории нельзя.
| Тип | Когда | Пример | Что нельзя |
|---|---|---|---|
| Counter | Монотонно растущее значение | Всего запросов, всего ошибок | Уменьшать; хранить текущее состояние |
| Gauge | Значение, которое растёт и падает | Размер очереди, активные соединения | Считать rate — теряется смысл |
| Histogram | Распределение по бакетам | Латентность, размер ответа | Менять бакеты задним числом |
Практический критерий: если хотите посчитать «сколько в секунду» — это Counter, а частоту вы получите функцией rate() уже в Prometheus. Если нужно текущее число прямо сейчас — Gauge. Если нужны перцентили — Histogram.
Что делать дальше
После того как экспорт работает, настройте скрейп в Prometheus на эндпоинт /metrics и постройте первую панель в Grafana: p99 латентности, доля 5xx и event loop lag на одном экране. Это три графика, которые первыми покажут проблему в проде.
Следующий шаг — алерты. Порог по event loop lag и по доле ошибок 5xx закрывает большую часть инцидентов раньше, чем о них сообщат пользователи. Отдельная тема — трейсинг через OpenTelemetry: метрики скажут, что медленно, но не скажут где; здесь мы её не разворачиваем. Мерьте кардинальность своего реестра регулярно: число рядов имеет свойство расти незаметно, пока не упрётся в лимиты хранилища.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: prom-client — GitHub репозиторий, Node.js perf_hooks — monitorEventLoopDelay
Частые вопросы
У меня Node.js 18, а не 20. prom-client 15.x будет работать?
Метрики не появляются на /metrics, отдаётся пустой ответ. Что проверить?
Сбор метрик сам не замедлит сервис?
Как понять, что метка раздувает кардинальность?
Я запускаю через PM2 в кластере, метрики то есть, то нет. Почему?
Можно ли поменять бакеты гистограммы на работающем сервисе?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.