Мониторинг Node.js в production: Prometheus, Grafana и OpenTelemetry
Приложение упало в 3 ночи. Пользователи пишут в поддержку. Вы открываете логи — строк миллион, непонятно что случилось и когда началось. Правильный мониторинг означает что вы узнаёте о проблеме за 2 минуты до того как её заметят пользователи.
Три компонента наблюдаемости
Метрики — числа во времени: RPS, latency, error rate, использование памяти.
Логи — событийная история: что произошло и когда.
Трейсы — путь конкретного запроса через все сервисы.
Для большинства Node.js приложений достаточно метрик + структурированных логов. Трейсы нужны при микросервисной архитектуре.
Prometheus метрики в Express
npm install prom-client
// src/metrics.ts
import { Counter, Histogram, Gauge, Registry, collectDefaultMetrics } from 'prom-client';
export const registry = new Registry();
// Системные метрики Node.js: CPU, memory, event loop lag
collectDefaultMetrics({ register: registry });
// HTTP запросы
export const httpRequestsTotal = new Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status_code'],
registers: [registry],
});
export const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5], // 10мс, 50мс, 100мс...
registers: [registry],
});
// Бизнес-метрики
export const activeOrders = new Gauge({
name: 'active_orders_total',
help: 'Number of orders currently processing',
registers: [registry],
});
export const orderProcessingErrors = new Counter({
name: 'order_processing_errors_total',
help: 'Number of order processing errors',
labelNames: ['error_type'],
registers: [registry],
});
// 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 duration = Number(process.hrtime.bigint() - start) / 1e9;
// Нормализуем маршрут — заменяем динамические части
const route = req.route?.path ?? req.path.replace(/\/\d+/g, '/:id');
httpRequestsTotal.inc({
method: req.method,
route,
status_code: res.statusCode,
});
httpRequestDuration.observe(
{ method: req.method, route, status_code: res.statusCode },
duration
);
});
next();
}
// src/app.ts
import express from 'express';
import { registry } from './metrics';
import { metricsMiddleware } from './middleware/metrics';
const app = express();
app.use(metricsMiddleware);
// Эндпоинт для Prometheus
app.get('/metrics', async (req, res) => {
res.set('Content-Type', registry.contentType);
res.end(await registry.metrics());
});
Конфигурация Prometheus
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'nodejs-app'
static_configs:
- targets: ['app:3000'] # или IP вашего сервера
metrics_path: /metrics
scrape_interval: 10s
- job_name: 'node-exporter' # системные метрики сервера
static_configs:
- targets: ['node-exporter:9100']
# docker-compose.yml
services:
app:
build: .
ports: ['3000:3000']
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
ports: ['9090:9090']
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=15d'
grafana:
image: grafana/grafana:latest
volumes:
- grafana_data:/var/lib/grafana
ports: ['3001:3000']
environment:
GF_SECURITY_ADMIN_PASSWORD: 'your-secure-password'
GF_USERS_ALLOW_SIGN_UP: 'false'
node-exporter:
image: prom/node-exporter:latest
network_mode: host
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
volumes:
prometheus_data:
grafana_data:
Grafana: ключевые дашборды
После подключения Prometheus как источника данных — эти запросы дают главную картину:
Request Rate (RPS):
rate(http_requests_total[5m])
Latency p95 по маршрутам:
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])
) by (route)
Error Rate:
rate(http_requests_total{status_code=~"5.."}[5m])
/
rate(http_requests_total[5m])
Использование памяти Node.js:
nodejs_heap_size_used_bytes / nodejs_heap_size_total_bytes
Event Loop Lag (важно для Node.js):
nodejs_eventloop_lag_seconds
Event loop lag > 100мс — ваш сервер перегружен синхронной работой.
Алерты в Prometheus
# alerts.yml
groups:
- name: nodejs-app
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% for 2 minutes"
- alert: HighLatency
expr: |
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "p95 latency > 2s"
- alert: HighMemoryUsage
expr: nodejs_heap_size_used_bytes / nodejs_heap_size_total_bytes > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "Node.js heap > 85%"
Структурированные логи
npm install pino pino-pretty
// src/logger.ts
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
// В production — JSON, парсится Grafana Loki / ELK
});
// Использование
logger.info({ userId, orderId, amount }, 'Order created');
logger.error({ err, orderId }, 'Payment processing failed');
JSON-логи автоматически парсятся Grafana Loki или ELK Stack.
OpenTelemetry для трейсинга
Если у вас несколько сервисов — трейсинг показывает где именно тратится время:
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-jaeger
// src/tracing.ts (подключается ДО всего остального)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
const sdk = new NodeSDK({
serviceName: 'aunimeda-api',
traceExporter: new JaegerExporter({
endpoint: process.env.JAEGER_ENDPOINT ?? 'http://jaeger:14268/api/traces',
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': { enabled: true },
'@opentelemetry/instrumentation-express': { enabled: true },
'@opentelemetry/instrumentation-pg': { enabled: true }, // PostgreSQL
}),
],
});
sdk.start();
// package.json
"scripts": {
"start": "node --require ./dist/tracing.js dist/app.js"
}
Автоинструментация добавляет трейсы для HTTP, Express маршрутов и PostgreSQL запросов без изменения бизнес-кода.
Мониторинг в production за 1 час
Минимальный набор который даст 80% пользы:
prom-clientсcollectDefaultMetrics()→ метрики Node.js- Middleware для HTTP метрик → RPS, latency, error rate
- Prometheus scraping каждые 15 секунд
- Grafana дашборд с 4 панелями: RPS, p95 latency, error rate, heap memory
- Алерт на error rate > 5% → Telegram/Slack уведомление
Aunimeda настраивает production-мониторинг для веб-приложений в Кыргызстане. Обсудим проект.
Смотрите также: Docker Node.js production deploy, PostgreSQL оптимизация на VPS