Пошук уроків, статей та іншого контенту
Визначте ключові метрики застосунку, HTTP-запитів і runtime та підготуйте їх для спостереження й алертингу.
Метрики показують стан застосунку числовими значеннями. На відміну від логів, вони зручні для побудови графіків, порівняння періодів і автоматичних сповіщень.
Для Node.js-застосунку зазвичай потрібні три групи метрик:
метрики застосунку — кількість замовлень, помилок, активних операцій;
метрики HTTP — кількість запитів, коди відповідей і тривалість обробки;
метрики runtime — використання пам’яті, CPU, кількість відкритих ресурсів і затримка event loop.
Метрика повинна допомагати відповісти на конкретне запитання. Наприклад:
Скільки запитів завершується помилкою?
Який час відповіді для 95 % запитів?
Чи перевищує затримка event loop допустиме значення?
Чи збільшується використання пам’яті протягом тривалого часу?
У системах моніторингу, сумісних із Prometheus, найчастіше використовують такі типи.
Counter — лічильник, який тільки збільшується. Він підходить для підрахунку подій:
кількості HTTP-запитів;
кількості помилок;
кількості створених замовлень.
Значення лічильника зазвичай аналізують через швидкість зміни за певний період:
rate(app_http_requests_total[5m])Після перезапуску процесу значення Counter починається знову. Це нормально: для аналізу використовують rate, який враховує скидання лічильника.
Gauge може збільшуватися і зменшуватися. Він підходить для поточного стану:
кількість активних запитів;
використання пам’яті;
розмір черги;
затримка event loop.
Histogram групує значення за діапазонами. Його використовують для тривалості запитів, розміру відповідей або інших розподілів.
Для HTTP-запитів важливо знати не тільки середнє значення, а й перцентилі:
p50 — медіанний час відповіді;
p95 — час, швидший за який завершуються 95 % запитів;
p99 — час, швидший за який завершуються 99 % запитів.
Середнє значення може приховувати невелику кількість дуже повільних запитів, тому для алертів частіше використовують p95 або p99.
Мінімальний набір HTTP-метрик:
загальна кількість запитів;
кількість запитів за методом, маршрутом і кодом відповіді;
тривалість обробки запитів;
кількість активних запитів.
Для групування запитів використовуйте шаблон маршруту, а не фактичний URL.
Правильно:
/api/users/:idНеправильно:
/api/users/123
/api/users/456
/api/users/789Фактичний ідентифікатор у label створює окремий часовий ряд для кожного значення. Це називається високою кардинальністю і може швидко перевантажити систему моніторингу.
Зазвичай labels для HTTP-метрик мають такий вигляд:
method;
route;
status_code.
Не додавайте до labels:
ідентифікатор користувача;
email;
повний URL із query-параметрами;
текст помилки;
IP-адресу, якщо кількість адрес не обмежена.
Окрім власних метрик, застосунок повинен експортувати показники процесу Node.js:
використання heap і зовнішньої пам’яті;
використання CPU;
тривалість роботи процесу;
кількість відкритих handles;
затримку event loop;
кількість HTTP-запитів і сокетів на рівні Node.js, якщо їх надає бібліотека метрик.
Важливий показник для Node.js — event loop lag. Node.js виконує JavaScript-код в event loop. Якщо синхронна операція блокує його, обробка інших подій затримується.
Висока затримка event loop може бути спричинена:
великим синхронним обчисленням;
синхронною роботою з файловою системою;
великим JSON-серіалізуванням або парсингом;
неочікуваним блокуванням у коді.
Використання CPU саме по собі не показує, чи заблокований event loop. Тому ці показники потрібно розглядати разом.
Зручно використовувати пакет prom-client, який:
створює Counter, Gauge і Histogram;
збирає стандартні метрики Node.js;
форматує результати у форматі Prometheus;
надає endpoint для збору метрик.
Встановлення залежності:
npm install prom-clientНижче наведено повний приклад застосунку на вбудованому модулі http. Він має:
endpoint /metrics;
HTTP-метрики;
метрику активних запитів;
метрику тривалості запитів;
лічильник помилок застосунку;
затримку event loop;
стандартні runtime-метрики Node.js.
const http = require('node:http');
const { monitorEventLoopDelay } = require('node:perf_hooks');
const client = require('prom-client');
const register = new client.Registry();
client.collectDefaultMetrics({
register,
prefix: 'nodejs_',
});
const httpRequestsTotal = new client.Counter({
name: 'app_http_requests_total',
help: 'Загальна кількість HTTP-запитів застосунку',
labelNames: ['method', 'route', 'status_code'],
registers: [register],
});
const httpRequestDuration = new client.Histogram({
name: 'app_http_request_duration_seconds',
help: 'Тривалість обробки HTTP-запитів у секундах',
labelNames: ['method', 'route'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 5],
registers: [register],
});
const httpRequestsInProgress = new client.Gauge({
name: 'app_http_requests_in_progress',
help: 'Кількість HTTP-запитів, які обробляються зараз',
registers: [register],
});
const appErrorsTotal = new client.Counter({
name: 'app_errors_total',
help: 'Кількість непередбачених помилок застосунку',
labelNames: ['type'],
registers: [register],
});
const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
eventLoopDelay.enable();
const eventLoopLagMs = new client.Gauge({
name: 'nodejs_event_loop_lag_p99_milliseconds',
help: '99-й перцентиль затримки event loop у мілісекундах',
registers: [register],
collect() {
const value = eventLoopDelay.percentile(99) / 1e6;
if (Number.isFinite(value)) {
this.set(value);
}
},
});
function getRoute(pathname) {
if (pathname === '/api/orders') {
return 'GET /api/orders';
}
return 'unknown';
}
function sendJson(res, statusCode, body) {
res.statusCode = statusCode;
res.setHeader('content-type', 'application/json; charset=utf-8');
res.end(JSON.stringify(body));
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, 'http://localhost');
const pathname = url.pathname;
// Endpoint метрик не включаємо в бізнес-метрики HTTP-запитів.
if (pathname === '/metrics') {
res.statusCode = 200;
res.setHeader('content-type', register.contentType);
res.end(await register.metrics());
return;
}
const route = getRoute(pathname);
const method = req.method || 'GET';
const endTimer = httpRequestDuration.startTimer({ method, route });
httpRequestsInProgress.inc();
try {
if (method === 'GET' && pathname === '/api/orders') {
// Імітуємо асинхронну операцію, наприклад звернення до бази даних.
await new Promise((resolve) => setTimeout(resolve, 40));
sendJson(res, 200, {
orders: [
{ id: 1, status: 'paid' },
{ id: 2, status: 'pending' },
],
});
} else {
sendJson(res, 404, { error: 'Not found' });
}
} catch (error) {
appErrorsTotal.inc({ type: 'unhandled' });
if (!res.headersSent) {
sendJson(res, 500, { error: 'Internal server error' });
} else {
res.destroy();
}
} finally {
httpRequestsTotal.inc({
method,
route,
status_code: String(res.statusCode),
});
endTimer();
httpRequestsInProgress.dec();
}
});
server.listen(3000, () => {
console.log('Сервер запущено на http://localhost:3000');
});Запустіть застосунок:
node server.jsВиконайте кілька запитів:
curl http://localhost:3000/api/orders
curl http://localhost:3000/not-found
curl http://localhost:3000/metricsEndpoint /metrics повертає текст, який система моніторингу може періодично збирати. Серед результатів будуть власні метрики з префіксом app_ і стандартні метрики з префіксом nodejs_.
Вимірюйте час від початку обробки запиту до моменту, коли застосунок сформував відповідь.
Для цього використовується Histogram. Важливо:
запускати таймер на початку обробки;
завершувати його в усіх гілках виконання;
фіксувати код відповіді;
використовувати однакові buckets для порівняння сервісів.
Buckets потрібно підбирати під очікувану поведінку сервісу. Для API, яке зазвичай відповідає за десятки або сотні мілісекунд, корисні значення від кількох мілісекунд до кількох секунд.
Якщо всі запити потрапляють у найбільший bucket, він підібраний неправильно або застосунок працює повільніше, ніж очікувалося.
HTTP-метрики показують стан транспорту і сервера, але не пояснюють, чи виконує застосунок свою основну роботу.
Додавайте метрики для важливих подій домену:
const ordersCreatedTotal = new client.Counter({
name: 'app_orders_created_total',
help: 'Кількість створених замовлень',
registers: [register],
});
// Після успішного створення замовлення:
// ordersCreatedTotal.inc();Для бізнес-метрик потрібно дотримуватися тих самих правил:
назва повинна описувати подію або стан;
лічильник подій має бути Counter;
поточне значення — Gauge;
не використовуйте у labels необмежені значення;
збільшуйте лічильник тільки після успішного завершення операції.
Наприклад, статус замовлення може бути допустимим label, якщо набір статусів обмежений:
app_orders_current{status="paid"}
app_orders_current{status="pending"}
app_orders_current{status="cancelled"}Не варто використовувати як label довільний текст помилки або ідентифікатор замовлення.
Метрика сама по собі не є алертом. Алерт — це правило, яке визначає, коли значення стало небезпечним.
Кількість помилок потрібно порівнювати із загальною кількістю запитів. Саме абсолютне число помилок не завжди показове: 100 помилок на мільйон запитів і 100 помилок на 100 запитів — різні ситуації.
Приклад PromQL для частки відповідей із кодами 5xx:
sum(rate(app_http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(app_http_requests_total[5m]))
> 0.05Таке правило спрацьовує, якщо за останні 5 хвилин частка помилок перевищує 5 %.
Для production-системи поріг і тривалість потрібно підбирати з урахуванням нормальної поведінки застосунку. Не створюйте алерт, який спрацьовує через одну випадкову помилку.
Для histogram Prometheus створює серії _bucket, _sum і _count. Приблизний запит для оцінки p95:
histogram_quantile(
0.95,
sum by (le) (
rate(app_http_request_duration_seconds_bucket[5m])
)
) > 0.5Це правило перевіряє, чи перевищує 95-й перцентиль 500 мілісекунд.
Для окремого маршруту можна додати фільтр:
histogram_quantile(
0.95,
sum by (le) (
rate(app_http_request_duration_seconds_bucket{route="GET /api/orders"}[5m])
)
) > 0.5Приклад перевірки високої затримки:
nodejs_event_loop_lag_p99_milliseconds > 100Одного короткого перевищення може бути недостатньо для сповіщення. Зазвичай умову утримують протягом певного часу, щоб відрізнити системну проблему від короткого стрибка навантаження.
Поріг повинен ґрунтуватися на очікуваній поведінці системи:
Спочатку визначте нормальний діапазон метрики.
Перевірте поведінку під типовим і піковим навантаженням.
Встановіть поріг вище звичайного рівня.
Додайте мінімальний обсяг трафіку, якщо це необхідно.
Перевірте, чи алерт приводить до конкретної дії.
Наприклад, алерт про частку помилок без достатньої кількості запитів може бути нестабільним. Одна помилка під час одного запиту дасть 100 % помилок, але це не обов’язково інцидент.
Корисний алерт повинен відповідати на питання:
Що саме зламалося?
Наскільки це терміново?
Яку дію має виконати команда?
Який графік або log допоможе знайти причину?
Не використовуйте user_id, request_id або повний URL як labels. Кожне унікальне значення створює новий часовий ряд.
Для пошуку конкретного запиту використовуйте логи або трасування, а метрики залишайте агрегованими.
Середній час відповіді може виглядати нормальним, навіть якщо частина користувачів регулярно отримує дуже повільні відповіді. Додавайте histogram і контролюйте p95 або p99.
Counter не повинен зменшуватися. Для поточної кількості активних операцій використовуйте Gauge.
Групування тільки за методом і кодом відповіді приховує проблемний endpoint. Додавайте нормалізований маршрут, але не фактичний URL із динамічними ідентифікаторами.
/metrics разом із бізнес-трафікомСистема моніторингу регулярно звертається до /metrics. Якщо включити ці запити до загальних HTTP-метрик, вони можуть спотворити статистику. У прикладі endpoint метрик обробляється окремо.
Значення Counter скидається після перезапуску процесу. Для алертів використовуйте rate або increase, а не саме поточне значення лічильника.
Кожна метрика збільшує обсяг даних і складність моніторингу. Додавайте її, якщо заздалегідь зрозуміло, яке рішення допоможе прийняти це значення.
Counter використовуйте для подій, Gauge — для поточного стану, Histogram — для розподілу значень.
Для HTTP вимірюйте кількість запитів, коди відповідей, тривалість і кількість активних запитів.
У labels використовуйте тільки значення з обмеженою кількістю варіантів.
Додавайте runtime-метрики Node.js і контролюйте затримку event loop.
Для часу відповіді аналізуйте p95 і p99, а не лише середнє.
Алерти будуються на швидкості зміни лічильників, частках помилок і порогах тривалості.
Метрики повинні вести до конкретних дій під час діагностики проблеми.