gendesign/docker-compose.metrics.yml
bot-backend b90872b5d7
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m57s
CI / backend-tests (pull_request) Successful in 17m26s
feat(ops/metrics): инфра-алерты уезжают в тему «метрики», клиенты остаются в «алертах»
Форумная группа имеет три темы, но тема «метрики» была пуста: оба прямых
получателя Alertmanager — telegram и telegram-heartbeat — брали топик из той
же переменной METRICS_TELEGRAM_TOPIC_ID, что и сервис alert-ack. Развести их
было нечем, и heartbeat вместе со всем инфраструктурным шумом падал в ленту
клиентских инцидентов.

Смешанные в одной теме, инфраструктура и клиентский инцидент не равны по
срочности и приучают пролистывать обе.

Вводится METRICS_TELEGRAM_INFRA_TOPIC_ID для прямых получателей Alertmanager.
alert-ack и вебхук GlitchTip остаются на прежней переменной, тема поддержки
не тронута. Пока новая переменная не задана, берётся старая — до этого момента
поведение ровно прежнее, а не сломанное.

Клиентская METRICS_TELEGRAM_TOPIC_LINE убрана целиком: после переезда обоих
получателей на инфраструктурную строку шаблон её не содержит, а деплой
продолжал бы её собирать и объявлять в envsubst. Тест, закрепляющий сборку
такой строки, зеленел бы вечно и мешал бы её убрать.

Проверено рендером, а не чтением: при заданной теме telegram и
telegram-heartbeat дают 245, telegram-clients уходит вебхуком без темы; при
незаданной — поля message_thread_id нет вовсе (пустое значение уронило бы
Alertmanager целиком). Логика отката прогнана во всех трёх состояниях
переменных. backend/tests/ops — 86 passed.

Closes #3163
2026-08-27 21:50:51 +03:00

