305 lines
18 KiB
YAML
305 lines
18 KiB
YAML
# Стек наблюдаемости — СЕРВЕРНАЯ сторона. Живёт на инфраструктурном хосте (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
|
||
|
||
# ── 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
|