Legan Studio
Все статьи
~ 7 мин чтения

Prometheus и Grafana для бота MAX: метрики, дашборды, алерты

Полный стек observability для бота MAX: метрики приложения, Prometheus + node_exporter + postgres_exporter, дашборды Grafana, SLO и алерты в Alertmanager.

  • MAX
  • observability
  • DevOps

Без метрик и дашборда состояние бота — это «вроде работает, пользователи не жалуются». Реальность — на пике 18:00 webhook отвечает 12 секунд вместо 200 мс, 30% запросов падают на timeout, и вы узнаёте об этом утром по жалобам в чат поддержки. В этой статье — полный стек observability для бота в MAX: какие метрики собирать в приложении, как развернуть Prometheus + Grafana + Alertmanager, какие дашборды нужны, какие алерты обязательны и как сформулировать SLO так, чтобы алерт срабатывал не на каждый чих, а только когда реально пора чинить.

Три кита observability

  • Метрики (Prometheus) — числа во времени, агрегаты. Дешёвы и быстры, но не показывают «что именно сломалось у пользователя 12345».
  • Логи (Loki / ELK) — детализация конкретных событий. Дороги в хранении, но без них не разобрать инцидент.
  • Трейсы (Tempo / Jaeger) — связь между сервисами в одном запросе. Незаменимы при сложных пайплайнах LLM → CRM → MAX API.

Эта статья — про метрики. Логи и трейсы — отдельные темы.

Минимальный набор метрик в коде бота

from prometheus_client import Counter, Histogram, Gauge

webhook_requests = Counter(
    "bot_webhook_requests_total",
    "Total webhook requests",
    ["status"],            # 200, 401, 500, ...
)
webhook_duration = Histogram(
    "bot_webhook_duration_seconds",
    "Webhook handler duration",
    buckets=(0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10),
)
handler_duration = Histogram(
    "bot_handler_duration_seconds",
    "Per-handler duration",
    ["handler"],
)
max_api_calls = Counter(
    "bot_max_api_calls_total",
    "Calls to MAX Bot API",
    ["method", "status"],
)
llm_tokens = Counter(
    "bot_llm_tokens_total",
    "LLM tokens consumed",
    ["model", "type"],     # type=in|out
)
llm_latency = Histogram(
    "bot_llm_latency_seconds",
    "LLM call latency",
    ["model"],
)
active_dialogs = Gauge(
    "bot_active_dialogs",
    "Currently active dialogs",
)
queue_depth = Gauge(
    "bot_queue_depth",
    "Background queue depth",
    ["queue"],
)

Эндпоинт /metrics

from prometheus_client import make_asgi_app
from fastapi import FastAPI

app = FastAPI()
app.mount("/metrics", make_asgi_app())

@app.middleware("http")
async def metrics_mw(request, call_next):
    if request.url.path == "/max/webhook":
        with webhook_duration.time():
            response = await call_next(request)
        webhook_requests.labels(status=str(response.status_code)).inc()
        return response
    return await call_next(request)

Защитите /metrics либо basic auth в nginx, либо доступ только из приватной сети (NetworkPolicy / VPC). Метрики — лакомый источник информации о вашей инфре.

Prometheus: установка и конфиг

docker-compose.yml:

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prom-data:/prometheus
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.retention.time=90d
      - --web.enable-lifecycle
    ports: ["9090:9090"]

prometheus.yml:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: bot-app
    static_configs:
      - targets: ["bot:8080"]
    metrics_path: /metrics

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

  - job_name: postgres
    static_configs:
      - targets: ["postgres-exporter:9187"]

  - job_name: redis
    static_configs:
      - targets: ["redis-exporter:9121"]

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

