gendesign/docker-compose.metrics.yml
bot-backend 309d273f3f
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-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 Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(observability): стек метрик и логов — Prometheus, Loki, Grafana, агенты на обоих хостах
Метрик в проекте не было ни одной: ни экспортеров, ни /metrics в бэкендах,
единственный канал наблюдения — journald, единственный сигнал об аварии —
исключение в GlitchTip. Из-за этого целый класс отказов невидим в принципе:
задача рапортует done, строк ноль, исключения нет. Так протухли данные на семь
месяцев (#2998), 34 дня был мёртв house_imv_backfill (#2698), 8 суток писал ноль
newbuilding_enrich (#2767), 91 день копилось раздутие listings (#2992).

Grafana не заменяет GlitchTip: ошибки остаются там. Grafana OSS не принимает
Sentry DSN ни одним компонентом, а скрубберы в before_send — требование 152-ФЗ.
Здесь появляется другой класс данных: ряды и алерты по трендам.

Наблюдатель поставлен у ДРУГОГО провайдера, чем наблюдаемое: серверная сторона
на Beget, рядом с GlitchTip. Если ляжет Poincare, мониторинг должен об этом
сказать, а не лечь вместе с ним.

Транспорт push, а не pull: агент на Poincare шлёт remote_write и логи исходящим
HTTPS, поэтому там не открывается ни одного входящего порта сверх 22/80/443.
При обрыве канала Alloy копит в WAL и досылает — pull-скрейп в той же ситуации
терял бы точки именно в аварии, ради которой мониторинг и нужен.

Два контура доступа с разными учётками. Пароль приёмника по построению лежит
открытым на продуктовом хосте, значит его компрометация неизбежна вместе с
хостом; будь это учётка витрины, утёк бы и доступ к дашбордам.

GlitchTip читается прямым SQL, а не Sentry-плагином: у плагина на 6.1.6
stats_v2 отдаёт 500 (баг GlitchTip #381), Events/Discover — 404 (#416), а в
grafana/sentry-datasource слово glitchtip не встречается ни разу. Схема сверена
на живой базе: колонка времени называется timestamp, а не received, и отдельной
таблицы IssueIndex не существует — агрегаты лежат на самой issue_events_issue.

Алерты за профилем alerts: канал доставки — открытый вопрос #3078, и стек не
должен на нём стоять. Деплой предупреждает, что уведомлять пока некому.

Каждая настройка, способная отказать молча, закрыта явно: ретенция Prometheus
задана и по времени и по размеру, retention_enabled у компактора Loki (без него
retention_period не работает вовсе), путь к журналу и запуск Alloy от root
(иначе агент читает ноль записей без ошибки), проверка Caddy до перезагрузки
(на этом хосте тот же Caddy держит git, errors и obsidian).

Refs #3078
2026-08-26 10:45:54 +03:00

202 lines
9.9 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
- 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"
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:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9093/-/healthy"]
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