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