Мониторинг Node.js: Prometheus + Grafana + алерты для production
Сервер упал в 3 ночи. Пользователи не могут оплатить через Kaspi Pay. Вы открываете логи — строк миллион. Правильно настроенный мониторинг означает: алерт в Telegram приходит раньше, чем звонит клиент.
Три уровня наблюдаемости
Метрики — числа во времени: RPS, latency, error rate, CPU, memory.
Логи — события с контекстом: что произошло, с кем, когда.
Трейсы — путь запроса через сервисы: где потерялась 800мс.
Для большинства Node.js production-приложений: метрики + структурированные логи. Трейсы — при микросервисной архитектуре.
Prometheus метрики с prom-client
npm install prom-client
// src/metrics.ts
import { Counter, Histogram, Gauge, Registry, collectDefaultMetrics } from 'prom-client';
export const registry = new Registry();
// Node.js системные метрики: heap, event loop lag, GC, active handles
collectDefaultMetrics({ register: registry, prefix: 'nodejs_' });
// HTTP запросы — главная метрика API
export const httpRequestsTotal = new Counter({
name: 'http_requests_total',
help: 'Общее количество HTTP запросов',
labelNames: ['method', 'route', 'status_code'],
registers: [registry],
});
export const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Длительность HTTP запроса в секундах',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
registers: [registry],
});
// Бизнес-метрики
export const ordersCreated = new Counter({
name: 'orders_created_total',
help: 'Количество созданных заказов',
labelNames: ['store_id', 'payment_method'],
registers: [registry],
});
export const paymentErrors = new Counter({
name: 'payment_errors_total',
help: 'Ошибки при оплате',
labelNames: ['provider', 'error_type'], // kaspi, freedompay, etc.
registers: [registry],
});
export const activeConnections = new Gauge({
name: 'active_websocket_connections',
help: 'Текущее число активных WebSocket соединений',
registers: [registry],
});
Middleware для HTTP метрик
// src/middleware/metrics.ts
import { Request, Response, NextFunction } from 'express';
import { httpRequestsTotal, httpRequestDuration } from '../metrics';
export function metricsMiddleware(req: Request, res: Response, next: NextFunction) {
const start = process.hrtime.bigint();
res.on('finish', () => {
const durationSeconds = Number(process.hrtime.bigint() - start) / 1e9;
// Нормализуем маршрут — убираем динамические части
// /api/orders/123 → /api/orders/:id
const route = req.route?.path ??
req.path
.replace(/\/[0-9a-f]{8}-[0-9a-f-]{23}/gi, '/:uuid') // UUIDs
.replace(/\/\d+/g, '/:id'); // numeric IDs
const labels = {
method: req.method,
route,
status_code: res.statusCode.toString(),
};
httpRequestsTotal.inc(labels);
httpRequestDuration.observe(labels, durationSeconds);
});
next();
}
// src/app.ts
import express from 'express';
import { registry } from './metrics';
import { metricsMiddleware } from './middleware/metrics';
const app = express();
app.use(metricsMiddleware);
// Эндпоинт для Prometheus scraping
// Закрыть от публичного доступа через Nginx!
app.get('/metrics', async (req, res) => {
// Простая защита: проверяем IP или токен
const token = req.headers['x-metrics-token'];
if (token !== process.env.METRICS_TOKEN) {
return res.status(403).end();
}
res.set('Content-Type', registry.contentType);
res.end(await registry.metrics());
});
Конфигурация Prometheus + Grafana
# docker-compose.yml
services:
app:
build: .
environment:
METRICS_TOKEN: ${METRICS_TOKEN}
ports: ['3000:3000']
prometheus:
image: prom/prometheus:latest
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- ./monitoring/alerts.yml:/etc/prometheus/alerts.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle' # /reload API
ports: ['9090:9090']
grafana:
image: grafana/grafana:latest
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
GF_USERS_ALLOW_SIGN_UP: 'false'
GF_SERVER_ROOT_URL: 'https://monitoring.yourdomain.kz'
ports: ['3001:3000']
alertmanager:
image: prom/alertmanager:latest
volumes:
- ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml
ports: ['9093:9093']
volumes:
prometheus_data:
grafana_data:
# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- 'alerts.yml'
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
- job_name: 'nodejs-app'
static_configs:
- targets: ['app:3000']
metrics_path: /metrics
authorization:
credentials: ${METRICS_TOKEN}
scrape_interval: 10s
Алерты
# monitoring/alerts.yml
groups:
- name: nodejs-critical
rules:
- alert: HighErrorRate
expr: |
(
rate(http_requests_total{status_code=~"5.."}[5m])
/
rate(http_requests_total[5m])
) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "Error rate > 5% — {{ $value | humanizePercentage }}"
- alert: HighP95Latency
expr: |
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "p95 latency > 2s"
- alert: NodejsHeapCritical
expr: |
nodejs_heap_size_used_bytes / nodejs_heap_size_total_bytes > 0.90
for: 3m
labels:
severity: critical
annotations:
summary: "Node.js heap usage > 90%"
- alert: PaymentErrorsSpike
expr: rate(payment_errors_total[5m]) > 0.1
for: 1m
labels:
severity: critical
annotations:
summary: "Ошибки платёжных систем — проверить Kaspi/Freedom Pay"
Алерты в Telegram
# monitoring/alertmanager.yml
global:
resolve_timeout: 5m
route:
receiver: 'telegram'
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receivers:
- name: 'telegram'
telegram_configs:
- bot_token: '${TELEGRAM_BOT_TOKEN}'
chat_id: ${TELEGRAM_CHAT_ID}
message: |
🚨 *{{ .GroupLabels.alertname }}*
Severity: {{ .GroupLabels.severity }}
{{ range .Alerts }}
{{ .Annotations.summary }}
{{ end }}
send_resolved: true
Ключевые PromQL запросы для Grafana
Request Rate:
sum(rate(http_requests_total[5m])) by (route)
Error Rate:
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
p95 Latency по маршрутам:
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (route, le)
) > 0
Event Loop Lag (критично для Node.js):
nodejs_eventloop_lag_seconds * 1000
Event loop lag > 100мс означает синхронную блокировку — нужна оптимизация.
Heap Utilization:
nodejs_heap_size_used_bytes / nodejs_heap_size_total_bytes * 100
Структурированные логи (Pino)
npm install pino pino-pretty
// src/logger.ts
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
formatters: {
level: (label) => ({ level: label }), // 'info' вместо числа
},
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard' } }
: undefined,
});
// Использование с контекстом
logger.info({ userId, orderId, amount: total.amount }, 'Заказ создан');
logger.error({ err, orderId, provider: 'kaspi' }, 'Ошибка платежа');
logger.warn({ userId, resource: '/api/admin', ip: req.ip }, 'Отказ в доступе');
JSON-логи в production автоматически парсятся Grafana Loki или ELK Stack.
Минимальный стек за 1 час
prom-client+collectDefaultMetrics→ базовые Node.js метрики- Middleware для HTTP-метрик → RPS, latency, error rate
- Prometheus + Grafana в Docker Compose
- 4 панели в Grafana: RPS, p95 latency, error rate, heap
- Алерт на error rate > 5% → Telegram
Это даёт 80% пользы мониторинга с минимальными усилиями.
Aunimeda настраивает production-мониторинг для казахстанских проектов. Обсудим задачу.
Смотрите также: ClickHouse vs PostgreSQL аналитика, Event-driven архитектура