Commit graph

10 commits

Author SHA1 Message Date
01960b03be Merge pull request 'Резервный канал для алертов GlitchTip: приём на alert-ack, другой хост и другой провайдер' (#3482) from feat/3471-glitchtip-fallback-alert-ack into main
All checks were successful
Deploy Metrics / server (push) Successful in 32s
Deploy Metrics / agent-infra (push) Successful in 26s
Deploy Metrics / agent-apps (push) Successful in 35s
2026-09-12 11:08:25 +00:00
bot-backend
423842ae36 feat(ops): резервный получатель GlitchTip-алертов в alert-ack
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
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 / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Все три alert-правила GlitchTip (backend, frontend, Trade-In) сейчас шлют
единственный вебхук в продуктовый бэкенд на Selectel — тот самый хост, за
которым они следят. Если там упал backend или Caddy, ошибки приложения
задерживаются или пропадают именно тогда, когда нужнее всего.

Добавлен POST /glitchtip в alert-ack (живёт на инфраструктурном хосте Beget,
не зависит от здоровья продукта): второй получатель того же Slack-совместимого
payload, аутентификация секретом в заголовке X-GlitchTip-Secret или query
?secret= (тот же подход, что у tradein-mvp/backend/app/api/v1/glitchtip.py).
Сообщение уходит в существующую тему клиентских инцидентов с явной пометкой
«резервный канал». Секрет свой (ALERT_ACK_GLITCHTIP_SECRET), не переиспользует
продуктовый TRADEIN_INTERNAL_AUTH_SECRET.

Refs #3471
2026-09-12 14:01:30 +03:00
bot-backend
93451fae08 chore(ops): выключить встроенный Grafana Alerting, единственный путь — Alertmanager
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 14s
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 / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Проверка живого API Grafana 12.09.2026: правил алертинга ноль, контакт-поинт
единственный и стоковый — grafana-default-email на example@email.com,
GF_SMTP_* не заданы. Интерфейс выглядит настроенным, кнопка "New alert rule"
работает, а результат молча уходит в никуда — ровно тот случай, из-за
которого никто не проверяет второй, настоящий путь доставки.

Дублирующий движок на том же датасорсе Prometheus надёжности не добавляет
(общая точка отказа), а поддержку удваивает. Решение: Alertmanager —
единственный путь доставки тревог, Grafana только рисует.

GF_UNIFIED_ALERTING_ENABLED: "false" в docker-compose.metrics.yml (секция
grafana) — единственная официальная секция [unified_alerting] в Grafana 11.x,
легаси-[alerting] удалён из Grafana ещё в 9.0 (сверено с grafana.com/docs/
grafana/v11.5/setup-grafana/configure-grafana/#unified_alerting). Разом
убирает Alerting из UI и глушит движок правил, так что искать и вычищать
стоковый контакт-поинт отдельно не требуется.

ops/metrics/grafana/provisioning/alerting/README.md — явная отметка для
следующего человека: провижинить contact points/rules в эту папку не нужно,
Grafana её при выключенном unified alerting не читает.

Closes #3158, refs #3471
2026-09-12 14:01:04 +03:00
bot-backend
b90872b5d7 feat(ops/metrics): инфра-алерты уезжают в тему «метрики», клиенты остаются в «алертах»
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / changes (pull_request) Successful in 9s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m57s
CI / backend-tests (pull_request) Successful in 17m26s
Форумная группа имеет три темы, но тема «метрики» была пуста: оба прямых
получателя Alertmanager — telegram и telegram-heartbeat — брали топик из той
же переменной METRICS_TELEGRAM_TOPIC_ID, что и сервис alert-ack. Развести их
было нечем, и heartbeat вместе со всем инфраструктурным шумом падал в ленту
клиентских инцидентов.

Смешанные в одной теме, инфраструктура и клиентский инцидент не равны по
срочности и приучают пролистывать обе.

Вводится METRICS_TELEGRAM_INFRA_TOPIC_ID для прямых получателей Alertmanager.
alert-ack и вебхук GlitchTip остаются на прежней переменной, тема поддержки
не тронута. Пока новая переменная не задана, берётся старая — до этого момента
поведение ровно прежнее, а не сломанное.

Клиентская METRICS_TELEGRAM_TOPIC_LINE убрана целиком: после переезда обоих
получателей на инфраструктурную строку шаблон её не содержит, а деплой
продолжал бы её собирать и объявлять в envsubst. Тест, закрепляющий сборку
такой строки, зеленел бы вечно и мешал бы её убрать.

Проверено рендером, а не чтением: при заданной теме telegram и
telegram-heartbeat дают 245, telegram-clients уходит вебхуком без темы; при
незаданной — поля message_thread_id нет вовсе (пустое значение уронило бы
Alertmanager целиком). Логика отката прогнана во всех трёх состояниях
переменных. backend/tests/ops — 86 passed.

Closes #3163
2026-08-27 21:50:51 +03:00
bot-backend
790691d0b4 fix(ops/metrics): Alertmanager отвечал 404 на /api/v2/alerts — Prometheus писал мимо
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Приёмочная проверка #3155 показала второй отказ на том же пути. Prometheus
нашёл приёмник (`activeAlertmanagers` непуст), отправил уведомление — и
получил 404: `alertmanager_alerts_received_total 0` при
`prometheus_notifications_errors_total 1` и `dropped_total 1`.

`--web.external-url=…/alertmanager` без `--web.route-prefix=/` заставляет
Alertmanager обслуживать ВСЁ под этим префиксом. Проверено прямой пробой из
контейнера Prometheus: `/api/v2/status` → 404, `/alertmanager/api/v2/status`
→ 200. Снаружи префикс при этом никому не нужен: маршрута `/alertmanager`
в Caddy нет вовсе, внешний адрес используется только для ссылок в сообщениях.

Симптом этого же префикса уже ловили 27.08 на healthcheck — и вылечили на
стороне проверки, прописав ей путь с префиксом. Из-за этого причина осталась
жива и продолжила ломать то, что чинить куда важнее. Возвращаю обычный путь
healthcheck вместе с флагом.

Заодно оживает датасорс Alertmanager в Grafana
(`ops/metrics/grafana/provisioning/datasources/datasources.yml:37`) — он
настроен на корень и до сих пор упирался в тот же 404.

Closes #3161
2026-08-27 21:16:06 +03:00
bot-backend
99125b8093 fix(ops/metrics): Prometheus не видел ни одного Alertmanager — цель file_sd осталась пустой
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Профиль alerts включили, а файл целей `alertmanager_targets.yml` остался
плейсхолдером `[]`. Снаружи всё зелёное: alertmanager и alert-ack Up/healthy,
деплой зелёный, в логах ни одной ошибки — при этом `activeAlertmanagers: []`
и 1568 уведомлений в `prometheus_notifications_dropped_total`. Горящий с 26.08
`Watchdog` не доехал никуда, как и HostAgentDown с RemoteWriteStalled.

Причина в том, что включатель профиля и цель для Prometheus лежали в разных
местах: профиль поднимает deploy-metrics.yml по наличию токена и чата, а файл
целей правился руками. Разъезд не ловится ничем — `[]` штатен при выключенном
профиле, поэтому ни валидация, ни healthcheck, ни лог на него не реагируют.

Файл становится производным (`alertmanager_targets.gen.yml`, в .gitignore) и
рендерится деплоем тем же условием, что включает профиль: цель при включённых
алертах, `[]` при выключенных. Рендер идёт до `up` и пишет усечением на месте,
поэтому инод сохраняется и работающий Prometheus подхватывает цель сам — та же
ловушка одиночного бинд-маунта, что уже описана в этом workflow у Alertmanager.

Пустой список пишется явно, а не удалением файла: несуществующий путь docker
подменяет каталогом, и Prometheus не стартует вовсе.

Closes #3155
2026-08-27 21:10:25 +03:00
bot-backend
053a5fb75c feat(observability): кнопка «Принял в работу» под клиентским инцидентом (#3078)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Failing after 1m12s
CI / openapi-codegen-check (pull_request) Successful in 1m55s
Alertmanager инлайн-клавиатуру не поддерживает, а без кнопки нет обратной
связи «человек увидел и взял в работу»: 27.08 продукты лежали 10 часов, и
вопрос «а кто-нибудь это читает» было не к кому адресовать.

ГДЕ ЖИВЁТ. Рядом с Alertmanager, на инфраструктурной машине. У бота МЕРЫ
уже есть приём обновлений, и повесить обработку туда было бы дешевле, но
он работает на продуктовом хосте: при падении продукта кнопка оказалась бы
мёртвой ровно тогда, когда нужна.

ССЫЛКА, А НЕ CALLBACK. Callback требует читателя обновлений бота. Бот один,
и его обновления уже читает МЕРА — второй читатель получил бы 409 Conflict
и отобрал бы сообщения у поддержки.

БЕЗ ПАРОЛЯ НА /ack/*, ОСОЗНАННО. Кнопку жмут ночью с телефона, когда лежит
прод; требование пароля даст ноль нажатий. Защита — 128-битный токен под
конкретное сообщение, живущий сутки; максимум, чего добьётся угадавший, —
ложная отметка в чате, где сразу видно, что её поставил не человек.

ТОЛЬКО КЛИЕНТСКИЙ МАРШРУТ идёт через сервис. Прочие алерты сохраняют прямой
путь в Telegram: чем меньше звеньев, тем надёжнее. Если сервис лёг,
Alertmanager повторяет доставку и переуведомляет каждые 30 минут — алерт
задерживается, но не теряется. Дублировать вторым прямым каналом не стали:
шум в канале тревог опаснее задержки.

Девять тестов дёргают настоящие функции, подменяя один шов — вызов Bot API.
Важнейший: при отказе отправки с клавиатурой сообщение уходит БЕЗ неё —
алерт важнее кнопки.
2026-08-27 13:56:00 +03:00
bot-backend
ede45f2a31 fix(observability): healthcheck Alertmanager стучится по правильному пути — контейнер был вечно unhealthy
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 15s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
--web.external-url=…/alertmanager заставляет Alertmanager обслуживать ВСЁ
под этим префиксом, включая служебные ручки. Проверено на проде:

    /-/healthy               → 404 Not Found
    /alertmanager/-/healthy  → OK

Контейнер при этом работал и рассылал алерты, но docker держал его
unhealthy бессрочно. Само по себе безобидно — и ровно поэтому вредно:
статус, который всегда красный, приучают не смотреть. А любая автоматика
вида «перезапусти нездоровое» устроила бы из этого вечный перезапуск
канала оповещений — то есть выбивала бы именно то, что должно работать
в аварию.
2026-08-27 13:48:57 +03:00
bot-backend
c6e15954ba fix(observability): контейнерные метрики терялись целиком + два хвоста стека
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 Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Три независимых дефекта, найденных на живом проде после подъёма стека метрик.

1. cAdvisor-метрики не доезжали ВООБЩЕ. В Prometheus ноль имён container_*
   при 2034 именах всего, хотя cAdvisor отдаёт 880 рядов, scrape-таргет в
   alloy health=up с последним скрейпом 10 мс назад, а remote_write рабочий
   (node/postgres идут через него же и доезжают). Методом исключения — потери
   в prometheus.relabel.cadvisor_trim, во втором правиле:

       rule { source_labels = ["name"], regex = "", action = "drop" }

   Замысел был выкинуть безымянные cgroup-ряды (id="/"). Но regex в Alloy
   документированно дефолтится в (.*), и пустая строка неотличима от
   незаданного значения — такой drop рискует выкидывать вообще всё, что и
   наблюдалось. Заменено на однозначное keep regex=".+" — тот же замысел,
   без зависимости от того, как трактуется пустой regex.

2. healthcheck alloy не мог пройти никогда: дёргал wget, которого в образе
   grafana/alloy нет (как и curl, и nc). Контейнер вечно unhealthy при
   полностью исправном alloy — ложная тревога, маскирующая настоящие сбои.
   Заменено на сырой HTTP через bash /dev/tcp, без внешних утилит.

3. Prometheus раз в минуту писал "lookup alertmanager: no such host" и держал
   up{job="alertmanager"}=0. Alertmanager намеренно за профилем alerts до
   решения #3078 — дефект не в профиле, а в безусловной ссылке на сервис.
   Оба места (alerting.alertmanagers и job_name: alertmanager) переведены на
   file_sd_configs с файлом целей, по умолчанию пустым: целей нет — ошибок
   тоже нет. Prometheus перечитывает file_sd на лету, поэтому включение
   профиля сведётся к наполнению файла, без рестарта и правки конфига.
   Файл целей смонтирован в сервис prometheus явным volume.

Проверено на живом хосте, не на глаз:
- alloy fmt обоих .alloy в одноразовом контейнере grafana/alloy:v1.6.1 - exit 0
- promtool check config в prom/prometheus:v3.1.0 - valid, 16 rules found
- механизм нового healthcheck выполнен внутри работающего gendesign-alloy:
  первая строка ответа "HTTP/1.0 200 OK", grep матчится, RESULT=HEALTHY
- наличие bash/head/grep/printf в образе alloy подтверждено command -v
2026-08-26 13:14:28 +03:00
bot-backend
309d273f3f feat(observability): стек метрик и логов — Prometheus, Loki, Grafana, агенты на обоих хостах
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
Метрик в проекте не было ни одной: ни экспортеров, ни /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