rule_files:
  - rules/*.yml

Retention 90 дней — хватает для разбора большинства инцидентов и week-over-week сравнений. Для долгосрочного — Thanos или VictoriaMetrics.

Обязательные exporters

ExporterЧто мониторит
node_exporterCPU, RAM, диск, сеть, load average хоста
postgres_exporterсоединения, размер таблиц, IOPS, replication lag
redis_exporterops/sec, hit rate, память, eviction
nginx-prometheus-exporterrequests, response time, статус-коды
blackbox_exporterHTTPS endpoint health snimu (внешний пинг)
cAdvisorметрики Docker-контейнеров

postgres_exporter особенно важен: он покажет slow queries, pg_stat_statements, размер WAL, replication lag standby. Половина инцидентов «бот тормозит» — это медленные запросы в Postgres.

Grafana: 4 обязательных дашборда

1. Overview / SLO. RPS, error rate, latency p50/p95/p99 за последний час, день, неделю. Один взгляд — понятно, всё ли в порядке.

# RPS webhook
sum(rate(bot_webhook_requests_total[5m]))

# Error rate
sum(rate(bot_webhook_requests_total{status=~"5.."}[5m]))
  /
sum(rate(bot_webhook_requests_total[5m]))

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

2. Application internals. Per-handler duration, длина очередей, количество активных диалогов, эскалации к оператору, ошибки FSM, частота вызовов CRM.

3. Infrastructure. CPU/RAM/disk/network по хостам и подам, диск Postgres, replication lag, hit rate Redis, размер очередей в Object Storage.

4. LLM / Cost. Токены по моделям (in/out), стоимость в рублях, latency p95 LLM-вызовов, hit rate семантического кеша.

# Стоимость в рублях за час, GigaChat-Pro по 100 ₽ за 1M токенов
sum(rate(bot_llm_tokens_total{model="GigaChat-Pro"}[1h])) * 3600 * 100 / 1000000

SLO и error budget

Без SLO алерты превращаются в спам. Сформулируйте 2–3 ключевых:

SLOЦельОкно
Webhook availability99.9% запросов с 2xx30 дней
Webhook latencyp95 < 500 мс30 дней
LLM ответ доходит за 10 с99%7 дней

Error budget на 99.9% за 30 дней = 43 минуты допустимого простоя. Алерт «срочно чинить» срабатывает, когда burn rate такой, что budget закончится за < 24 часа:

groups:
  - name: slo-webhook
    rules:
      - alert: WebhookFastBurn
        expr: |
          (
            sum(rate(bot_webhook_requests_total{status=~"5.."}[1h]))
              /
            sum(rate(bot_webhook_requests_total[1h]))
          ) > (1 - 0.999) * 14.4
        for: 5m
        labels: { severity: critical }
        annotations:
          summary: "Webhook error rate burns through SLO budget too fast"

      - alert: WebhookSlowBurn
        expr: |
          (
            sum(rate(bot_webhook_requests_total{status=~"5.."}[6h]))
              /
            sum(rate(bot_webhook_requests_total[6h]))
          ) > (1 - 0.999) * 6
        for: 1h
        labels: { severity: warning }

Multi-window multi-burn-rate (Google SRE) — золотой стандарт. Спасает от ложных срабатываний на 30-секундных всплесках.

Обязательный набор алертов

groups:
  - name: bot-critical
    rules:
      - alert: BotDown
        expr: up{job="bot-app"} == 0
        for: 2m
        labels: { severity: critical }
        annotations:
          summary: "Bot pod is down"

      - alert: WebhookHighErrorRate
        expr: |
          sum(rate(bot_webhook_requests_total{status=~"5.."}[5m]))
            /
          sum(rate(bot_webhook_requests_total[5m])) > 0.05
        for: 5m
        labels: { severity: critical }

      - alert: PostgresDown
        expr: up{job="postgres"} == 0
        for: 1m
        labels: { severity: critical }

      - alert: PostgresReplicationLag
        expr: pg_replication_lag_seconds > 30
        for: 5m
        labels: { severity: warning }

      - alert: RedisHighMemory
        expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.9
        for: 10m
        labels: { severity: warning }

      - alert: DiskAlmostFull
        expr: |
          (node_filesystem_avail_bytes{mountpoint="/"} 
            / node_filesystem_size_bytes{mountpoint="/"}) < 0.1
        for: 10m
        labels: { severity: critical }

      - alert: BotQueueBackup
        expr: bot_queue_depth > 1000
        for: 10m
        labels: { severity: warning }

      - alert: LLMCostSurge
        expr: |
          sum(rate(bot_llm_tokens_total{type="out"}[10m])) 
            > 2 * sum(rate(bot_llm_tokens_total{type="out"}[1h] offset 1d))
        for: 10m
        labels: { severity: warning }
        annotations:
          summary: "LLM token usage 2× higher than yesterday"

LLMCostSurge — отдельная боль: один баг в роутинге может сжечь дневной бюджет за час. Алерт на резкий рост — must-have.

Alertmanager: маршрутизация

route:
  group_by: ["alertname", "severity"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 12h
  receiver: telegram-warning
  routes:
    - matchers: [severity="critical"]
      receiver: telegram-critical
      repeat_interval: 30m
    - matchers: [severity="warning"]
      receiver: telegram-warning

receivers:
  - name: telegram-critical
    telegram_configs:
      - bot_token: "{{ .TelegramBotToken }}"
        chat_id: -1001234567890
        message: |
          🚨 *{{ .GroupLabels.alertname }}*
          {{ range .Alerts }}{{ .Annotations.summary }}{{ end }}
  - name: telegram-warning
    telegram_configs:
      - bot_token: "{{ .TelegramBotToken }}"
        chat_id: -1001234567891

Critical — отдельный канал с громкими уведомлениями + on-call в pagerduty/dialogue. Warning — канал для команды без эскалации.

Логирование с trace ID

Метрики не помогут расследовать «что случилось в этом конкретном диалоге». Прокидывайте trace_id из webhook handler во все downstream-вызовы:

import uuid, contextvars

trace_id_var: contextvars.ContextVar[str] = contextvars.ContextVar("trace_id", default="-")

@app.middleware("http")
async def trace_mw(request, call_next):
    tid = request.headers.get("x-trace-id") or uuid.uuid4().hex[:12]
    trace_id_var.set(tid)
    response = await call_next(request)
    response.headers["x-trace-id"] = tid
    return response

logger = logging.getLogger("bot")
class TraceFilter(logging.Filter):
    def filter(self, record):
        record.trace_id = trace_id_var.get()
        return True
logger.addFilter(TraceFilter())

В формате лога — [trace_id=abc123], в Loki ищется по {job="bot"} |= "trace_id=abc123".

Стоимость

Стек Prometheus + Grafana + Alertmanager + Loki — open source, инфраструктура:

  • 1 VM 4 GB RAM / 2 vCPU / 100 GB SSD = 1 500–3 000 ₽/мес.
  • На 10 экспортёров с retention 90 дней — хватает.
  • Альтернативы: Yandex Monitoring (платный, 0.5 ₽ за 1000 точек), VictoriaMetrics (open source, эффективнее по памяти на больших объёмах).

Common pitfalls

  1. Метрики без labels — нельзя отфильтровать «только 5xx». Метрики с слишком большим количеством labels (cardinality explosion: user_id в label) — Prometheus падает.
  2. Алерты без for: — срабатывают на 30-секундный всплеск, спам.
  3. /metrics доступен из интернета — утечка инфы об инфраструктуре.
  4. Не настроен retention — диск Prometheus заполняется за 2 недели.
  5. Алерт = email — никто не читает email в час ночи. Telegram + on-call.
  6. Нет рантбук-ссылок в annotations — дежурный получает алерт и не знает, что делать.
  7. Только метрики, без логов — невозможно расследовать «что случилось у user 12345 в 14:32».

Итого

Стек observability для бота MAX: Prometheus с node/postgres/redis/nginx-exporter, Grafana с 4 ключевыми дашбордами (Overview/SLO, App, Infra, LLM/Cost), Alertmanager с маршрутизацией critical/warning в разные Telegram-каналы. SLO формулируйте 2–3 (availability 99.9%, latency p95 < 500 мс, LLM SLA), используйте multi-window multi-burn-rate алерты против ложных срабатываний. В коде — Counter для запросов и токенов, Histogram для latency, Gauge для очередей и активных диалогов, обязательно с trace_id для связки с логами. Защитите /metrics от внешнего доступа, настройте retention 90 дней. Стек на open source стоит 2–3 тыс. ₽/мес и предотвращает «узнаём об инциденте по жалобам пользователей утром».

Частые вопросы

Какие метрики обязательно собирать в боте MAX?

Минимальный набор: Counter bot_webhook_requests_total с label status (для error rate), Histogram bot_webhook_duration_seconds (для latency), Counter bot_max_api_calls_total с method/status (мониторинг внешнего API), Counter bot_llm_tokens_total с моделью и in/out (стоимость), Histogram bot_llm_latency_seconds, Gauge bot_active_dialogs, Gauge bot_queue_depth для фоновых задач. Этого хватает на дашборд Overview, SLO-алерты и расследование 80% инцидентов. Дополнительно — кастомные счётчики бизнес-метрик: завершённые заказы, эскалации к оператору.

Как избежать cardinality explosion в Prometheus?

Не используйте в качестве label высоко-кардинальные значения: user_id, message_id, chat_id, full URL с query string, IP-адреса. Каждое уникальное значение label создаёт отдельную time series, при миллионе пользователей Prometheus падает по памяти. Допустимые label: HTTP status (5–10 значений), handler name (десятки), модель LLM (несколько). Если нужно расследовать конкретного пользователя — это задача логов с trace_id, а не метрик. В Grafana строите гистограммы и квантили, не нужны индивидуальные временные ряды.

Что такое SLO и зачем он нужен боту?

SLO (Service Level Objective) — формальная цель надёжности: например, 99.9% webhook-запросов должны вернуть 2xx за 30 дней. Из SLO выводится error budget — допустимая доля сбоев (43 минуты для 99.9%). На основе budget строятся multi-window multi-burn-rate алерты: critical только когда нынешний темп ошибок съест budget за < 24 часа. Без SLO алерты срабатывают на каждый 5-секундный всплеск, дежурный игнорирует чат, и реальный инцидент пропускают.

Какие алерты обязательны для бота в проде?

BotDown (под не отвечает 2 мин), WebhookHighErrorRate (> 5% 5xx за 5 мин), PostgresDown, PostgresReplicationLag (> 30 сек), RedisHighMemory (> 90%), DiskAlmostFull (< 10% свободного), BotQueueBackup (очередь > 1000), LLMCostSurge (использование токенов 2× выше вчерашнего). Для критичных — отдельный канал в Telegram с громкими нотификациями и on-call расписанием. Warning — отдельный канал для команды без ночных будильников. Каждый алерт должен иметь annotation с ссылкой на runbook.

Где хранить логи и как связывать их с метриками?

Стандарт open source — Loki + Promtail. Логи в JSON с обязательным полем trace_id, который генерируется в webhook middleware и прокидывается через contextvars во все downstream-вызовы. В Grafana вы видите всплеск ошибок на дашборде, кликаете на точку — переходите в Explore с фильтром по времени, ищете {job="bot"} | json | level="ERROR" за этот интервал, находите trace_id, дальше {job="bot"} |= "trace_id=abc123" показывает полную цепочку обработки конкретного запроса.

Сколько стоит развернуть полный мониторинг для бота?

Open source стек (Prometheus + Grafana + Alertmanager + Loki) запускается на одной VM 4 GB / 2 vCPU / 100 GB SSD за 1.5–3 тыс. ₽/мес. Этого хватает на 10–15 экспортёров с retention 90 дней. На крупных нагрузках (100K+ MAU, миллионы метрик) — переходите на VictoriaMetrics (эффективнее по памяти и диску в 3–5 раз) или managed Yandex Monitoring (платный по точкам). Грaфану и Alertmanager оставляете теми же. Для DR — сделайте бэкап /data/prometheus и /var/lib/grafana раз в сутки.

Как защитить /metrics от внешнего доступа?

Метрики раскрывают внутренности инфраструктуры, версии, эндпоинты — это ценная разведка для атакующего. Простейший вариант: в nginx добавить allow 10.0.0.0/8; deny all; в location /metrics — доступ только из приватной сети. В k8s — NetworkPolicy, разрешающая обращения только от пода Prometheus. Дополнительно basic auth с длинным паролем, известным только Prometheus job. И никогда не публикуйте /metrics без префикса /internal — это снизит вероятность случайного индекса в google.