gendesign/docker-compose.metrics.yml
bot-backend 790691d0b4
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
fix(ops/metrics): Alertmanager отвечал 404 на /api/v2/alerts — Prometheus писал мимо
Приёмочная проверка #3155 показала второй отказ на том же пути. Prometheus
нашёл приёмник (`activeAlertmanagers` непуст), отправил уведомление — и
получил 404: `alertmanager_alerts_received_total 0` при
`prometheus_notifications_errors_total 1` и `dropped_total 1`.

`--web.external-url=…/alertmanager` без `--web.route-prefix=/` заставляет
Alertmanager обслуживать ВСЁ под этим префиксом. Проверено прямой пробой из
контейнера Prometheus: `/api/v2/status` → 404, `/alertmanager/api/v2/status`
→ 200. Снаружи префикс при этом никому не нужен: маршрута `/alertmanager`
в Caddy нет вовсе, внешний адрес используется только для ссылок в сообщениях.

Симптом этого же префикса уже ловили 27.08 на healthcheck — и вылечили на
стороне проверки, прописав ей путь с префиксом. Из-за этого причина осталась
жива и продолжила ломать то, что чинить куда важнее. Возвращаю обычный путь
healthcheck вместе с флагом.

Заодно оживает датасорс Alertmanager в Grafana
(`ops/metrics/grafana/provisioning/datasources/datasources.yml:37`) — он
настроен на корень и до сих пор упирался в тот же 404.

Closes #3161
2026-08-27 21:16:06 +03:00

273 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:-}
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