From 347342bb5cd806801128eabc37141c55e8fcf9dc Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 12 Sep 2026 14:44:56 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix(metrics):=20=D0=B3=D0=B0=D1=81=D0=B8?= =?UTF-8?q?=D0=BC=20crash-loop=20tg-relay=20=D0=BF=D1=83=D1=81=D1=82=D1=8B?= =?UTF-8?q?=D0=BC=20=D1=81=D0=B5=D0=BA=D1=80=D0=B5=D1=82=D0=BE=D0=BC=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8?= =?UTF-8?q?=D0=BB=D1=8C=20relay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01JY6iWDnGDthdvsMWgK1BMG --- .forgejo/workflows/deploy-metrics.yml | 33 +++++++++++++++++++++++---- docker-compose.metrics.yml | 5 ++++ tradein-mvp/docker-compose.prod.yml | 7 ++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/.forgejo/workflows/deploy-metrics.yml b/.forgejo/workflows/deploy-metrics.yml index 86a702b9..5f2b8dc6 100644 --- a/.forgejo/workflows/deploy-metrics.yml +++ b/.forgejo/workflows/deploy-metrics.yml @@ -91,8 +91,12 @@ jobs: METRICS_TELEGRAM_INFRA_TOPIC_ID: ${{ secrets.METRICS_TELEGRAM_INFRA_TOPIC_ID }} METRICS_TELEGRAM_ONCALL: ${{ secrets.METRICS_TELEGRAM_ONCALL }} ALERT_ACK_GLITCHTIP_SECRET: ${{ secrets.ALERT_ACK_GLITCHTIP_SECRET }} + # #3471: секрет ретранслятора Telegram Bot API (tg-relay). Пусто — + # профиль relay не включаем (см. PROFILES ниже), а не падаем в + # рестарт-луп: контейнер сам делает SystemExit на пустом секрете. + TG_RELAY_SECRET: ${{ secrets.TG_RELAY_SECRET }} with: - envs: METRICS_TELEGRAM_BOT_TOKEN,METRICS_TELEGRAM_CHAT_ID,METRICS_TELEGRAM_TOPIC_ID,METRICS_TELEGRAM_INFRA_TOPIC_ID,METRICS_TELEGRAM_ONCALL,ALERT_ACK_GLITCHTIP_SECRET + envs: METRICS_TELEGRAM_BOT_TOKEN,METRICS_TELEGRAM_CHAT_ID,METRICS_TELEGRAM_TOPIC_ID,METRICS_TELEGRAM_INFRA_TOPIC_ID,METRICS_TELEGRAM_ONCALL,ALERT_ACK_GLITCHTIP_SECRET,TG_RELAY_SECRET host: ${{ secrets.INFRA_DEPLOY_HOST || secrets.DEPLOY_HOST }} username: ${{ secrets.INFRA_DEPLOY_USER || secrets.DEPLOY_USER }} key: ${{ secrets.INFRA_DEPLOY_SSH_KEY || secrets.DEPLOY_SSH_KEY }} @@ -259,6 +263,19 @@ jobs: echo "::warning title=Алерты выключены::METRICS_TELEGRAM_BOT_TOKEN/CHAT_ID не заданы. Метрики и логи собираются, но при срабатывании правила НИКТО не будет уведомлён. Канал доставки — открытый вопрос #3078." fi + # Ретранслятор Telegram Bot API (#3471, PR #3487 сломал прод: сервис + # без profiles уходил в SystemExit на пустом секрете и висел в + # Restarting). Профиль relay включаем НЕЗАВИСИМО от alerts — это + # разные каналы (один шлёт алерты боту, другой ретранслирует + # продуктовый Bot API трафик с Selectel). PROFILES — список через + # запятую, как того требует COMPOSE_PROFILES. + if [ -n "${TG_RELAY_SECRET:-}" ]; then + PROFILES="${PROFILES:+$PROFILES,}relay" + echo "Ретранслятор Telegram: секрет задан, профиль relay включён." + else + echo "::warning title=Резервный ретранслятор Telegram выключен::TG_RELAY_SECRET пуст — tg-relay не поднимается (профиль relay выключен). Продуктовый Telegram-трафик пойдёт напрямую с Selectel, где теряется примерно каждый четвёртый короткий запрос." + fi + # ── Цели file_sd для Prometheus (#3155) ──────────────────────── # Включатель профиля и цель для Prometheus обязаны стоять в ОДНОМ # условии. Пока они жили порознь, вышло так: 27.08 профиль alerts @@ -277,19 +294,27 @@ jobs: AM_TARGETS_FILE=ops/metrics/prometheus/alertmanager_targets.gen.yml : > "$AM_TARGETS_FILE" echo "# Файл рендерится деплоем (deploy-metrics.yml), правки руками затрутся." >> "$AM_TARGETS_FILE" - if [ "$PROFILES" = "alerts" ]; then + # Сравнение через case, а не "=": PROFILES теперь может быть + # комбинацией через запятую ("alerts,relay") с тех пор, как #3471 + # завёл независимый профиль relay — точное равенство строке + # "alerts" сломалось бы молча в тот момент, когда оба профиля + # включены разом. + case ",$PROFILES," in + *,alerts,*) echo '- targets: ["alertmanager:9093"]' >> "$AM_TARGETS_FILE" echo " labels:" >> "$AM_TARGETS_FILE" echo " host: infra" >> "$AM_TARGETS_FILE" echo "Prometheus: приёмник alertmanager:9093 прописан в целях." - else + ;; + *) # Пустой список, а НЕ отсутствующий файл: одиночный бинд-маунт # несуществующего пути docker подменяет каталогом, и Prometheus # не стартует вовсе. echo "# Профиль alerts выключен — приёмников нет." >> "$AM_TARGETS_FILE" echo "[]" >> "$AM_TARGETS_FILE" echo "Prometheus: профиль alerts выключен — целей нет, это штатно." - fi + ;; + esac # ── read-only роль для датасорса GlitchTip ───────────────────── # Идемпотентно. Прав на запись не выдаём вовсе: датасорс Grafana diff --git a/docker-compose.metrics.yml b/docker-compose.metrics.yml index 967bdad9..db68e27c 100644 --- a/docker-compose.metrics.yml +++ b/docker-compose.metrics.yml @@ -236,6 +236,11 @@ services: 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"] diff --git a/tradein-mvp/docker-compose.prod.yml b/tradein-mvp/docker-compose.prod.yml index b5151794..191dfd8a 100644 --- a/tradein-mvp/docker-compose.prod.yml +++ b/tradein-mvp/docker-compose.prod.yml @@ -425,6 +425,13 @@ services: # (env_file ниже); пусто/нет токена = бот молча не стартует (см. tgbot_main.py). # ⚠️ НЕ дублировать TELEGRAM_* в блоке environment: (см. предупреждение у backend # выше про environment: перекрывающий env_file при пустой host-env). + # #3471: TELEGRAM_RELAY_BASE_URL/TELEGRAM_RELAY_SECRET (клиент резервного + # ретранслятора tg-relay на инфраструктурном хосте) заводятся тем же + # способом — строкой в backend/.env.runtime на ЭТОМ (продуктовом, Selectel) + # хосте. deploy-tradein.yml секреты приложения в CI не инжектит вовсе — + # только читает уже лежащий на хосте .env.runtime, поэтому Forgejo-секрет + # TELEGRAM_RELAY_SECRET сюда сам по себе не долетит: файл на хосте нужно + # дополнить вручную (см. README-АДМИНУ.md). tgbot: image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest} container_name: tradein-tgbot From 4b74356c604e8330f3595a9f8e23a5b1d9da85a9 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 12 Sep 2026 14:47:17 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(metrics):=20force-recreate=20alert-ack/?= =?UTF-8?q?tg-relay=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20up=20-d=20=E2=80=94?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=B4=20=D0=BC=D0=BE=D0=BD=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D1=81=20=D1=85=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit up -d сравнивает описание сервиса, не содержимое бинд-маунта. alert-ack и tg-relay получают app.py именно бинд-маунтом (не сборкой образа), поэтому правка файла не пересоздаёт уже работающий контейнер — он продолжает исполнять старый код в памяти интерпретатора. Подтверждено на проде 12.09.2026: PR #3490 (фикс alert-ack) слился, файл на диске обновился (git reset --hard), а gendesign-alert-ack, запущенный 25 минут назад, отвечал по старой логике. Помог только ручной docker restart. force-recreate для обоих сервисов сделан условным по PROFILES (case ",$PROFILES,"), чтобы не падать на несуществующем контейнере, когда профиль alerts/relay в этом прогоне не включён. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JY6iWDnGDthdvsMWgK1BMG --- .forgejo/workflows/deploy-metrics.yml | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.forgejo/workflows/deploy-metrics.yml b/.forgejo/workflows/deploy-metrics.yml index 5f2b8dc6..80a648ae 100644 --- a/.forgejo/workflows/deploy-metrics.yml +++ b/.forgejo/workflows/deploy-metrics.yml @@ -326,6 +326,43 @@ jobs: COMPOSE_PROFILES="$PROFILES" \ docker compose -p gendesign-metrics -f docker-compose.metrics.yml up -d --remove-orphans + # ── alert-ack / tg-relay: код монтируется с хоста ──────────────── + # Тот же класс бага, что у Alertmanager (см. ниже) и Caddyfile: + # `up -d` сравнивает ОПИСАНИЕ сервиса, а не содержимое бинд-маунта. + # alert-ack и tg-relay получают код именно бинд-маунтом файла + # (./ops/metrics/{alert-ack,tg-relay}/app.py:/app/app.py:ro), а не + # сборкой образа — правка app.py оставляет уже запущенный + # контейнер работать на СТАРОМ коде в памяти интерпретатора сколько + # угодно, и `up -d` этого не видит вовсе. + # + # Пойман на проде 12.09.2026: PR #3490 (фикс alert-ack) слился, + # `git reset --hard` обновил файл на диске (grep по новому + # комментарию находил его), а gendesign-alert-ack, запущенный за + # 25 минут до этого, продолжал отвечать по старой логике — + # зелёный деплой, тихо неверное поведение. Починил только ручной + # `docker restart gendesign-alert-ack`. force-recreate здесь — + # замена этому ручному шагу. + # + # case ",$PROFILES," — пересоздаём только если профиль сервиса + # реально включён в ЭТОМ прогоне, иначе force-recreate ругается на + # несуществующий контейнер (сервис не создан вовсе). + case ",$PROFILES," in + *,alerts,*) + COMPOSE_PROFILES="$PROFILES" \ + docker compose -p gendesign-metrics -f docker-compose.metrics.yml \ + up -d --force-recreate alert-ack + echo "alert-ack: контейнер пересоздан — код монтируется с хоста, up -d его не подхватывает (#3490)." + ;; + esac + case ",$PROFILES," in + *,relay,*) + COMPOSE_PROFILES="$PROFILES" \ + docker compose -p gendesign-metrics -f docker-compose.metrics.yml \ + up -d --force-recreate tg-relay + echo "tg-relay: контейнер пересоздан — код монтируется с хоста, up -d его не подхватывает (#3490)." + ;; + esac + # ── Alertmanager: пересоздать, если конфиг перерисовали ───────── # `up -d` выше СЧИТАЕТ alertmanager неизменившимся: он сравнивает # описание сервиса, а содержимое бинд-маунта в это сравнение не