gendesign/docker-compose.metrics.yml
bot-backend 347342bb5c
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
fix(metrics): гасим crash-loop tg-relay пустым секретом через профиль relay
PR #3487 добавил сервис tg-relay без profiles: контейнер поднимался
всегда и падал в SystemExit на пустом TG_RELAY_SECRET (на проде
подтверждён Restarting в бесконечном цикле).

- deploy-metrics.yml: TG_RELAY_SECRET прокинут в ssh-action по образцу
  ALERT_ACK_GLITCHTIP_SECRET; профиль relay включается независимо от
  alerts, только когда секрет непуст; ::warning на пустом секрете.
  Сравнение PROFILES с "alerts" переведено на case, иначе комбинация
  "alerts,relay" сломала бы прежнюю точную строковую проверку.
- docker-compose.metrics.yml: tg-relay получил profiles: ["relay"].
- tradein-mvp/docker-compose.prod.yml: комментарий у tgbot — deploy-tradein.yml
  секреты приложения в CI не инжектит, TELEGRAM_RELAY_BASE_URL и
  TELEGRAM_RELAY_SECRET на продуктовом хосте заводятся так же, как
  прочие TELEGRAM_* — строкой в user-managed runtime-файле окружения
  backend на хосте, без правок workflow (существующий механизм этого
  файла, см. README-АДМИНУ.md).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY6iWDnGDthdvsMWgK1BMG
2026-09-12 14:44:56 +03:00

356 lines
21 KiB
YAML
Raw 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}
# Резервный получатель GlitchTip-алертов (#3471, POST /glitchtip) — второй
# получатель наряду с основным вебхуком в продуктовый бэкенд на Selectel.
# Секрет СВОЙ, не общий с продуктовым TRADEIN_INTERNAL_AUTH_SECRET: разные
# хосты/домены безопасности. Пусто — эндпоинт отвечает 503, остальной
# функционал сервиса не затронут.
ALERT_ACK_GLITCHTIP_SECRET: ${ALERT_ACK_GLITCHTIP_SECRET:-}
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
# ── Ретранслятор Bot API продукта через Beget (#3471) ───────────────────────
#
# Замер 12.09.2026, оба хоста в одни и те же минуты: `getMe` из контейнера
# `tradein-tgbot` на Selectel — 9 успешных из 12, три ConnectTimeout. TCP-443
# до адреса, резолвящегося на Selectel — 5/6. Тот же TCP-443 до адреса,
# резолвящегося на Beget — 8/8. За сутки 508 строк `network error` в логе
# бота, за 30 дней 92 обрыва итерации poll loop. Путь до Telegram с Selectel
# лоссовый, с Beget чистый — Alertmanager (тот же чат, живёт рядом) шлёт без
# проблем. Продуктовые sendMessage/copyMessage/getUpdates идут сюда вместо
# прямого пути; выключается пустым TELEGRAM_RELAY_BASE_URL на стороне
# продукта — это и есть откат.
#
# НЕ рядом с продуктом: смысл ретранслятора именно в том, что он живёт там,
# откуда путь до Telegram чистый, а не там, откуда он лоссовый.
#
# Образ без сборки и без зависимостей (только stdlib) — тот же принцип, что у
# alert-ack: сервис обязан подниматься даже когда сломано всё остальное.
tg-relay:
image: python:3.12-slim
container_name: gendesign-tg-relay
# #3471 (PR #3487 инцидент): без profiles сервис поднимался ВСЕГДА, а при
# пустом TG_RELAY_SECRET делает SystemExit — то есть уходит в бесконечный
# Restarting сразу после деплоя. Профиль включает deploy-metrics.yml, и
# только когда секрет реально задан (см. PROFILES там).
profiles: ["relay"]
restart: unless-stopped
user: "65534:65534"
command: ["python", "-u", "/app/app.py"]
env_file:
- path: ./backend/.env.runtime
required: false
- path: ./backend/.env
required: false
environment:
# Общий секрет с продуктовым клиентом (TELEGRAM_RELAY_SECRET на стороне
# tradein-backend/tradein-tgbot) — домен публичный, без секрета отказ.
TG_RELAY_SECRET: ${TG_RELAY_SECRET:-}
volumes:
- ./ops/metrics/tg-relay/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 здесь ТОЛЬКО рисует — не решает, что считать инцидентом и куда его
# слать. Тревоги живут в Prometheus (правила) и Alertmanager (маршрутизация,
# Telegram); это единственный путь доставки (#3158).
#
# Встроенный Alerting выключен ЯВНО, а не просто «не настроен». Проверка на
# живом API 12.09.2026 нашла: 0 правил, единственный контакт-поинт —
# стоковый grafana-default-email на example@email.com, GF_SMTP_* не заданы.
# То есть кнопка «New alert rule» в интерфейсе есть и работает, а результат
# молча уходит в никуда — ровно та ситуация, из-за которой никто не проверяет
# второй, настоящий путь. Дублирующий движок на том же датасорсе Prometheus
# надёжности всё равно не прибавляет (общая точка отказа), только даёт второе
# место, где правило может быть заведено и забыто.
#
# Если это когда-нибудь понадобится включить обратно — сначала подключить
# реальный SMTP или другой contact point и завести хотя бы одно тестовое
# правило руками, иначе вернётся тот же капкан.
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"
# Единственный официальный переключатель Grafana Alerting в 11.x — секция
# [unified_alerting], легаси-[alerting] удалён из Grafana ещё в 9.0 и в
# 11.5 в конфиге отсутствует (сверено с grafana.com/docs/grafana/v11.5/
# setup-grafana/configure-grafana/#unified_alerting). false здесь убирает
# раздел Alerting из UI и глушит движок правил целиком — см. #3158 выше.
GF_UNIFIED_ALERTING_ENABLED: "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