Commit graph

3017 commits

Author SHA1 Message Date
bot-backend
8945ea5d04 fix(ops): оповещения уходят в тему «алерты», а не в переговорку (#2203)
Some checks failed
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 10s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Failing after 1m6s
CI / openapi-codegen-check (pull_request) Successful in 1m55s
Канал включили — и алерты бэкапов посыпались в ОБЩУЮ тему форума. В форуме
Telegram адрес сообщения это пара «чат + тема»: без message_thread_id всё
попадает в General, причём без единой ошибки. sendMessage возвращает 200,
доставка «успешна», просто не туда.

Отказ того же класса, что и всё остальное сегодня: зелено везде, а человек,
которому адресован алерт, его не видит.

Оба отправителя (ops/lib-backup.sh и ops/uptime-healthcheck.sh) получили
условную подстановку ${TELEGRAM_TOPIC_ID:+-d "message_thread_id=..."}.
Условная намеренно: пустой message_thread_id= Telegram отвергает вместе со
всем сообщением, а молчащий алерт хуже алерта не в той теме. Нет переменной —
нет параметра, поведение прежнее бит в бит.

Тест ИСПОЛНЯЕТ настоящий notify(), извлечённый из файла построчно, подсовывая
подставной curl и проверяя, что реально ушло бы в сеть. Проверять подстроку в
файле бессмысленно: она может стоять в мёртвой ветке. Копировать функцию в
тест — тоже: копия разойдётся с оригиналом на первой правке. Сорсить файл
целиком нельзя: у uptime-healthcheck.sh нет guard'а по BASH_SOURCE, и сорсинг
запустил бы настоящие сетевые проверки.
2026-08-27 13:12:10 +03:00
51c779c092 Merge pull request 'fix(observability): конфиг Alertmanager читается контейнером — гейт падал на своих же правах (#3078)' (#3127) from fix/3078-alertmanager-config-perms into main
All checks were successful
Deploy Metrics / server (push) Successful in 13s
Deploy Metrics / agent-infra (push) Successful in 20s
Deploy Metrics / agent-apps (push) Successful in 22s
2026-08-27 10:01:37 +00:00
bot-backend
630f3e6e76 fix(observability): конфиг Alertmanager читается контейнером — гейт падал на своих же правах (#3078)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
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 / backend-tests (pull_request) Has been skipped
CI / frontend-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
Как только канал алертов реально включился (#3126), профиль alerts впервые
дошёл до проверки конфига — и деплой встал:

    amtool: error: failed to validate 1 file(s)
    Checking '/tmp/am.yml'  FAILED: open /tmp/am.yml: permission denied

Отрендеренный конфиг пишется с правами 600 и принадлежит деплой-пользователю,
а и amtool, и сам Alertmanager в образе prom/alertmanager работают под nobody
(65534). Прочитать чужой файл 600 они не могут. Проверка падала не на
содержимом конфига, а на доступе к нему — и контейнер после подъёма упал бы
ровно там же.

Дефект не поймали раньше по понятной причине: без токена профиль alerts не
включался, и эта ветка не исполнялась НИ РАЗУ с момента появления гейта в
#3111. Проверка, которая никогда не запускалась, ничем не отличается от
отсутствующей — это ровно тот класс тихой поломки, ради которого весь стек и
заводится.

Права не ослабляем: в файле лежит токен бота. Вместо chmod 644 (который внёс
бы токен в список файлов, читаемых любым локальным пользователем машины)
отдаём файл во владение 65534 одноразовым контейнером от root — passwordless
sudo на хосте нет, а бинд-маунт правит host-инод напрямую. Доступ остаётся
ровно у того, кто конфиг читает.

Следствие, которое легко проглядеть: после смены владельца `>` в этот файл
на следующем деплое уже не запишет, поэтому добавлен rm перед рендером —
каталог принадлежит деплой-пользователю, пересоздать файл он может.
2026-08-27 12:58:15 +03:00
70db090813 Merge pull request 'feat(ops): сторож расхождения «прод ↔ main» — недоехавший деплой перестаёт быть молчаливым (#3029)' (#3125) from chore/3029-deploy-drift-guard into main
All checks were successful
deploy-drift / drift (push) Successful in 8s
Deploy / changes (push) Successful in 8s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 35s
Deploy / build-worker (push) Successful in 37s
Deploy / deploy (push) Successful in 1m1s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 10s
2026-08-27 09:55:03 +00:00
5ffd4fe28e Merge pull request 'feat(observability): токен алертов приходит из секретов Actions, а не только с машины (#3078)' (#3126) from feat/3078-alert-secrets-from-actions into main
Some checks failed
Deploy Metrics / server (push) Failing after 13s
Deploy Metrics / agent-apps (push) Has been skipped
Deploy Metrics / agent-infra (push) Has been skipped
2026-08-27 09:53:56 +00:00
bot-backend
30a21abfa7 feat(observability): токен алертов приходит из секретов Actions, а не только с машины (#3078)
All checks were successful
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 Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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
Стек наблюдаемости был готов ещё вчера, но Alertmanager не поднимался:
профиль alerts включается, только когда заданы METRICS_TELEGRAM_BOT_TOKEN и
METRICS_TELEGRAM_CHAT_ID, а читались они ИСКЛЮЧИТЕЛЬНО из файла окружения на
инфраструктурной машине. То есть включить алерты можно было только правкой
прод-файла руками по ssh — в обход репозитория, без следа в истории и без
возможности сделать это из CI.

Именно это и держало задачу открытой дольше нужного. Цена промедления
измерена: 27.08 продукты лежали 10 часов, и ни одно звено оповещения не
сработало (#3119); в тот же день сутки не доезжал деплой, и об этом тоже
никто не узнал (#3029).

Теперь три переменные форвардятся в ssh-шаг из секретов Actions. Порядок
разрешения сохранён осознанно: инжектированные значения ставятся ДО того,
как скрипт подхватит окружение машины, поэтому хост, если ключи заданы на
нём, переопределяет секреты — последнее слово остаётся за машиной, а
секреты работают как разумный дефолт.

Канал проверен вживую: бот MERAsupport_bot, форум «МЕРА», тема «алерты»
(message_thread_id=158) — пробное сообщение доставлено.
2026-08-27 12:52:25 +03:00
ff2335899e Merge pull request 'fix(tradein/scheduler): boot-reap виден и при нуле (#3122)' (#3124) from fix/3122-boot-reap-observable into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m52s
Deploy Trade-In / build-backend (push) Successful in 1m36s
Deploy Trade-In / deploy (push) Successful in 2m5s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-27 09:39:06 +00:00
bot-backend
de56b8ae01 feat(ops): сторож расхождения «прод ↔ main» — молчаливый недоехавший деплой становится видимым (#3029)
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 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m8s
CI / backend-tests (pull_request) Successful in 17m30s
27.08 прод сутки жил на позавчерашнем коммите, и это нашлось только руками.
Замена IP сервера (#3110) осиротила секрет DEPLOY_HOST: deploy.yml падал на
`dial tcp ***:***: i/o timeout`, при этом CI оставался зелёным, PR
продолжали мержиться, а deploy-infra.yml исправно обновлял Beget и создавал
впечатление, что всё в порядке.

Проверок, которые спрашивают не «прошёл ли прогон», а «доехал ли код», не
было ни одной. Этот сторож — ровно такая: ежечасно читает HEAD с прод-хоста
и сверяет с tip main.

Три исхода вместо двух. «Хост не ответил» (2) отделён от «на хосте не тот
код» (1) — это разные аварии с разной первой командой в разборе, и сегодня
погорели именно на их смешении: недоступность выглядела как обычный красный
прогон. Льготный период 30 минут гасит ложную тревогу на деплое, который
ещё в полёте: ежечасный сторож неизбежно попадёт в окно между мержем и
концом выката, а сторож, которого научились игнорировать, хуже отсутствующего.

Логика вынесена в scripts/check-deploy-drift.sh и не ходит по сети — SSH
живёт в workflow, где секреты. Благодаря этому тест ИСПОЛНЯЕТ настоящий
скрипт, а не пересказывает его: ошибка в самом bash видна только при запуске.

Read-only: один ssh и git rev-parse, ничего не деплоит.
2026-08-27 12:35:43 +03:00
3c315b5105 fix(tradein/scheduler): boot-reap виден и при нуле — молчащий механизм неотличим от неподключённого (#3122)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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
CI Trade-In / backend-tests (pull_request) Successful in 4m45s
Первая живая приёмка #3122: деплоевский startup-reap успел снять зомби
раньше, boot-reap отработал с нулём — и не оставил НИКАКОГО следа
исполнения. Ноль — штатный исход, но «сторож, молчащий при нуле» — это
слепая зона по построению: оборванную проводку не отличить от чистого
прода. INFO-строка при нуле, warning при снятых — как было.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:33:06 +05:00
dea08a0523 Merge pull request 'feat(tradein/scheduler): boot-reap — прогоны предыдущего контейнера снимаются на старте (#3122)' (#3123) from feat/3122-boot-reap into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m50s
Deploy Trade-In / build-backend (push) Successful in 1m37s
Deploy Trade-In / deploy (push) Successful in 1m58s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-08-27 09:23:44 +00:00
63956f0891 feat(tradein/scheduler): boot-reap — прогоны предыдущего контейнера снимаются на старте (#3122)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 11s
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
CI Trade-In / backend-tests (pull_request) Successful in 4m40s
Прод-факт 27.08 (после ночного офлайна #3119): 4 прогона 'running' со
стартами до старта контейнера блокировали свои источники через
has_running_run до 6-часового порогового reap'а — до пяти часов слепоты
на источник ровно после простоя, когда догон нужнее всего.

Критерий — started_at < старт процесса планировщика (минус минута на
дрейф), пульс не участвует: ложные срабатывания класса #2702 (редкий
пульс длинных прогонов) невозможны по построению — живой прогон этого
процесса не может быть старше самого процесса.

Маркер counters.boot_reaped=true открывает boot-зомби подхват чекпоинта
(_resume_decision): у порогового zombie процесс может быть жив
(движущаяся точка — причина исключения 'zombie' из _RESUME_STATUSES),
у boot-зомби — гарантированно мёртв. Пороговый zombie без маркера
по-прежнему отвергается (закреплено тестом-инвариантом).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:17:42 +05:00
c6c934fb99 Merge pull request 'feat(tradein/matching): region_code у houses — вывод, а не выдумка (#3051 п.4)' (#3121) from feat/3051-houses-region-code into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 20s
Deploy Trade-In / build-browser (push) Successful in 36s
Deploy Trade-In / build-frontend (push) Successful in 2m30s
Deploy Trade-In / test (push) Successful in 3m54s
Deploy Trade-In / build-backend (push) Successful in 1m36s
Deploy Trade-In / deploy (push) Successful in 6m47s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-08-27 09:01:08 +00:00
27fdd55aae feat(tradein/matching): region_code у houses — вывод, а не выдумка (#3051 п.4)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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
CI Trade-In / backend-tests (pull_request) Successful in 4m32s
Миграция 272: колонка + бэкфилл ТОЛЬКО по bbox региона 66 (значения
байт-в-байт из реестра, синхронизацию держит тест). Карантин NULL:
620 домов без geom, 23 порченых (ЕКБ-адреса с чужими координатами —
«Вильгельма де Геннина» на Байкале, «Крауля» под Москвой, «Учителей» в
Таллине, с живыми ссылками листингов) — им регион не присваивается,
включая 3 дома с координатами в bbox Москвы (порча, не переезд). NOT NULL
из постановки — отдельной миграцией, когда карантин опустеет.

Запись: новый дом наследует регион РАЗВЁРТКИ (base.py → контракт →
адаптер → matching), только если координаты не противоречат; координаты
другого региона → NULL-карантин + warning. Вне-bbox координаты при живой
развёртке наследуют её регион (адрес и развёртка согласны, координатам
веры нет) — семантика закреплена тестом, чтобы смена была осознанной.

Матчинг-запросы НЕ тронуты — гард по региону это #3052.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:51:48 +05:00
094dbd0fc2 Merge pull request 'chore(ops): адрес Poincare — 188.124.37.140 вместо заменённого 188.246.224.93 (#3110)' (#3120) from chore/3110-new-server-ip into main
Some checks failed
Deploy / deploy-caddy (push) Has been skipped
Deploy Metrics / agent-infra (push) Successful in 25s
Deploy / deploy (push) Failing after 49s
Deploy / perimeter-smoke (push) Has been skipped
Deploy Infra Host / sync-infra-host (push) Successful in 5s
Deploy / build-frontend (push) Successful in 40s
Deploy / build-backend (push) Successful in 41s
Deploy / build-worker (push) Successful in 42s
Deploy / deploy-status (push) Failing after 1s
Deploy / changes (push) Successful in 9s
Deploy Metrics / server (push) Successful in 12s
Deploy Metrics / agent-apps (push) Failing after 43s
2026-08-27 08:45:23 +00:00
bot-backend
efaab2efda chore(ops): адрес Poincare — 188.124.37.140 вместо заменённого 188.246.224.93 (#3110)
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-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 / changes (pull_request) Successful in 9s
CI / openapi-codegen-check (pull_request) Successful in 1m58s
CI / backend-tests (pull_request) Successful in 17m16s
Selectel заменил адрес сервера в ночь на 27.08 по нашей заявке: старый
188.246.224.93 фильтровался российскими операторами и не открывался ни с
домашнего интернета, ни с мобильного (#3110). Новый адрес фильтрации не имеет —
проверено с машины владельца после переключения DNS.

Замена уронила сервер на 10 часов: адрес на порте сменился, а в ОС остался
прежний (разбор и восстановление — #3119). Здесь только то, что осталось в
репозитории.

Единственное функциональное вхождение — дефолт HOST_IP в ops/selectel-ci-access.sh:
скрипт открывает раннеру доступ на прод-хост, и с прежним значением он молча
настроил бы доступ на адрес, которого у нас больше нет. Остальные семь — тексты
комментариев в Caddyfile-секциях, compose, bootstrap-скриптах и раннбуке крона;
они не исполняются, но именно по ним сверяются при переезде, и разошедшийся
адрес в них дороже, чем кажется.
2026-08-27 11:23:37 +03:00
8a4215fe20 Merge pull request 'feat(tradein/geo): реестр регионов — единственный источник гео-границ (#3051, шард 1a)' (#3116) from feat/3051-region-registry into main
All checks were successful
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m51s
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 7m42s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
Deploy Trade-In / build-backend (push) Successful in 57s
Deploy Trade-In / deploy-status (push) Successful in 1s
2026-08-26 15:23:59 +00:00
54438039e5 Merge pull request 'feat(tradein/scraper): чекпоинт по якорям для cian_city_sweep (#3074)' (#3117) from feat/3074-cian-anchor-checkpoint into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m39s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m48s
Deploy Trade-In / deploy (push) Successful in 1m36s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-08-26 14:28:15 +00:00
bot-backend
68daac2a30 feat(tradein/scraper): чекпоинт по якорям для cian_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m43s
Третий шард после yandex (#3098) и avito (#3112). Выбран по замеру за 60 дней:
65 прогонов, среднее 35 минут, максимум 72, две отмены деплоем. Пятиминутного
дренажа (#3029) на такие прогоны не хватает - убитый на 35-й минуте сбор
начинался заново с первого якоря.

Ключ чекпоинта - ИМЯ якоря, а не индекс: состав списка зависит от city_slug
(областные свипы идут по своим наборам), позиция между городами не устойчива.
По той же причине гарда по числу якорей не нужна - в отличие от combo-чекпоинта
яндекса, где ключ якоря не содержал.

Отличие от avito-шарда: там успех и неудача якоря сходились в одной строке и
потребовался отдельный флаг _anchor_ok. У циана граница уже проведена самим
потоком управления - все ветки отказа делают return или continue и до записи
чекпоинта не доходят. Добавлять флаг значило бы дублировать то, что уже
выражено структурой; достаточно писать чекпоинт в единственной точке успеха.
Тест сторожит эту границу отдельно, потому что рефакторинг, сливающий ветки,
сломал бы её незаметно.

Пропущенный якорь двигает anchors_done - чтобы счётчик продолжал означать
"докуда дошли по списку", а не "сколько собрал именно этот прогон".

Тесты (4) поведенческие, с подменой CianScraper и save_listings: якорь из
чекпоинта не опрашивается вовсе; пройденный дописывается поверх унаследованных;
без чекпоинта обходятся все; упавший в чекпоинт не попадает. Двойнику пришлось
добавить счётчики state_extraction_* - конвейер читает их после каждого якоря
(#2625), и без них падал бы сам двойник, а не проверяемая логика.

Фальсификация: на исходном коде краснеют все 4. Весь набор #3074 (yandex, avito,
cian, claim) - 14 passed.
2026-08-26 17:21:58 +03:00
24369a6bf2 test(3051): tests/services/test_location_index.py переведён на _covered_region
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m38s
CI поймал то, что мой локальный прогон пропустил: сьют в ПОДДИРЕКТОРИИ
tests/services/ звал удалённый _in_ekb_bbox. Граничные точки сохранены те же
(продукт-ядро 66 байт-в-байт), проверка дополнена кодом региона.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:47:29 +05:00
35d46f3526 Merge pull request 'fix(observability): пароли БД не уезжают в Loki — скруббер учётных данных в Alloy (#3114)' (#3115) from fix/3114-scrub-credentials-in-logs into main
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Metrics / server (push) Successful in 14s
Deploy Metrics / agent-infra (push) Successful in 21s
Deploy Metrics / agent-apps (push) Successful in 23s
Deploy / build-backend (push) Successful in 39s
Deploy / build-worker (push) Successful in 40s
Deploy / deploy (push) Successful in 1m3s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 10s
2026-08-26 13:44:17 +00:00
0529bb249c feat(tradein/geo): реестр регионов — единственный источник гео-границ (#3051, шард 1a)
Some checks failed
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
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 Trade-In / backend-tests (pull_request) Failing after 4m39s
Границы покрытия лежали литералами в трёх файлах (location_index / geocoder /
matching.normalize), и каждая молча отвергла бы Москву. Новый модуль
app.services.regions — лист дерева импортов — держит per-регион bbox'ы
(tight/wide/region/product_core), города, city_token и набор доступных тиров
обогащения; потребители держат прежние имена как алиасы на объекты реестра
(identity закреплена тестом — копии, разъезжающиеся при правке, невозможны).

Регион 66 — байт-в-байт прежние литералы (закреплено тестом: этот PR только
переносит границы, менять их = отдельное решение). Регион 77 (Москва): МКАД-
ядро + генеральный bbox с Новой Москвой и Зеленоградом; тиров обогащения НЕТ
ни одного — и это явный факт реестра с готовой формулировкой
(unsupported_tier_reason), а не молчаливое «посчитаем без источника».

Приёмка #3051: точка 55.75/37.62 больше не out_of_coverage — location_index
узнаёт регион 77 и считает в его ядре (сегодня листингов Москвы нет → честный
insufficient_data). Область 50 отложена по решению в #2996.

Не здесь (следующие шарды): city_fias_id сквозняком (п.2), doc_type в deals
(п.3), region_code у houses (п.4), депромоут описаний (п.5), параметры
загрузчиков (п.6). Гейт ЕКБ-тиров геокодера (#2582) уже деградирует правильно
для Москвы — fail-closed открывает их только при подтверждённом ЕКБ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:39:28 +05:00
036608f1bc Merge pull request 'fix(tradein/estimate): фильтр свежести в якоре дома и знаменателе коэффициента выкупа (#2656)' (#2661) from fix/2656-anchor-ratio-freshness into main
All checks were successful
Deploy Trade-In / test (push) Successful in 3m48s
Deploy Trade-In / build-backend (push) Successful in 1m8s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 1m56s
Deploy Trade-In / deploy-status (push) Successful in 1s
2026-08-26 13:31:53 +00:00
7a5997b059 test(2656): краснота на main — по значению, не ImportError
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
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
CI Trade-In / backend-tests (pull_request) Successful in 4m39s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
getattr вместо прямого импорта LISTINGS_FRESH_DAYS из config: на origin/main
константы там ещё нет, и тест умирал ImportError'ом на сборке модуля —
«возможности нет» вместо «значение неверно». Теперь на main: 5 красных
ассертами (предикат отсутствует/окно None/протухшие комплы в пуле), 3 зелёных
(сброс anchor_tier уже влит отдельно).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:25:54 +05:00
3d0541b2e2 Merge origin/main into fix/2656-anchor-ratio-freshness — конфликты: метки #2656→#2661 (main), импорт-union, baseline unrecorded=0 (перезахваченная фикстура) 2026-08-26 18:22:54 +05:00
bot-backend
c093eaafe5 fix(observability): пароли не уезжают в Loki — скруббер учётных данных (#3114)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m58s
CI / backend-tests (pull_request) Successful in 17m33s
postgres_exporter при неудачном скрейпе печатает полный DSN вместе с паролем.
Замер по Loki за сутки: 104 такие строки на инфра-хосте и 106 на продуктовом.
При ретенции 30 дней это порядка 6000 строк с паролями БД в хранилище, доступ
к которому даёт вход в Grafana - причём внешний basic_auth с витрины сегодня
же снят (#3113).

Проверено экспериментом, а не предположено. Напрашивалось передать пароль
отдельно от строки подключения (DATA_SOURCE_URI + DATA_SOURCE_USER +
DATA_SOURCE_PASS_FILE). Прогон на скретч-контейнерах с настоящим файлом
запросов показал: НЕ помогает - экспортер собирает строку сам и логирует её
целиком. Первые три попытки воспроизведения были неинформативны (контейнер
падал сразу; скрейпа не было; не подключён файл кастомных запросов) - утечка
воспроизводится только при неудачном скрейпе с нашим queries.yml.

Стало: ступень loki.process между источником журнала и loki.write, маскирует
пароль в любом URL вида scheme://user:pass@host. Пользователь и адрес
остаются - без них строка ошибки перестаёт годиться для диагностики. Ровно
одна группа захвата: Alloy заменяет содержимое групп, вторая затёрла бы имя
пользователя.

Это защита в глубину, а не замена причине. Конкретно эта ошибка уходит грантом
pg_monitor (запрос pg_wal_bytes в нашем queries.yml требует pg_ls_waldir) -
это боевая БД и остаётся за владельцем. Скруббер же ловит любой пароль в URL,
включая компоненты, о которых мы ещё не знаем.

Регулярка без экранирования намеренно: Alloy не принимает \s в строке
(unknown escape sequence), поэтому класс задан явным пробелом. Оба конфига
прогнаны через `alloy fmt` образом grafana/alloy:v1.6.1 - синтаксис ok.

Тесты (8) берут выражение ИЗ КОНФИГА и применяют к настоящей строке из прода:
пароль исчезает; пользователь и адрес остаются; обычные URL и почтовые адреса
не портятся; журнал направлен в скруббер, а не мимо него. Фальсификация: на
исходных конфигах краснеют все 8. tests/ops целиком - 43 passed.
2026-08-26 15:50:57 +03:00
6dd2d26c60 Merge pull request 'fix(observability): у Grafana остаётся один вход — внешний basic_auth снят с витрины (#3078)' (#3113) from fix/3078-grafana-single-auth into main
All checks were successful
Deploy Infra Host / sync-infra-host (push) Successful in 6s
Deploy / changes (push) Successful in 8s
Deploy Metrics / server (push) Successful in 12s
Deploy / deploy-caddy (push) Has been skipped
Deploy Metrics / agent-infra (push) Successful in 26s
Deploy Metrics / agent-apps (push) Successful in 27s
Deploy / build-worker (push) Successful in 38s
Deploy / build-frontend (push) Successful in 40s
Deploy / build-backend (push) Successful in 43s
Deploy / deploy (push) Successful in 1m4s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 10s
2026-08-26 12:25:41 +00:00
bot-backend
f893662ef1 fix(observability): у Grafana остаётся один вход (#3078)
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 / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Внешний basic_auth Caddy снят с витрины по решению владельца: два запроса
пароля подряд мешали работе, а у Grafana есть собственная аутентификация с
ролями и GF_USERS_ALLOW_SIGN_UP=false.

Что теряется, чтобы решение было осознанным: basic_auth отсекал сканеры до
Grafana и прикрыл бы её собственную будущую уязвимость. Теперь страница входа
видна из интернета напрямую. Это записано комментарием прямо в конфиге, чтобы
через полгода не выглядело недосмотром.

Приём метрик НАРОЧНО остаётся под basic_auth: туда ходит агент по паролю,
который лежит в открытом виде в окружении продуктового хоста, и отдельная
учётка там ограничивает ущерб записью. Снят ровно один слой и ровно с витрины.

Файл metrics-ui.caddy.snippet и его bind-mount оставлены на месте: диффу так
меньше, вернуть слой можно одной строкой. Гард импортов (#3104) проверяет
обратное направление - что каждый import покрыт маунтом, - поэтому лишний
маунт его не трогает.

Проверено: scripts/check-caddy-snippet-mounts.py зелёный (6 конфигов,
7 маунтов); конфиг с новой версией infra.caddy проходит caddy validate в
одноразовом контейнере на Beget - Valid configuration.
2026-08-26 15:21:04 +03:00
17548e9046 Merge pull request 'feat(tradein/scraper): чекпоинт по якорям для avito_city_sweep (#3074)' (#3112) from feat/3074-avito-anchor-checkpoint into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m41s
Deploy Trade-In / test (push) Successful in 3m57s
Deploy Trade-In / deploy (push) Successful in 1m24s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
Deploy Trade-In / deploy-status (push) Successful in 2s
Reviewed-on: #3112
2026-08-26 12:03:08 +00:00
bot-backend
0586951772 feat(tradein/scraper): чекпоинт по якорям для avito_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (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 Trade-In / backend-tests (pull_request) Successful in 4m47s
Продолжение после yandex-свипа. Выбор источника — по замеру, а не «для
полноты»: за 60 дней avito_city_sweep дал 68 прогонов, 25 банов и 3 отмены
деплоем при среднем времени 15 мин и максимуме 97.

Прод-факт, который решает дело. Типичный итог свипа:

    {"anchors_done": 1, "anchors_total": 5, ...,
     "enrichment_abort_note": "detail enrichment aborted (Avito detail
      firewall/soft-block ...)"}

Прогон срывается блокировкой на ПЕРВОМ из пяти якорей. Без чекпоинта
следующий прогон снова идёт в первый якорь, упирается в ту же стену, и якоря
2-5 не собираются никогда.

Ключ чекпоинта — ИМЯ якоря, а не индекс: состав списка зависит от city_slug,
позиция в нём между городами не устойчива. По той же причине здесь не нужна
гарда по числу якорей, которая есть у combo-чекпоинта яндекса: там ключ
якоря не содержал, здесь якорь и есть ключ.

Инвариант, ради которого отдельный флаг _anchor_ok: в чекпоинт попадает
только якорь, пройденный до конца. Ветка блокировки делает return и до записи
не доходит, а generic-except доходит — якорь упал, но цикл продолжается.
Записать такой якорь пройденным значило бы, что следующий прогон пропустит
его навсегда, причём молча: прогон завершится штатно, просто часть города не
соберётся. Флаг сбрасывается на каждой итерации, иначе один упавший якорь
заразил бы все последующие.

Пропущенный якорь двигает anchors_done — чтобы счётчик продолжал означать
«докуда дошли по списку», а не «сколько собрал именно этот прогон».

domclick_city_sweep намеренно НЕ трогаю: 54 прогона, ноль отмен деплоем,
среднее время 3 минуты — чекпоинт там не окупается. cian_city_sweep (среднее
35 мин, 2 отмены) — следующий шард.

Тесты (4) поведенческие: якорь из чекпоинта не опрашивается вовсе; пройденный
дописывается поверх унаследованных; без чекпоинта обходятся все; упавший в
чекпоинт НЕ попадает. Оговорка: на исходном коде они падают по сигнатуре
(unexpected keyword argument), то есть доказывают отсутствие параметра, а не
поведение — поведенческую часть держат сами проверки. Весь набор #3074 —
10 passed.
2026-08-26 14:52:46 +03:00
a941d1899f Merge pull request 'feat(observability): алерты адресуются в топик форумной группы + проверка конфига до подъёма (#3078)' (#3111) from feat/3078-alert-topic into main
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-frontend (push) Has been skipped
Deploy Metrics / server (push) Successful in 14s
Deploy / deploy-caddy (push) Has been skipped
Deploy Metrics / agent-apps (push) Successful in 21s
Deploy Metrics / agent-infra (push) Successful in 23s
Deploy / build-worker (push) Successful in 40s
Deploy / build-backend (push) Successful in 41s
Deploy / deploy (push) Successful in 1m6s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 11s
Reviewed-on: #3111
2026-08-26 11:15:54 +00:00
bot-backend
5b7ef161e3 feat(observability): алерты адресуются в топик форумной группы (#3078)
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m2s
CI / backend-tests (pull_request) Successful in 17m24s
Бот, которым шлются тревоги, — тот же, что пересылает сообщения поддержки, а
его чат форумный. Без message_thread_id Alertmanager кладёт тревоги в общую
тему, вперемешку с клиентской перепиской.

Поле поддерживается: проверено amtool check-config на том же образе, что
поднимается в проде (prom/alertmanager:v0.28.0). Схема Alertmanager строгая и
неизвестные поля отвергает, так что успешная проверка означает именно
поддержку, а не молчаливое игнорирование.

Подставляется ЦЕЛАЯ СТРОКА, а не значение: envsubst не умеет условий, и при
шаблоне вида `message_thread_id: ${TOPIC_ID}` незаданный топик дал бы
`message_thread_id:` без значения. Это не деградация - Alertmanager с таким
конфигом не стартует вовсе, то есть алертинг исчезает целиком. Деплой
формирует либо всю строку с отступом, либо пустую.

Топик необязателен: без него поле отсутствует, алерты уходят в общую тему,
поведение прежнее.

Попутно добавлена проверка конфига через amtool ДО подъёма стека - по образцу
`caddy validate` ниже в этом же файле. amtool берётся из того же образа, что и
сам Alertmanager, иначе проверялась бы не та версия схемы. Битый конфиг теперь
роняет деплой громко, а не выключает алертинг тихо.

Тесты (4) рендерят шаблон обоими способами и разбирают результат как YAML -
проверяется фактический конфиг, а не наличие нужных слов в тексте. Отдельно
проверено, что переменная объявлена в списке envsubst: забыть её - значит
оставить в конфиге литерал плейсхолдера.

Фальсификация: на исходных файлах краснеют 3 из 4; проходит только тест,
фиксирующий сохранённое поведение при незаданном топике. tests/ops целиком -
35 passed.
2026-08-26 14:10:56 +03:00
52b076fa4a Merge pull request 'fix(observability): cAdvisor 0.55.1 — 0.52 не видит контейнеры на Docker 29' (#3109) from fix/cadvisor-bump-055 into main
All checks were successful
Deploy Metrics / server (push) Successful in 11s
Deploy Metrics / agent-apps (push) Successful in 20s
Deploy Metrics / agent-infra (push) Successful in 22s
Reviewed-on: #3109
2026-08-26 10:41:25 +00:00
bot-backend
a72d39d74d fix(observability): cAdvisor 0.55.1 — 0.52 не видит контейнеры на Docker 29
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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
#3108 не починил пустые панели по контейнерам, и правило relabel там было ни
при чём. Настоящая причина глубже: Docker 29 на обоих хостах работает через
containerd-snapshotter (Storage Driver = overlayfs), а cAdvisor 0.52 ищет
метаданные слоя в легаси-хранилище:

    failed to identify the read-write layer ID for container "<id>"
    - open /rootfs/var/lib/docker/image/overlayfs/layerdb/mounts/<id>/mount-id:
      no such file or directory

При снапшоттере этого каталога нет вовсе — в /var/lib/docker/image/ лежит
только identity-cache.db, метаданные слоёв живут в containerd. Обработчик
контейнера не создаётся, и наружу уходит ровно один ряд: корневой cgroup
container_last_seen{id="/"}. Отсюда и «нодата» на панелях контейнеров при
живых node/postgres/app метриках.

0.55.1 умеет читать containerd-снапшоттер. Проверено пробами с ПРОДОВЫМИ
флагами на обоих хостах (одноразовые контейнеры, убраны за собой):

  Beget    (Docker 29.4.1): 22 ряда, все с name=, ошибок rw-layer 0
  Poincare (Docker 29.7.2): 23 ряда, все с name=, ошибок rw-layer 0

Примеры рядов — name="gendesign-alloy", name="gendesign-backend-1",
name="gendesign-osrm-1", с лейблом image. То есть именно то, чего не хватало
панелям.

Заодно поправлен комментарий, который я же вписал в cadvisor_trim в #3108: он
объяснял пустые панели трактовкой пустого regex, а это оказалось неверно.
Правило корректно и остаётся (корневой ряд приходит с name="" и должен
отсеиваться), но объяснение рядом с ним вводило в заблуждение.

Почему тег .1, а не .0: в реестре нет ни v0.53.0, ни v0.54.0, ни v0.55.0 —
только v0.54.1 и v0.55.1. Проверял по списку тегов, а не подбором.

Проверки: yaml.safe_load compose — ok; alloy fmt обоих .alloy в
grafana/alloy:v1.6.1 — exit 0.
2026-08-26 13:40:03 +03:00
fe8b022ce5 Merge pull request 'fix(observability): контейнерные метрики терялись целиком + два хвоста стека' (#3108) from fix/metrics-cadvisor-drop into main
All checks were successful
Deploy Metrics / server (push) Successful in 12s
Deploy Metrics / agent-infra (push) Successful in 19s
Deploy Metrics / agent-apps (push) Successful in 21s
Reviewed-on: #3108
2026-08-26 10:18:10 +00:00
8fbcdd2927 Merge pull request 'chore(hooks): точечный allowlist на три ключа метрик вместо снятия гарда' (#3107) from chore/hook-allow-metrics-keys into main
Reviewed-on: #3107
2026-08-26 10:17:59 +00: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
0e4beb8457 chore(hooks): точечный allowlist на три ключа метрик вместо снятия гарда
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Владельцу понадобились креды Grafana (basic_auth Caddy + внутренний вход), а
они лежат в backend/.env.runtime, который check-secret-read.py закрывает
целиком. Снимать гард нельзя: в том же файле prod DB-пароли и токены
Forgejo/GlitchTip.

Вместо этого — ALLOWED_KEYS из трёх ключей (METRICS_UI_PASSWORD,
GRAFANA_ADMIN_USER, GRAFANA_ADMIN_PASSWORD) и разрешение ровно на anchored-греп
по ним. Условия намеренно жёсткие, чтобы «прочитать один ключ» нельзя было
развернуть в «выгрузить файл»: блокируются инверсия (-v / --invert-match),
пайпы, цепочки ; && ||, подстановки $(...) и обратные кавычки, редиректы.

Тест на 10 кейсов: разрешён только anchored-греп по ключу из списка; отбиты
инверсия, пайп, цепочка, редирект, подстановка, чужой ключ, греп без якоря ^ и
обычный cat. Проверяется и функция, и хук end-to-end через настоящий
stdin-payload.

Гард в деле: он же отбил эту самую команду коммита, когда текст сообщения
содержал имя закрытого файла рядом с read-verb.
2026-08-26 13:09:37 +03:00
9ede71d0b1 Merge pull request 'fix(observability): агент не падает из-за переменной чужой роли + DSN экспортеров собираются (#3078)' (#3106) from fix/3078-exporter-dsn-and-profiles into main
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy Metrics / agent-infra (push) Successful in 28s
Deploy Metrics / agent-apps (push) Successful in 34s
Deploy Metrics / server (push) Successful in 11s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 40s
Deploy / build-worker (push) Successful in 42s
Deploy / deploy (push) Successful in 1m5s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
Reviewed-on: #3106
2026-08-26 09:50:41 +00:00
bot-backend
beafe6925b 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
Джоба 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-переменная проверяется в деплое; профили не
захардкожены. Фальсификация: на исходных файлах краснеют все три.
2026-08-26 12:45:31 +03:00
1da2eee142 Merge pull request 'fix(observability): стек не поднимался — привилегированную роль спрашиваем у контейнера, а не угадываем (#3078)' (#3105) from fix/3078-metrics-role-discovery into main
Some checks failed
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Metrics / server (push) Failing after 26s
Deploy Metrics / agent-apps (push) Has been skipped
Deploy Metrics / agent-infra (push) Has been skipped
Deploy / build-backend (push) Successful in 37s
Deploy / build-worker (push) Successful in 40s
Deploy / deploy (push) Successful in 1m4s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 10s
Reviewed-on: #3105
2026-08-26 09:27:29 +00:00
f9fc07aea3 Merge pull request 'ci(caddy): гард — import обязан быть покрыт volume-маунтом' (#3104) from ci/caddy-import-mount-guard into main
Reviewed-on: #3104
2026-08-26 09:27:10 +00:00
bot-backend
33bc7e4acf fix(observability): привилегированную роль спрашиваем у контейнера, а не угадываем (#3078)
All checks were successful
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 12s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m2s
CI / backend-tests (pull_request) Successful in 17m21s
Стек наблюдаемости не поднялся ни на одном хосте после мержа #3099: джоба
server упала, agent-apps и agent-infra пропустились как зависимые.

Причина (задача 23657, 26.08 08:55):

    err:   ОШИБКА: не нашёл роль с правом CREATE ROLE в gendesign-infra-postgres

setup-metrics-grafana-role.sh искал привилегированную роль перебором трёх
имён - glitchtip, forgejo, postgres. Ни одно не совпадает ни с одним реальным
кластером проекта: infra-postgres -> infra, gendesign-postgres-1 -> gendesign,
tradein-postgres -> tradein. Комментарий над перебором сам предупреждал, что
"угадывать postgres неверно", и дальше шло угадывание.

Замер на живом контейнере 26.08 (read-only, ничего не создавалось):

    POSTGRES_USER изнутри контейнера: infra
    glitchtip - отказ, forgejo - отказ, postgres - отказ, infra - 1

Стало: имя берём из POSTGRES_USER самого контейнера - это та переменная,
которой роль и создана при initdb, то есть источник истины. Прежний список
оставлен ПОСЛЕ него запасным путём для кластера не из образа postgres.

Попутно - глоб в paths деплоя. Воркфлоу запускает ТРИ setup-скрипта, а в
триггере стоял только setup-metrics-secrets.sh: правка двух остальных не
заводила выкат, и на хосте молча оставалась старая версия. Тот же класс, что
#2203 закрыл глобом ops/*.sh. Добавлен тест, который сверяет запускаемые
скрипты с шаблонами paths - на исходном воркфлоу он краснеет, указывая на
setup-metrics-exporter-dsn.sh.

Тесты (6) исполняют РЕАЛЬНЫЙ скрипт с подставным docker и проверяют
фактический выбор роли, а не наличие правильных слов в комментарии.
Фальсификация: на исходном коде краснеют 3 из 5 ролевых тестов; проходят
только те два, что фиксируют сохранённое поведение (запасной перебор и
громкая ошибка при отсутствии привилегий). tests/ops целиком - 28 passed.
2026-08-26 12:08:37 +03:00
bot-backend
46b42c80db ci(caddy): гард — import обязан быть покрыт volume-маунтом
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m10s
CI / openapi-codegen-check (pull_request) Successful in 2m18s
CI / backend-tests (pull_request) Successful in 17m41s
Follow-up к #3103. Прод лёг на ~30 минут потому, что PR завёл
`import ../metrics-*.caddy.snippet` в caddy/sites/infra.caddy, но не добавил
bind-монты этих файлов в docker-compose.prod.yml.

Соседний гард `caddy validate` эту дыру не ловит принципиально: он копирует
каталог caddy/ целиком (`docker cp caddy ...`), а на проде смонтированы только
отдельные файлы плюс два каталога. Расхождение между «что лежит в репозитории»
и «что реально видит контейнер» видно только если сверять с маунтами.

check-caddy-snippet-mounts.py разбирает bind-монты сервиса caddy:, резолвит
каждый `import` в Caddyfile / caddy/sites/*.caddy / caddy/*.caddy.snippet
относительно КОНТЕЙНЕРНОГО пути импортирующего файла и падает, если цель не
покрыта ни одним маунтом. Именованные сниппеты `(name) { }` пропускаются,
для glob/placeholder-импортов (`caddy/sites/{$CADDY_SITES:*}.caddy`)
проверяется каталог. --selftest воспроизводит ровно баг #3102.
2026-08-26 12:07:29 +03:00
41f5be2136 Merge pull request 'fix(caddy): смонтировать сниппеты метрик — без них Caddy валит ВЕСЬ прод' (#3103) from fix/caddy-metrics-snippet-mounts into main
Some checks failed
Deploy / changes (push) Successful in 8s
Deploy / perimeter-smoke (push) Successful in 10s
Deploy Infra Host / sync-infra-host (push) Failing after 3s
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 35s
Deploy / build-frontend (push) Successful in 36s
Deploy / build-worker (push) Successful in 38s
Deploy / deploy (push) Successful in 1m4s
Deploy / deploy-status (push) Successful in 1s
Reviewed-on: #3103
2026-08-26 09:04:06 +00:00
bot-backend
6f9a77f6db fix(caddy): смонтировать сниппеты метрик — без них Caddy валит ВЕСЬ прод
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / 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 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
PR #3102 добавил caddy/metrics-ingest.caddy.snippet и metrics-ui.caddy.snippet
плюс `import ../metrics-*.caddy.snippet` в caddy/sites/infra.caddy, но не
добавил bind-монты в docker-compose.prod.yml. Каталог caddy/ внутрь контейнера
целиком не пробрасывается — только пофайлово (как users.caddy.snippet) плюс
каталоги caddy/local и caddy/sites. Файлы лежали на хосте, но внутри
контейнера их не было.

Итог на проде 2026-08-26 ~11:26 MSK: Caddy не смог адаптировать конфиг
(`File to import not found: ../metrics-ingest.caddy.snippet, at
/etc/caddy/caddy/sites/infra.caddy:87`) и ушёл в restart-loop. Легли ВСЕ
сайты хоста — gendsgn.ru, merahome.ru, обе mera-витрины, а не только
metrics.gendsgn.ru, ради которого сниппет и добавляли.

Монтируем оба файла явно, рядом с users.caddy.snippet.
2026-08-26 12:03:23 +03:00
ad6fc1b2d6 Merge pull request 'feat(observability): /metrics в обоих бэкендах — счётчики, задержка, дашборд приложений' (#3102) from feat/observability-app-metrics into main
Some checks failed
Deploy Infra Host / sync-infra-host (push) Failing after 4s
Deploy / changes (push) Successful in 9s
Deploy / deploy-caddy (push) Has been skipped
Deploy Metrics / agent-apps (push) Has been skipped
Deploy Metrics / agent-infra (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-frontend (push) Successful in 39s
Deploy / build-backend (push) Successful in 3m43s
Deploy Trade-In / test (push) Successful in 3m54s
Deploy / build-worker (push) Successful in 5m36s
Deploy / deploy (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 2m52s
Deploy Trade-In / build-backend (push) Successful in 1m40s
Deploy / deploy-status (push) Successful in 1s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Has been cancelled
Deploy Trade-In / perimeter-smoke (push) Successful in 30s
Deploy Metrics / server (push) Failing after 10s
Deploy Trade-In / changes (push) Successful in 13s
Reviewed-on: #3102
2026-08-26 08:54:55 +00:00
bot-backend
124cfb3d5d feat(observability): /metrics в обоих бэкендах — счётчики, задержка, дашборд
All checks were successful
CI / backend-tests (pull_request) Successful in 17m30s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m22s
CI Trade-In / backend-tests (pull_request) Successful in 4m51s
Третья часть #3078 и единственная, трогающая прод-код.

До неё числовых рядов у приложений не было вовсе: только логи и исключения в
GlitchTip. Класс отказов «отвечает, но медленно» и «отдаёт 401 потоком» в такой
картине невидим — исключения нет, строка в логе выглядит обычной, а продукт
при этом не работает.

Метка route — ШАБЛОН маршрута, а не путь запроса. Это несущее решение, а не
деталь: кадастровый номер или идентификатор заявки в метке даёт новый временной
ряд на каждую сущность, а ряд у Prometheus стоит памяти постоянно, а не в момент
запроса. Самый известный способ уронить мониторинг тем самым мониторингом.
Незаматченные пути (404, сканеры) сведены в одну метку, иначе тот же взрыв
устроит любой бот, перебирающий адреса. Оба свойства сторожатся тестами, а не
комментарием: тест бьёт тремя разными идентификаторами и требует ОДИН ряд.

Слой регистрируется последним и потому оказывается самым внешним. Изнутри
RBAC-гварда не видно ни отказов авторизации, ни времени, которое он тратит на
резолв сессии в БД auth, — а именно этот путь уже давал инцидент с блокирующим
I/O в middleware (#1202). Упавший исключением запрос считается как 500 в
finally: без этого он просто отсутствовал бы в счётчике, то есть ровно тогда,
когда метрики нужнее всего.

Путь публичен ВНУТРИ и закрыт СНАРУЖИ — это два разных периметра. Скрейп идёт
из docker-сети, где заголовка X-Authenticated-User нет ни у кого, поэтому
/metrics внесён в _PUBLIC_PATHS обоих бэкендов; иначе агент получал бы 401 и
метрик не было бы вовсе. Наружу путь не открывается ни через gendsgn.ru, ни
через meraocenka.ru, и вдобавок закрыт явным respond 404 в обоих site-блоках —
чтобы закрытость осталась решением, а не следствием текущего порядка директив.

Ограничитель частоты и аудит «Меры» не трогались: оба смотрят только на пути
под /api/, скрейп под них не попадает. Проверено тестом, а не чтением.

Прод-поведение не меняется ничем, кроме нового публичного пути: ни один
существующий обработчик, гвард или маршрут не тронут.

Refs #3078
2026-08-26 11:30:18 +03:00
afd881d6b1 Merge pull request 'feat(observability): стек метрик и логов — Prometheus, Loki, Grafana на Beget, агенты на обоих хостах' (#3099) from feat/observability-metrics-stack into main
Some checks failed
Deploy Infra Host / sync-infra-host (push) Failing after 4s
Deploy / changes (push) Successful in 8s
Deploy Metrics / server (push) Failing after 8s
Deploy Metrics / agent-apps (push) Has been skipped
Deploy Metrics / agent-infra (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-frontend (push) Successful in 34s
Deploy / build-worker (push) Successful in 36s
Deploy / build-backend (push) Successful in 37s
Deploy / deploy (push) Successful in 1m4s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Failing after 2m40s
2026-08-26 08:25:23 +00:00
bot-backend
7b6832e90f feat(observability): дашборд баз данных — раздутие, горизонт vacuum, WAL
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 10s
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
Вторая часть #3078. Конфигурация экспортеров приехала первым коммитом, но
смотреть на неё было негде: без витрины ряды есть, а ответа на вопрос нет.

Панели подобраны по разборам постфактум, а не по списку «что обычно рисуют».
Доля апдейтов мимо HOT — потому что у listings она была 0,43 % при 198
апдейтах на строку, и именно это дало 15 ГБ TOAST при 230 МБ живого
содержимого (#2992/#2989), копившиеся 91 день. Возраст самой старой
транзакции — потому что осиротевшие запросы висели 46 часов и держали
горизонт видимости, из-за чего autovacuum не убирал мёртвые строки во всей
базе (#2607). WAL за сутки — потому что 7,02 ГБ при четырёх пользовательских
расчётах это диспропорция, заметная только на ряде. Размер баз — потому что
у GlitchTip нет политики ретенции вовсе, и он растёт без ограничения.

Размеры разложены на heap / индексы / TOAST: суммарный размер таблицы не
объясняет ничего, а именно это разделение объяснило, куда ушли 19 ГБ.

Отдельная панель «экспортер отвечает»: пустой график и упавшая база выглядят
одинаково, и различать их должно что-то явное.

Refs #3078
2026-08-26 11:23:33 +03:00
2116c3774e Merge pull request 'fix(mera/ui): карточка прогресса перестала выдумывать стадии (#3081, вариант A)' (#3101) from fix/3081-honest-progress into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 1m18s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / build-frontend (push) Successful in 2m29s
Deploy Trade-In / test (push) Successful in 3m51s
Deploy Trade-In / build-backend (push) Successful in 35s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-08-26 08:01:56 +00:00