Monitoring — Prometheus и Grafana

Мониторинг — сбор, хранение, анализ и визуализация данных о состоянии приложений и инфраструктуры. В типичном стеке Prometheus собирает и хранит метрики, PromQL выполняет запросы, Grafana строит дашборды, а Alertmanager маршрутизирует уведомления.

Приложения и exporters ── /metrics ──► Prometheus ──► Grafana
                                         │
                                         └────────────► Alertmanager ──► уведомления

Содержание


Наблюдаемость и метрики

Наблюдаемость показывает, насколько хорошо внутреннее состояние системы можно понять по внешним сигналам. Обычно выделяют:

Сигнал Назначение Примеры
Метрики Числовые значения во времени RPS, CPU, error rate
Логи События и контекст Исключения, действия пользователей
Трейсы Путь запроса между сервисами API, БД, очереди

Метрика состоит из имени, labels и значения:

http_requests_total{method="GET",status="200"} 18432

Для HTTP-сервисов полезна модель RED:

Для инфраструктуры применяется USE:


Типы метрик

Тип Назначение Примеры
Counter Монотонно растущий счётчик Запросы, ошибки, сообщения
Gauge Значение может расти и уменьшаться Память, температура, соединения
Histogram Распределение значений по диапазонам Задержка, размер ответа
Summary Квантили на стороне клиента Локальные квантили задержки

В большинстве приложений основными являются Counter, Gauge и Histogram.

Counter

Counter увеличивается и после перезапуска процесса начинается заново:

http_requests_total
http_request_errors_total
jobs_processed_total

Суффикс _total является общепринятым. Число активных соединений не является Counter, поскольку может уменьшаться: для него нужен Gauge.

from prometheus_client import Counter

REQUESTS = Counter(
    "http_requests_total",
    "Total number of HTTP requests",
    ["method", "route", "status"],
)

REQUESTS.labels(
    method="GET",
    route="/api/users",
    status="200",
).inc()

Абсолютное значение Counter редко полезно само по себе. Обычно используют скорость или прирост:

rate(http_requests_total[5m])
increase(http_requests_total[1h])

rate() лучше подходит для графиков и алертов. irate() реагирует быстрее, но сильнее подвержен шуму.

Gauge

Gauge хранит текущее значение:

active_connections
queue_size
process_resident_memory_bytes
temperature_celsius
from prometheus_client import Gauge

ACTIVE_CONNECTIONS = Gauge(
    "active_connections",
    "Current number of active connections",
)

ACTIVE_CONNECTIONS.inc()
ACTIVE_CONNECTIONS.dec()
ACTIVE_CONNECTIONS.set(42)

Примеры PromQL:

active_connections
avg_over_time(queue_size[1h])
max_over_time(queue_size[24h])

Histogram

Histogram распределяет наблюдения по заранее заданным диапазонам — buckets. Для http_request_duration_seconds появляются связанные ряды:

http_request_duration_seconds_bucket{le="0.1"}
http_request_duration_seconds_bucket{le="0.5"}
http_request_duration_seconds_bucket{le="1"}
http_request_duration_seconds_bucket{le="+Inf"}
http_request_duration_seconds_sum
http_request_duration_seconds_count
from prometheus_client import Histogram

REQUEST_DURATION = Histogram(
    "http_request_duration_seconds",
    "HTTP request duration in seconds",
    ["method", "route"],
    buckets=(0.05, 0.1, 0.25, 0.5, 1, 2.5, 5),
)

with REQUEST_DURATION.labels(
    method="GET",
    route="/api/users",
).time():
    load_users()

Среднее время ответа:

sum(rate(http_request_duration_seconds_sum[5m]))
/
sum(rate(http_request_duration_seconds_count[5m]))

95-й перцентиль:

histogram_quantile(
  0.95,
  sum by (le) (
    rate(http_request_duration_seconds_bucket[5m])
  )
)

При агрегации Histogram нужно сохранять label le. Для распределённых сервисов Histogram обычно удобнее Summary: buckets можно агрегировать между инстансами, а клиентские квантили Summary обычно нельзя корректно объединить.


Labels и кардинальность

Label — атрибут временного ряда:

