Без метрик и дашборда состояние бота — это «вроде работает, пользователи не жалуются». Реальность — на пике 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_exporter | CPU, RAM, диск, сеть, load average хоста |
| postgres_exporter | соединения, размер таблиц, IOPS, replication lag |
| redis_exporter | ops/sec, hit rate, память, eviction |
| nginx-prometheus-exporter | requests, response time, статус-коды |
| blackbox_exporter | HTTPS 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 availability | 99.9% запросов с 2xx | 30 дней |
| Webhook latency | p95 < 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
- Метрики без labels — нельзя отфильтровать «только 5xx». Метрики с слишком большим количеством labels (cardinality explosion: user_id в label) — Prometheus падает.
- Алерты без
for:— срабатывают на 30-секундный всплеск, спам. /metricsдоступен из интернета — утечка инфы об инфраструктуре.- Не настроен retention — диск Prometheus заполняется за 2 недели.
- Алерт = email — никто не читает email в час ночи. Telegram + on-call.
- Нет рантбук-ссылок в annotations — дежурный получает алерт и не знает, что делать.
- Только метрики, без логов — невозможно расследовать «что случилось у 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.