feat(observability): кнопка «Принял в работу» под клиентским инцидентом (#3078) #3136

Merged
lekss361 merged 2 commits from feat/3078-alert-ack-button into main 2026-08-27 11:58:42 +00:00
Owner

Вторая половина запроса владельца: тег дежурного уже в #3128, здесь — обратная связь «я отреагировал». Вариант размещения выбран владельцем: сервис на наблюдателе.

Как выглядит

🚨 КЛИЕНТЫ ЗАТРОНУТЫ · HostAgentDown · apps
@leks361 — нужна реакция.
Агент метрик не отвечает
                    [ ✅ Принял в работу ]
        ↓ нажатие
✅ Принято в работу · HostAgentDown · apps
@leks361, 13:47

Кнопка после нажатия снимается, отметка уходит ответом на само сообщение инцидента — в ту же тему «алерты».

Три решения, каждое с ценой

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

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

/ack/* без пароля — осознанно. Кнопку жмут ночью с телефона, когда лежит прод; требование пароля в этот момент даст ноль нажатий и вернёт нас к вопросу «а кто-нибудь это читает». Защита — неугадываемость и одноразовость: 128-битный токен, выданный под конкретное сообщение и живущий сутки. Максимум, чего добьётся угадавший, — ложная отметка «принято» в чате, где сразу видно, что её поставил не человек. Прав в системе токен не даёт никаких.

Через сервис идёт только клиентский маршрут

Прочие алерты сохраняют прямой путь в Telegram: чем меньше звеньев у алерта, тем он надёжнее, и терять это для инфраструктурных сообщений незачем.

Если сервис лёг — Alertmanager считает доставку неудачной, повторяет, а сам инцидент остаётся активным и переуведомляется каждые 30 минут. Алерт задерживается, но не теряется. Дублировать его вторым прямым каналом не стал: два уведомления на каждый инцидент — шум, а шум в канале тревог опаснее задержки.

Тесты

$ python -m pytest tests/ops/ -q
75 passed

Девять новых дёргают настоящие функции сервиса, подменяя ровно один шов — вызов Bot API. Сеть не трогается, всё, что ушло бы в неё, проверяется.

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

Обновлён тест из #3128: упоминание дежурного переехало из шаблона в сервис вместе с текстом сообщения — проверка та же по смыслу, но смотрит туда, где текст формируется теперь. Заодно добавлена проверка, что прямой канал у остальных маршрутов сохранён.

Без зависимостей

Только стандартная библиотека Python, образ без сборки: сервис обязан подниматься даже когда сломано всё остальное, и не тащить за собой установку пакетов.

Refs #3078, #3128, #3119

Вторая половина запроса владельца: тег дежурного уже в #3128, здесь — обратная связь «я отреагировал». Вариант размещения выбран владельцем: сервис на наблюдателе. ## Как выглядит ``` 🚨 КЛИЕНТЫ ЗАТРОНУТЫ · HostAgentDown · apps @leks361 — нужна реакция. Агент метрик не отвечает [ ✅ Принял в работу ] ↓ нажатие ✅ Принято в работу · HostAgentDown · apps @leks361, 13:47 ``` Кнопка после нажатия снимается, отметка уходит ответом на само сообщение инцидента — в ту же тему «алерты». ## Три решения, каждое с ценой **Живёт на инфраструктурной машине, рядом с Alertmanager.** У бота МЕРЫ уже есть приём обновлений, и повесить обработку туда было бы дешевле по коду. Но он работает на продуктовом хосте: при падении продукта кнопка оказалась бы мёртвой ровно тогда, когда она нужна. **Ссылка, а не callback.** Callback-кнопка требует, чтобы кто-то читал обновления бота. Бот один, и его обновления **уже читает МЕРА** — второй читатель получил бы `409 Conflict` и отобрал бы сообщения у поддержки. URL-кнопка не требует обновлений вовсе. **`/ack/*` без пароля — осознанно.** Кнопку жмут ночью с телефона, когда лежит прод; требование пароля в этот момент даст ноль нажатий и вернёт нас к вопросу «а кто-нибудь это читает». Защита — неугадываемость и одноразовость: 128-битный токен, выданный под конкретное сообщение и живущий сутки. Максимум, чего добьётся угадавший, — ложная отметка «принято» в чате, где сразу видно, что её поставил не человек. Прав в системе токен не даёт никаких. ## Через сервис идёт только клиентский маршрут Прочие алерты сохраняют **прямой** путь в Telegram: чем меньше звеньев у алерта, тем он надёжнее, и терять это для инфраструктурных сообщений незачем. Если сервис лёг — Alertmanager считает доставку неудачной, повторяет, а сам инцидент остаётся активным и переуведомляется каждые 30 минут. Алерт **задерживается, но не теряется**. Дублировать его вторым прямым каналом не стал: два уведомления на каждый инцидент — шум, а шум в канале тревог опаснее задержки. ## Тесты ``` $ python -m pytest tests/ops/ -q 75 passed ``` Девять новых дёргают настоящие функции сервиса, подменяя ровно один шов — вызов Bot API. Сеть не трогается, всё, что ушло бы в неё, проверяется. Важнейший из них: **при отказе отправки с клавиатурой сообщение уходит без неё** — алерт важнее кнопки, молчание вместо сообщения было бы тем самым отказом, ради устранения которого весь стек и заводится. Плюс идемпотентность повторного нажатия (ссылка живёт сутки, по ней кликнут дважды), протухание токена, и то, что неизвестный токен ничего не отправляет в чат. Обновлён тест из #3128: упоминание дежурного переехало из шаблона в сервис вместе с текстом сообщения — проверка та же по смыслу, но смотрит туда, где текст формируется теперь. Заодно добавлена проверка, что прямой канал у остальных маршрутов сохранён. ## Без зависимостей Только стандартная библиотека Python, образ без сборки: сервис обязан подниматься даже когда сломано всё остальное, и не тащить за собой установку пакетов. Refs #3078, #3128, #3119
lekss361 added 1 commit 2026-08-27 10:56:32 +00:00
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
053a5fb75c
Alertmanager инлайн-клавиатуру не поддерживает, а без кнопки нет обратной
связи «человек увидел и взял в работу»: 27.08 продукты лежали 10 часов, и
вопрос «а кто-нибудь это читает» было не к кому адресовать.

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

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

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

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

Девять тестов дёргают настоящие функции, подменяя один шов — вызов Bot API.
Важнейший: при отказе отправки с клавиатурой сообщение уходит БЕЗ неё —
алерт важнее кнопки.
bot-backend added 1 commit 2026-08-27 11:32:41 +00:00
fix(tests): убрать директивы noqa на неактивное правило
All checks were successful
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
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 / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m8s
CI / backend-tests (pull_request) Successful in 17m35s
4e8daff675
`SLF001` (обращение к приватному члену) в конфиге ruff не включён, поэтому
`# noqa: SLF001` — подавление того, что и так не проверяется. Ruff ловит это
правилом RUF100 и валит проверку.

Тест намеренно лезет в приватные `_tg`, `_PENDING`, `_send_alert`: подмена
единственного шва до сети — и есть смысл этих тестов. Пояснение, которое
стояло после директивы, сохранено обычным комментарием.
lekss361 merged commit d0547ddb9a into main 2026-08-27 11:58:42 +00:00
lekss361 deleted branch feat/3078-alert-ack-button 2026-08-27 11:58:43 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3136
No description provided.