http_requests_total{
  method="GET",
  route="/api/users",
  status="200",
  instance="app-1:8080"
} 1520

Каждая уникальная комбинация labels создаёт отдельный ряд. Если метрика имеет 5 методов, 100 маршрутов, 10 кодов и 20 инстансов, теоретический максимум — 100 000 рядов.

Не следует помещать в labels значения с высокой или неограниченной кардинальностью:

user_id
request_id
email
session_id
raw_url
error_message

Плохо:

http_requests_total{path="/users/913847/orders/44810"}

Лучше:

http_requests_total{route="/users/:user_id/orders/:order_id"}

Рекомендуемое именование:

Примеры:

shop_http_requests_total
shop_http_request_duration_seconds
shop_response_size_bytes

Prometheus

Prometheus — система мониторинга временных рядов. По умолчанию он работает по pull-модели: сам обращается к HTTP-эндпоинтам targets и сохраняет полученные данные.

Prometheus ──GET /metrics──► application
Prometheus ──GET /metrics──► node_exporter
Prometheus ──GET /metrics──► postgres_exporter

Основные компоненты:

Формат /metrics

# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 1520

# HELP active_connections Current active connections
# TYPE active_connections gauge
active_connections 27

Проверка:

curl http://localhost:8080/metrics

Минимальная конфигурация

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets:
          - prometheus:9090

  - job_name: application
    metrics_path: /metrics
    static_configs:
      - targets:
          - app:8080
Параметр Назначение
scrape_interval Частота сбора
scrape_timeout Максимальное время scrape
evaluation_interval Частота вычисления rules
job_name Имя группы targets
metrics_path Путь к метрикам
static_configs Список targets

Состояние targets проверяется в Status → Targets или запросом:

up
up{job="application"}

1 означает успешный scrape, 0 — ошибку.

Проверка конфигурации:

promtool check config /etc/prometheus/prometheus.yml
promtool check rules /etc/prometheus/rules.yml

Хранение

Prometheus хранит данные в локальной TSDB. В контейнере для этого нужен постоянный volume. Retention можно ограничить:

prometheus \
  --storage.tsdb.retention.time=30d \
  --storage.tsdb.retention.size=50GB

Для длительного хранения и объединения данных нескольких Prometheus применяют remote_write и совместимые внешние системы. При этом контролируют локальный диск, очередь отправки и доступность удалённого хранилища.

Service discovery и relabeling

В небольших окружениях достаточно static_configs. В динамических средах используются Kubernetes, Consul, обачные механизмы или file-based discovery:

scrape_configs:
  - job_name: backend
    file_sd_configs:
      - files:
          - /etc/prometheus/targets/*.json
        refresh_interval: 30s

relabel_configs изменяет labels targets до scrape. metric_relabel_configs работает после получения метрик и может удалить ненужные серии:

metric_relabel_configs:
  - source_labels: [__name__]
    regex: application_debug_event_total
    action: drop

Recording rules

Recording rule заранее вычисляет тяжёлое выражение и сохраняет его под новым именем:

groups:
  - name: application-recording
    interval: 30s
    rules:
      - record: job:http_requests:rate5m
        expr: sum by (job) (rate(http_requests_total[5m]))

Это ускоряет дашборды и позволяет одинаково использовать важный показатель в запросах и алертах.


Exporters

Exporter получает данные внешней системы и публикует их в формате Prometheus.

Exporter Назначение
Node Exporter CPU, память, диски и сеть Linux
Blackbox Exporter HTTP, HTTPS, TCP, ICMP и DNS-проверки
PostgreSQL Exporter Метрики PostgreSQL
Redis Exporter Метрики Redis
cAdvisor Ресурсы контейнеров
SNMP Exporter Сетевые устройства

Node Exporter обычно доступен на 9100:

scrape_configs:
  - job_name: node
    static_configs:
      - targets: ["server-1:9100", "server-2:9100"]

CPU:

100 - (
  avg by (instance) (
    rate(node_cpu_seconds_total{mode="idle"}[5m])
  ) * 100
)

Память:

100 * (
  1 - node_memory_MemAvailable_bytes
  / node_memory_MemTotal_bytes
)

Blackbox Exporter использует /probe, а основная метрика результата — probe_success:

probe_success

Значение 1 означает успех, 0 — ошибку.

Exporter и /metrics не следует публиковать в интернет без необходимости. Используйте внутреннюю сеть, firewall, TLS, аутентификацию и минимальные права.


PromQL

PromQL фильтрует, агрегирует и преобразует временные ряды.

Выбор и фильтрация

http_requests_total
http_requests_total{method="GET"}
http_requests_total{status=~"5.."}
http_requests_total{endpoint!~"/health|/ready"}

Операторы labels: =, !=, =~, !~.

Скорость и прирост

rate(http_requests_total[5m])
increase(http_requests_total[1h])

Общий RPS:

sum(rate(http_requests_total[5m]))

RPS по сервисам:

sum by (service) (
  rate(http_requests_total[5m])
)

Агрегация

sum by (status) (rate(http_requests_total[5m]))
sum without (instance, pod) (rate(http_requests_total[5m]))
topk(5, process_resident_memory_bytes)

Процент ошибок

100 *
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))

По сервисам:

100 * sum by (service) (
  rate(http_requests_total{status=~"5.."}[5m])
)
/ sum by (service) (
  rate(http_requests_total[5m])
)

Histogram

Среднее время ответа:

sum(rate(http_request_duration_seconds_sum[5m]))
/ sum(rate(http_request_duration_seconds_count[5m]))

p95:

histogram_quantile(
  0.95,
  sum by (le) (
    rate(http_request_duration_seconds_bucket[5m])
  )
)

Другие операции

max_over_time(queue_size[1h])
delta(queue_size[15m])
absent(up{job="application"})
up == 0

Сравнение с прошлой неделей:

sum(rate(http_requests_total[5m]))
/ sum(rate(http_requests_total[5m] offset 7d))

В Grafana используйте $__rate_interval, чтобы диапазон был согласован с масштабом графика:

sum(rate(http_requests_total[$__rate_interval]))

Grafana

Grafana — платформа визуализации и анализа данных.

Основные сущности:

Подключение Prometheus

Connections → Data sources → Add data source → Prometheus

Если сервисы находятся в одной Docker-сети, URL обычно такой:

http://prometheus:9090

localhost внутри контейнера Grafana означает сам контейнер Grafana, а не соседний контейнер Prometheus.

Панели и дашборды

Типовая структура дашборда приложения:

Обзор: доступность, RPS, error rate, p95 latency
Приложение: маршруты, соединения, очереди, зависимости
Ресурсы: CPU, память, диск, сеть

Основные типы панелей:

Панель Назначение
Time series Изменения во времени
Stat Одно значение
Gauge Значение относительно диапазона
Table Значения и labels
Heatmap Распределение наблюдений

RPS:

sum(rate(http_requests_total[$__rate_interval]))

p95:

histogram_quantile(
  0.95,
  sum by (le) (
    rate(http_request_duration_seconds_bucket[$__rate_interval])
  )
)

Настраивайте единицы: секунды для _seconds, байты для _bytes, requests/sec для скорости и проценты для значений, умноженных на 100.

Переменная окружения:

label_values(up, environment)

Использование:

sum by (status) (
  rate(http_requests_total{
    environment="$environment"
  }[$__rate_interval])
)

Annotations удобно использовать для деплоев и изменений конфигурации. Provisioning позволяет хранить data sources и dashboards как код, загружать их автоматически и проверять в Git.


Алертинг и Alertmanager

Prometheus вычисляет alerting rules, а Alertmanager группирует, подавляет и отправляет уведомления.

Prometheus → Alertmanager → Email / Slack / webhook / on-call-система

Alerting rule

groups:
  - name: application-alerts
    rules:
      - alert: ApplicationTargetDown
        expr: up{job="application"} == 0
        for: 2m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "Application target is unavailable"
          description: >-
            Target {{ $labels.instance }} is unavailable for more than 2 minutes.
          runbook_url: "https://docs.example.com/runbooks/application-down"

      - alert: HighHttpErrorRate
        expr: |
          sum by (service) (
            rate(http_requests_total{status=~"5.."}[5m])
          )
          /
          sum by (service) (
            rate(http_requests_total[5m])
          ) > 0.05
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "High HTTP error rate"
          description: "More than 5% of requests return 5xx."

Состояния:

Inactive → Pending → Firing

for уменьшает шум от кратких всплесков, но не должен задерживать критические уведомления.

Alertmanager

global:
  resolve_timeout: 5m

route:
  receiver: default-webhook
  group_by: [alertname, service, environment]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers: [severity="critical"]
      receiver: critical-webhook
      repeat_interval: 30m

receivers:
  - name: default-webhook
    webhook_configs:
      - url: http://notification-service:8080/alerts
        send_resolved: true

  - name: critical-webhook
    webhook_configs:
      - url: http://incident-service:8080/critical-alerts
        send_resolved: true

Хороший алерт содержит событие, сервис, окружение, severity, длительность, ссылку на дашборд и runbook. Алерт должен требовать конкретного действия и не быть постоянным фоном.


Практический пример

Структура:

monitoring/
├── compose.yml
├── prometheus/prometheus.yml
├── prometheus/rules/alerts.yml
├── alertmanager/alertmanager.yml
└── grafana/provisioning/datasources/prometheus.yml

compose.yml

services:
  prometheus:
    image: prom/prometheus:<version>
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.retention.time=15d
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./prometheus/rules:/etc/prometheus/rules:ro
      - prometheus-data:/prometheus
    ports:
      - "127.0.0.1:9090:9090"
    networks: [monitoring]

  alertmanager:
    image: prom/alertmanager:<version>
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
    ports:
      - "127.0.0.1:9093:9093"
    networks: [monitoring]

  grafana:
    image: grafana/grafana:<version>
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
    ports:
      - "127.0.0.1:3000:3000"
    networks: [monitoring]

  node-exporter:
    image: prom/node-exporter:<version>
    command: ["--path.rootfs=/host"]
    volumes:
      - /:/host:ro,rslave
    networks: [monitoring]

volumes:
  prometheus-data:
  grafana-data:

networks:
  monitoring:

В реальном проекте <version> заменяется фиксированной проверенной версией. Для внешнего доступа используйте reverse proxy с HTTPS и аутентификацией. Не публикуйте Prometheus и exporters напрямую в интернет.

prometheus/prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - /etc/prometheus/rules/*.yml

alerting:
  alertmanagers:
    - static_configs:
        - targets: [alertmanager:9093]

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: [prometheus:9090]

  - job_name: node
    static_configs:
      - targets: [node-exporter:9100]

Запуск и проверка:

docker compose config
docker compose up -d
docker compose ps
docker compose logs -f prometheus

Адреса при локальном запуске:

Prometheus:   http://localhost:9090
Alertmanager: http://localhost:9093
Grafana:      http://localhost:3000

Best practices

Полезные метрики самого Prometheus:

prometheus_tsdb_head_series
rate(prometheus_rule_evaluation_failures_total[5m])
prometheus_rule_group_last_duration_seconds

Чек-лист

Метрики

Prometheus

Grafana

Алертинг


Шпаргалка PromQL

# Доступность
up
up == 0

# Общий RPS
sum(rate(http_requests_total[5m]))

# Запросы за час
sum(increase(http_requests_total[1h]))

# Ошибки 5xx, проценты
100 * sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))

# Средняя задержка
sum(rate(http_request_duration_seconds_sum[5m]))
/ sum(rate(http_request_duration_seconds_count[5m]))

# p95
histogram_quantile(0.95,
  sum by (le) (
    rate(http_request_duration_seconds_bucket[5m])
  )
)

# CPU
100 - avg by (instance) (
  rate(node_cpu_seconds_total{mode="idle"}[5m])
) * 100

# Память
100 * (1 - node_memory_MemAvailable_bytes
  / node_memory_MemTotal_bytes)

# Максимальная очередь за час
max_over_time(queue_size[1h])

# Отсутствующая метрика
absent(up{job="application"})

Итог

Prometheus собирает и хранит временные ряды, PromQL превращает их в полезные показатели, Grafana визуализирует данные, а Alertmanager доставляет уведомления. Хороший мониторинг отвечает на практические вопросы: доступен ли сервис, как быстро он отвечает, сколько ошибок возникает, какая зависимость ограничивает систему и требуется ли вмешательство человека.