fix(observability): агент не падает из-за переменной чужой роли + DSN экспортеров собираются (#3078) #3106

Merged
lekss361 merged 1 commit from fix/3078-exporter-dsn-and-profiles into main 2026-08-26 09:50:42 +00:00
Owner

Продолжение после #3105. Стек серверной части поднялся (Prometheus / Loki / Grafana здоровы, metrics.gendsgn.ru отвечает 401), а агенты не поднялись ни на одном хосте — джоба agent-apps упала целиком.

Дефект 1: обязательная переменная чужой роли

error while interpolating services.postgres-exporter-infra.environment.
DATA_SOURCE_NAME: required variable INFRA_EXPORTER_DSN is missing a value

INFRA_EXPORTER_DSN нужен экспортеру с profiles: ["infra"] — сервису, который на продуктовом хосте не поднимается вовсе. Но Compose интерполирует весь файл до фильтрации по профилям, поэтому ${VAR:?} роняет команду из-за переменной чужой роли.

Цена: вместе с экспортером не поднялись alloy, node-exporter и cadvisor, которым никакой DSN не нужен. Симметрично упал бы и инфраструктурный агент — на двух продуктовых переменных.

Дефект 2: DSN не собирались вовсе

GENDESIGN_EXPORTER_DSN тоже отсутствовал. setup-metrics-exporter-dsn.sh читает только runtime-файл окружения, а DATABASE_URL и TRADEIN_DATABASE_URL живут в основном. Значит подстановка всегда была пустой, add_key печатал «нечем заполнить, пропускаю» и выходил с кодом 0 — а compose затем падал на обязательной переменной.

Мягкий пропуск встречался с жёстким требованием: скрипт считал ситуацию нормальной, compose — фатальной.

Что стало

место было стало
docker-compose.metrics-agent.yml ${..._EXPORTER_DSN:?} ×3 ${..._EXPORTER_DSN:-} — интерполяция не может упасть
deploy-metrics.yml COMPOSE_PROFILES=apps / =infra жёстко профиль включается, только если DSN этой роли заполнены; иначе ::warning и агент поднимается без экспортера
setup-metrics-exporter-dsn.sh читал один файл окружения читает оба (базовый, затем runtime — он перекрывает); пишет по-прежнему только в runtime

:- — это не ослабление. Пустой DATA_SOURCE_NAME поднял бы экспортер, который молча не отдаёт метрик, — ровно тот тихий отказ, ради которого весь стек и заводится. Поэтому пустой DSN теперь означает «профиль не включаем», а не «поднимаем пустым». Громкость не убрана, а перенесена туда, где роль известна — тот же приём, что уже применён к Alertmanager в джобе server.

Честно про остаток

INFRA_EXPORTER_DSN не собирает никто: скрипт знает только про GENDESIGN_/TRADEIN_ и запускается на продуктовом хосте. Пока это так, инфраструктурный агент будет предупреждать, что метрик Postgres инфры нет — вместо того чтобы падать целиком. Отмечено комментарием прямо в воркфлоу, чтобы не выглядело недосмотром.

Также в силе: TRADEIN_EXPORTER_DSN выводится из TRADEIN_DATABASE_URL, которой на Poincare нет — это #3094. До его закрытия продуктовый профиль экспортеров не включится, и предупреждение об этом теперь будет видно в логе деплоя явно.

Тесты

backend/tests/ops/test_3078_exporter_profiles.py, 3 структурных — проверяют оба конца инварианта:

  1. :? не вернулись в compose;
  2. каждая DSN-переменная из compose проверяется в деплое (иначе экспортер поднимется пустым и замолчит);
  3. профили не захардкожены строкой — иначе гейт по DSN обходится.

Фальсификация: на исходных файлах краснеют все три.

Проверено помимо тестов: bash -n скрипта, оба YAML парсятся, в коммите нет CR.

Вне этого PR — но сделано и проверено на проде

CADDY_SITES на Beget вернул в infra: мой же предыдущий пересоздание Caddy подняло контейнер с дефолтом * из compose, и Beget начал грузить чужой apps.caddy. Сейчас git / errors / obsidian / metrics отвечают, конфиг валиден.

На Poincare остаётся CADDY_SITES=* — он грузит чужой infra.caddy и выпрашивает сертификат на metrics.gendsgn.ru, который обслуживает Beget. Это уже стоило лимита Let's Encrypt: HTTP 429 too many failed authorizations (5) for metrics.gendsgn.ru in the last 1h. Сертификат Beget в итоге получил, но пока Poincare продолжает попытки, лимит будет выжигаться снова — а он же нужен для продления живых сертов. Правится строкой в файле окружения Poincare (CADDY_SITES=apps) плюс пересоздание Caddy; файл закрыт хуком, поэтому за владельцем.

Refs #3078, #3099, #3105

Продолжение после #3105. Стек серверной части поднялся (Prometheus / Loki / Grafana здоровы, `metrics.gendsgn.ru` отвечает 401), а **агенты не поднялись ни на одном хосте** — джоба `agent-apps` упала целиком. ## Дефект 1: обязательная переменная чужой роли ``` error while interpolating services.postgres-exporter-infra.environment. DATA_SOURCE_NAME: required variable INFRA_EXPORTER_DSN is missing a value ``` `INFRA_EXPORTER_DSN` нужен экспортеру с `profiles: ["infra"]` — сервису, который на продуктовом хосте **не поднимается вовсе**. Но Compose интерполирует весь файл **до** фильтрации по профилям, поэтому `${VAR:?}` роняет команду из-за переменной чужой роли. Цена: вместе с экспортером не поднялись `alloy`, `node-exporter` и `cadvisor`, которым никакой DSN не нужен. Симметрично упал бы и инфраструктурный агент — на двух продуктовых переменных. ## Дефект 2: DSN не собирались вовсе `GENDESIGN_EXPORTER_DSN` тоже отсутствовал. `setup-metrics-exporter-dsn.sh` читает только runtime-файл окружения, а `DATABASE_URL` и `TRADEIN_DATABASE_URL` живут в основном. Значит подстановка всегда была пустой, `add_key` печатал «нечем заполнить, пропускаю» и выходил с кодом **0** — а compose затем падал на обязательной переменной. Мягкий пропуск встречался с жёстким требованием: скрипт считал ситуацию нормальной, compose — фатальной. ## Что стало | место | было | стало | |---|---|---| | `docker-compose.metrics-agent.yml` | `${..._EXPORTER_DSN:?}` ×3 | `${..._EXPORTER_DSN:-}` — интерполяция не может упасть | | `deploy-metrics.yml` | `COMPOSE_PROFILES=apps` / `=infra` жёстко | профиль включается, только если DSN **этой роли** заполнены; иначе `::warning` и агент поднимается без экспортера | | `setup-metrics-exporter-dsn.sh` | читал один файл окружения | читает оба (базовый, затем runtime — он перекрывает); пишет по-прежнему только в runtime | **`:-` — это не ослабление.** Пустой `DATA_SOURCE_NAME` поднял бы экспортер, который молча не отдаёт метрик, — ровно тот тихий отказ, ради которого весь стек и заводится. Поэтому пустой DSN теперь означает «профиль не включаем», а не «поднимаем пустым». Громкость не убрана, а перенесена туда, где роль **известна** — тот же приём, что уже применён к Alertmanager в джобе `server`. ## Честно про остаток `INFRA_EXPORTER_DSN` **не собирает никто**: скрипт знает только про `GENDESIGN_`/`TRADEIN_` и запускается на продуктовом хосте. Пока это так, инфраструктурный агент будет предупреждать, что метрик Postgres инфры нет — вместо того чтобы падать целиком. Отмечено комментарием прямо в воркфлоу, чтобы не выглядело недосмотром. Также в силе: `TRADEIN_EXPORTER_DSN` выводится из `TRADEIN_DATABASE_URL`, которой на Poincare нет — это [#3094](https://git.gendsgn.ru/lekss361/gendesign/issues/3094). До его закрытия продуктовый профиль экспортеров не включится, и предупреждение об этом теперь будет видно в логе деплоя явно. ## Тесты `backend/tests/ops/test_3078_exporter_profiles.py`, 3 структурных — проверяют **оба конца** инварианта: 1. `:?` не вернулись в compose; 2. каждая DSN-переменная из compose проверяется в деплое (иначе экспортер поднимется пустым и замолчит); 3. профили не захардкожены строкой — иначе гейт по DSN обходится. **Фальсификация:** на исходных файлах краснеют все три. Проверено помимо тестов: `bash -n` скрипта, оба YAML парсятся, в коммите нет CR. ## Вне этого PR — но сделано и проверено на проде `CADDY_SITES` на Beget вернул в `infra`: мой же предыдущий пересоздание Caddy подняло контейнер с дефолтом `*` из compose, и Beget начал грузить чужой `apps.caddy`. Сейчас `git` / `errors` / `obsidian` / `metrics` отвечают, конфиг валиден. **На Poincare остаётся `CADDY_SITES=*`** — он грузит чужой `infra.caddy` и выпрашивает сертификат на `metrics.gendsgn.ru`, который обслуживает Beget. Это уже стоило лимита Let's Encrypt: `HTTP 429 too many failed authorizations (5) for metrics.gendsgn.ru in the last 1h`. Сертификат Beget в итоге получил, но пока Poincare продолжает попытки, лимит будет выжигаться снова — а он же нужен для продления живых сертов. Правится строкой в файле окружения Poincare (`CADDY_SITES=apps`) плюс пересоздание Caddy; файл закрыт хуком, поэтому за владельцем. Refs #3078, #3099, #3105
lekss361 added 1 commit 2026-08-26 09:46:16 +00:00
fix(observability): агент не падает из-за переменной чужой роли (#3078)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m55s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Successful in 17m25s
beafe6925b
Джоба agent-apps упала целиком:

    error while interpolating services.postgres-exporter-infra.environment.
    DATA_SOURCE_NAME: required variable INFRA_EXPORTER_DSN is missing a value

INFRA_EXPORTER_DSN нужен экспортеру с profiles: ["infra"], который на
продуктовом хосте не поднимается вовсе. Но compose интерполирует ВЕСЬ файл
до фильтрации по профилям, поэтому `${VAR:?}` роняет команду из-за чужой
переменной. Вместе с агентом не поднялись alloy, node-exporter и cadvisor,
которым никакой DSN не нужен. Симметрично упал бы и инфраструктурный агент -
на двух продуктовых переменных.

Второй дефект в той же цепочке: GENDESIGN_EXPORTER_DSN тоже отсутствовал.
setup-metrics-exporter-dsn.sh читает только runtime-файл окружения бэкенда, а
DATABASE_URL и TRADEIN_DATABASE_URL живут в основном. Значит подстановка
всегда была пустой, add_key печатал "нечем заполнить" и выходил с кодом 0 -
мягкий пропуск встречался с жёстким требованием compose.

Стало:
- compose: `:-` вместо `:?` у трёх DSN. Интерполяция больше не может упасть.
- deploy-metrics.yml: профиль экспортеров включается, только если нужные ЭТОЙ
  роли DSN заполнены; иначе ::warning и агент поднимается без экспортера.
  Громкость не убрана, а перенесена туда, где роль известна. Тот же приём, что
  уже применён к Alertmanager в джобе server.
- setup-metrics-exporter-dsn.sh: читает оба файла окружения (базовый, затем
  runtime - он перекрывает). Пишет по-прежнему только в runtime, лишних копий
  пароля не заводит.

Почему `:-` не ослабление: пустой DATA_SOURCE_NAME поднял бы экспортер,
который молча не отдаёт метрик, - ровно тот тихий отказ, ради которого весь
стек и заводится. Поэтому пустой DSN теперь означает "профиль не включаем",
а не "поднимаем пустым".

Известное следствие, отмеченное в коде: INFRA_EXPORTER_DSN не собирает никто -
скрипт знает только про GENDESIGN_/TRADEIN_ и работает на продуктовом хосте.
Пока это так, инфраструктурный агент будет честно предупреждать, что метрик
Postgres инфры нет, вместо того чтобы падать целиком.

Тесты (3) структурные, проверяют оба конца инварианта: обязательности не
вернулись в compose; каждая DSN-переменная проверяется в деплое; профили не
захардкожены. Фальсификация: на исходных файлах краснеют все три.
lekss361 merged commit 9ede71d0b1 into main 2026-08-26 09:50:42 +00:00
lekss361 deleted branch fix/3078-exporter-dsn-and-profiles 2026-08-26 09:50:42 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3106
No description provided.