277 lines
15 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Стек наблюдаемости — СЕРВЕРНАЯ сторона. Живёт на инфраструктурном хосте (Beget).
#
# Деплоится ОТДЕЛЬНО от продуктового стека, своим project-name:
# docker compose -p gendesign-metrics -f docker-compose.metrics.yml up -d
#
# ПОЧЕМУ ЗДЕСЬ, А НЕ РЯДОМ С ПРОДОМ. После переезда 25.08 продукт живёт на Selectel
# Poincare, инфраструктура (Forgejo, GlitchTip, CouchDB) осталась на Beget. Наблюдатель
# сознательно поставлен у ДРУГОГО провайдера, чем наблюдаемое: если ляжет Poincare,
# мониторинг должен об этом сказать, а не лечь вместе с ним.
#
# ТРАНСПОРТ — PUSH, А НЕ PULL. Prometheus не ходит на Poincare за метриками: там агент
# Alloy сам шлёт remote_write и loki.write исходящим HTTPS через metrics.gendsgn.ru.
# Поэтому на Poincare не открывается НИ ОДНОГО входящего порта — ufw там остаётся
# 22/80/443. Приёмник включён флагом --web.enable-remote-write-receiver ниже.
#
# СЕТИ. Обе внешние, обе уже существуют на Beget:
# gendesign_shared — через неё Caddy ходит на grafana:3000 (и couchdb, garmin-mcp)
# gendesign_default — в ней infra-postgres, glitchtip, forgejo: их скрейпит агент
# Тот же приём, что у Caddy — он подключён к обеим.
#
# ПОРТЫ НАРУЖУ НЕ ПУБЛИКУЮТСЯ. Только expose. Наружу выходит один хост
# metrics.gendsgn.ru через Caddy под basic_auth (caddy/sites/infra.caddy).
# 3000/9090/3100/9093 на 0.0.0.0 не биндятся сознательно: на Beget уже был случай,
# когда redis уехал в интернет из-за лишней записи ports в базовом compose-файле.
x-logging: &default-logging
driver: journald
services:
# ── Prometheus: хранилище временных рядов + движок правил ────────────────────
prometheus:
image: prom/prometheus:v3.1.0
container_name: gendesign-prometheus
restart: unless-stopped
user: "65534:65534" # nobody — том создаётся снаружи, см. deploy-metrics.yml
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
# Ретенция задана ЯВНО и по обоим измерениям. Без --storage.tsdb.retention.size
# диск съедается молча: по умолчанию ограничение только по времени, а сколько
# это в байтах — зависит от числа рядов, которое растёт само.
- "--storage.tsdb.retention.time=30d"
- "--storage.tsdb.retention.size=8GB"
# Приёмник push-метрик от агентов. Без флага remote_write отвечает 404,
# и агент на Poincare будет молча копить в WAL, а графики останутся пустыми.
- "--web.enable-remote-write-receiver"
# Нужен для перезагрузки правил без рестарта (deploy-metrics.yml дёргает).
- "--web.enable-lifecycle"
- "--web.external-url=https://metrics.gendsgn.ru/prometheus"
- "--web.route-prefix=/"
volumes:
- ./ops/metrics/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./ops/metrics/prometheus/rules:/etc/prometheus/rules:ro
# file_sd для job "alertmanager" — см. комментарий в prometheus.yml.
# Файл производный: рендерится деплоем (deploy-metrics.yml) по тому же
# условию, что включает профиль alerts, и потому в репозитории его нет.
# Забыть этот монт — значит вернуть "no such host" из-за отсутствующего
# файла целей (был инцидент, когда похожий пропущенный монт положил Caddy).
- ./ops/metrics/prometheus/alertmanager_targets.gen.yml:/etc/prometheus/alertmanager_targets.gen.yml:ro
- prometheus_data:/prometheus
expose:
- "9090"
networks:
- shared
- infra
mem_limit: 2g
logging: *default-logging
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/-/healthy"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
# ── Loki: логи. Monolithic-режим — рекомендован до ~20 ГБ/сутки ──────────────
# У нас ~39 МБ/сутки (замер 24.08), запас в 500 раз. Микросервисный режим здесь
# был бы чистой сложностью без выигрыша.
loki:
image: grafana/loki:3.3.2
container_name: gendesign-loki
restart: unless-stopped
user: "10001:10001"
command: ["-config.file=/etc/loki/loki-config.yml"]
volumes:
- ./ops/metrics/loki/loki-config.yml:/etc/loki/loki-config.yml:ro
- loki_data:/loki
expose:
- "3100"
networks:
- shared
mem_limit: 1g
logging: *default-logging
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3100/ready"]
interval: 30s
timeout: 10s
retries: 10
start_period: 60s
# ── Alertmanager: маршрутизация и дедупликация алертов ───────────────────────
# Шлёт в Telegram НАПРЯМУЮ с Beget, а не через бэкенд МЕРЫ. Это осознанно:
# доставка алерта не должна зависеть от хоста, про который алерт. Если Poincare
# лёг, сообщение об этом обязано уйти без его участия.
alertmanager:
image: prom/alertmanager:v0.28.0
container_name: gendesign-alertmanager
restart: unless-stopped
user: "65534:65534"
# За профилем: канал доставки алертов — открытый вопрос #3078 (тот же чат,
# что у вебхука GlitchTip, или отдельный; порог ночной побудки). Пока он не
# решён, стек метрик поднимается и работает без Alertmanager, а не ждёт его.
# Включается добавлением `alerts` в COMPOSE_PROFILES — см. часть 5.
profiles: ["alerts"]
command:
- "--config.file=/etc/alertmanager/alertmanager.yml"
- "--storage.path=/alertmanager"
- "--web.external-url=https://metrics.gendsgn.ru/alertmanager"
# Внешний адрес — ТОЛЬКО для ссылок в сообщениях; обслуживать всё под
# этим префиксом Alertmanager не должен. Без этой строки он отвечает
# 404 на `/api/v2/alerts`, а Prometheus именно туда и пишет: 27.08
# Prometheus нашёл приёмник (`activeAlertmanagers` непуст), послал
# уведомление и получил 404 — `alertmanager_alerts_received_total 0`
# при `prometheus_notifications_errors_total 1`. Тот же флаг и по той
# же причине стоит у Prometheus выше.
- "--web.route-prefix=/"
env_file:
- path: ./backend/.env.runtime
required: false
- path: ./backend/.env
required: false
environment:
# Токен и чат берутся из окружения — в репозиторий не попадают.
# Отсутствие проверяется в deploy-metrics.yml ДО подъёма: пустой токен
# даёт Alertmanager, который стартует зелёным и молча ничего не шлёт.
METRICS_TELEGRAM_BOT_TOKEN: ${METRICS_TELEGRAM_BOT_TOKEN:-}
METRICS_TELEGRAM_CHAT_ID: ${METRICS_TELEGRAM_CHAT_ID:-}
volumes:
- ./ops/metrics/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
- alertmanager_data:/alertmanager
expose:
- "9093"
networks:
- shared
mem_limit: 256m
logging: *default-logging
healthcheck:
# Путь БЕЗ ПРЕФИКСА — вместе с `--web.route-prefix=/` выше.
#
# Раньше здесь стоял `/alertmanager/-/healthy`: внешний адрес заставлял
# Alertmanager обслуживать под префиксом всё, включая служебные ручки, и
# `/-/healthy` отдавал 404 (прод 27.08 — контейнер работал, а docker
# держал его `unhealthy` бессрочно). Лечили симптом на стороне проверки,
# и потому не заметили, что тот же префикс ломает `/api/v2/alerts`, куда
# пишет Prometheus. Теперь причина убрана, и путь снова обычный.
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9093/-/healthy"]
interval: 30s
timeout: 10s
retries: 5
# ── Приёмник алертов с кнопкой подтверждения ────────────────────────────────
#
# Alertmanager умеет писать в Telegram сам, но инлайн-клавиатуру его интеграция
# не поддерживает — а без кнопки нет обратной связи «человек увидел и взял в
# работу». Клиентские инциденты (severity=critical на продуктовом хосте) идут
# вебхуком сюда, всё остальное — прежним прямым путём: чем меньше звеньев у
# алерта, тем он надёжнее.
#
# Живёт РЯДОМ С ALERTMANAGER, на инфраструктурной машине. У бота МЕРЫ уже есть
# приём обновлений, и повесить обработку туда было бы дешевле, но он работает
# на продуктовом хосте: при падении продукта кнопка оказалась бы мёртвой ровно
# тогда, когда нужна.
#
# Образ без сборки и без зависимостей (только stdlib): сервис обязан
# подниматься даже когда сломано всё остальное.
alert-ack:
image: python:3.12-slim
container_name: gendesign-alert-ack
restart: unless-stopped
user: "65534:65534"
profiles: ["alerts"]
command: ["python", "-u", "/app/app.py"]
env_file:
- path: ./backend/.env.runtime
required: false
- path: ./backend/.env
required: false
environment:
METRICS_TELEGRAM_BOT_TOKEN: ${METRICS_TELEGRAM_BOT_TOKEN:-}
METRICS_TELEGRAM_CHAT_ID: ${METRICS_TELEGRAM_CHAT_ID:-}
# 158 — тема клиентских инцидентов (#3163). Инфраструктурная тема (245)
# теперь отдельная и живёт в конфиге Alertmanager
# (ops/metrics/alertmanager/alertmanager.yml.tmpl), не здесь: alert-ack
# получает только клиентские инциденты и остаётся на этой переменной.
METRICS_TELEGRAM_TOPIC_ID: ${METRICS_TELEGRAM_TOPIC_ID:-}
METRICS_TELEGRAM_ONCALL: ${METRICS_TELEGRAM_ONCALL:-}
# Внешний адрес попадает в кнопку. Пустой — сообщение уйдёт без кнопки,
# но уйдёт: алерт важнее подтверждения.
ALERT_ACK_PUBLIC_URL: ${ALERT_ACK_PUBLIC_URL:-https://metrics.gendsgn.ru}
volumes:
- ./ops/metrics/alert-ack/app.py:/app/app.py:ro
expose:
- "8080"
networks:
- shared
mem_limit: 128m
logging: *default-logging
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://localhost:8080/healthz',timeout=5)"]
interval: 30s
timeout: 10s
retries: 5
# ── Grafana: витрина ─────────────────────────────────────────────────────────
grafana:
image: grafana/grafana:11.5.1
container_name: gendesign-grafana
restart: unless-stopped
user: "472:472"
env_file:
- path: ./backend/.env.runtime
required: false
- path: ./backend/.env
required: false
environment:
# Доступ закрыт Caddy basic_auth снаружи. Внутренний вход Grafana оставлен
# включённым намеренно: анонимный Viewer + отключённый логин лишили бы
# возможности что-то настроить, а один общий пароль в Caddy не даёт
# разделения ролей внутри.
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-}
GF_SERVER_ROOT_URL: https://metrics.gendsgn.ru/
GF_SERVER_SERVE_FROM_SUB_PATH: "false"
# Телеметрия наружу — выключена. Отдельный хост, отдельный провайдер, и не
# хочется, чтобы наблюдатель сам ходил в интернет без нужды.
GF_ANALYTICS_REPORTING_ENABLED: "false"
GF_ANALYTICS_CHECK_FOR_UPDATES: "false"
GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES: "false"
GF_NEWS_NEWS_FEED_ENABLED: "false"
GF_USERS_ALLOW_SIGN_UP: "false"
# Датасорс к БД GlitchTip: пароль read-only роли из окружения.
GLITCHTIP_RO_PASSWORD: ${GLITCHTIP_RO_PASSWORD:-}
TRADEIN_RO_PASSWORD: ${TRADEIN_RO_PASSWORD:-}
GENDESIGN_RO_PASSWORD: ${GENDESIGN_RO_PASSWORD:-}
volumes:
- ./ops/metrics/grafana/provisioning:/etc/grafana/provisioning:ro
- ./ops/metrics/grafana/dashboards:/var/lib/grafana/dashboards:ro
- grafana_data:/var/lib/grafana
expose:
- "3000"
networks:
- shared
- infra
mem_limit: 512m
logging: *default-logging
depends_on:
- prometheus
- loki
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
volumes:
prometheus_data:
loki_data:
grafana_data:
alertmanager_data:
networks:
shared:
external: true
name: gendesign_shared
infra:
external: true
name: gendesign_default