О насБлогКонтакты
DevOps18 апреля 2026 г. 5 мин 2

Мониторинг Node.js: Prometheus + Grafana + алерты для production

AunimedaAunimeda
📋 Содержание

Мониторинг 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 час

  1. prom-client + collectDefaultMetrics → базовые Node.js метрики
  2. Middleware для HTTP-метрик → RPS, latency, error rate
  3. Prometheus + Grafana в Docker Compose
  4. 4 панели в Grafana: RPS, p95 latency, error rate, heap
  5. Алерт на error rate > 5% → Telegram

Это даёт 80% пользы мониторинга с минимальными усилиями.


Aunimeda настраивает production-мониторинг для казахстанских проектов. Обсудим задачу.

Смотрите также: ClickHouse vs PostgreSQL аналитика, Event-driven архитектура

Читайте также

Kubernetes networking для разработчиков: Service, Ingress, Network Policy без теорииaunimeda
DevOps

Kubernetes networking для разработчиков: Service, Ingress, Network Policy без теории

Практическое руководство по сети в Kubernetes для backend-разработчиков: ClusterIP/NodePort/LoadBalancer, Ingress с TLS, Network Policy для изоляции, HPA на кастомных метриках — с полными YAML манифестами.

OWASP Top 10 2025: безопасность веб-приложений для казахстанского разработчикаaunimeda
Разработка

OWASP Top 10 2025: безопасность веб-приложений для казахстанского разработчика

OWASP Top 10 — это стандарт критических рисков безопасности. SQL-инъекции, сломанный контроль доступа, SSRF — каждый пункт с реальной атакой на ваш Node.js/Next.js код и конкретным исправлением. Актуально для проектов на казахстанском рынке.

Node.js vs Bun vs Deno 2026: бенчмарки и выбор runtime для продакшнaunimeda
Разработка

Node.js vs Bun vs Deno 2026: бенчмарки и выбор runtime для продакшн

Bun 1.x стабилен в production. Deno 2.0 поддерживает npm-пакеты. Node.js 22 запускает TypeScript нативно. Реальные бенчмарки производительности, сравнение инструментов и конкретные рекомендации для казахстанских разработчиков.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Казахстане. Бесплатная консультация.

Получить консультацию Все статьи