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

Пилот решили начать с отдела транспортной логистики. Одна из киллер-фич большого “Петровича”, когда до 10 тонн самого разнообразного строительного материала можно получить на адресе через 2 часа после оформления заказа.

В этой статье - четыре связанные темы:

  1. Канал логистики - бизнес получает только релевантные уведомления. 

  2. Alerta → LifePOS - первый шаг к сбоям из мониторинга (с контролем человека).

  3. Postmortem - разбор после закрытия без смешения с новым сбоем.

  4. Web UI - те же сценарии, когда MAX не под рукой.

Путь сбоя
Путь сбоя

Часть 1. Канал логистики: сбой, который касается только тебя

Проблема: логистика тонула в чужом шуме

Раньше все уведомления об инцидентах уходили в один общий канал. Для IT-команды это нормально - там разбираются, какой сервис их касается, а какой нет. Для логистики (как отдельного бизнес-направления) это оказалось неудобно: чтобы понять, важен ли для них конкретный сбой, приходилось читать техническое сообщение целиком и додумывать, относится ли оно к их процессам.

Логистика прямо попросила: присылайте только то, что касается нас, и в понятном виде - без необходимости разбираться, в каком сервисе что упало.

Что сделали

Добавили отдельный MAX-канал (MAX_LOGISTICS_CHANNEL_ID) и новый опциональный шаг в сценарии создания сбоя - FAL1: «Управление логистикой» (то же значение, что в Jira для метрик по бизнес-домену).

При создании сбоя дежурный видит выбор:

  • «Управление логистикой» - сбой размечается как относящийся к этому домену, летит отдельным сообщением в канал логистики;

  • «Пропустить» - обычный сценарий, без дублирования.

Web UI
Web UI

Если выбрана логистика, в канал уходит сообщение в упрощенном формате - специально не техническом:

🚨 Технический сбой

• Проблема: <краткое описание>

• Сервис: <сервис>

• Исправим до: <ETA> (P85 ≈ <часы>)

• Ответственный: <ФИО из эскалации>

MAX
MAX

Почему так:

  • бизнесу не нужны детали реализации - только «что», «когда почините» и «к кому вопрос»;

  • шаг опциональный - не ломает обычный флоу для сбоев, которые логистики не касаются;

  • название поля сохранили от Jira, чтобы не плодить два разных термина для одной сущности в отчётности и в боте.

Конфиг и условие «это логистика»

В .env заводим отдельный канал; FAL1 при создании сбоя пишется в state:

MAX_LOGISTICS_CHANNEL_ID= -12345678901234

LOGISTICS_P85_MINUTES=60   # fallback, если Jira недоступна

Константа домена - одна строка, чтобы не разъехались Jira и бот:

# domain/constants.py

FAL1_LOGISTICS = "Управление логистикой"

Дублирование в канал логистики - не «второй пост везде», а проверка FAL1 при каждом событии (создание, продление, закрытие):

# utils/channel_helpers.py

async def maybe_send_to_logistics_for_alarm(alarm: dict, text: str) -> bool:

    if (alarm.get("fal1") or "").strip() != FAL1_LOGISTICS:

        return False

    return await send_to_logistics_channel(text)

Откуда берётся ETA: P85

Время «исправим до» не берётся с потолка и не является фиксированным SLA. Это 85-й перцентиль времени решения инцидентов с тем же FAL1 за прошедший период - сейчас считаем за текущий календарный год по закрытым FA в Jira.

Идея простая: обещать точное время невозможно, но можно опираться на статистику похожих случаев и дать разумный ориентир, который в 85% случаев не будет превышен. Это честнее, чем произвольная оценка дежурного, и не требует от него экспертизы в конкретном сервисе, чтобы прикинуть срок.

Расчёт в коде - JQL по закрытым FA с нужным FAL1 за текущий год, разница TimeEndProblem − TimeStartProblem, затем 85-й перцентиль:

# services/logistics_p85_service.py (упрощенно)

def _p85_minutes(sorted_minutes: list[float]) -> Optional[float]:

    return float(statistics.quantiles(sorted_minutes, n=100)[84])

# fix_until = now + P85; при < MIN_SAMPLES - LOGISTICS_P85_MINUTES из .env

Ответственный - не из общего справочника, а из таблицы эскалации

Раньше ответственных бот искал по справочнику сервисов в Confluence - там для каждого сервиса указаны те, кто устраняет проблему, и те, кто должен быть в курсе. Для канала логистики этого было недостаточно: нужен был человек, который отвечает перед бизнесом, а не обязательно тот же, кто чинит.

Добавили отдельную таблицу эскалации: по каждому сервису бот сначала подставляет ответственного за устранение, а при необходимости эскалации - уже владельца сервиса из отдельной колонки. Это дополнительный уровень поверх существующего справочника, а не замена ему.

/status и дисциплина коммуникации

Логистике важно не только узнать про сбой, но и получать обновления, не дергая дежурного вручную. Добавили команду /status и кнопку «Статус» в меню «Управлять»: бот проводит краткий опрос (причина, что делаем, обход, что нужно от логистов, когда следующий апдейт) и публикует структурированную карточку в канал логистики, а не простыню из FA-чата.

Интервал между апдейтами задаёт тот, кто заполняет статус (последний вопрос опроса - «следующее обновление через N минут»). За 3 минуты до этого времени бот присылает автору карточки напоминание.

Важная деталь: если ответственный не отреагировал на напоминание, в текущей реализации бот просто ждёт - без эскалации и повторных пингов. Это осознанное упрощение на первом этапе: сначала проверяем, работает ли сама механика регулярных апдейтов, усложнять поведение при пропуске будем по факту использования.

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

Команды и карточка в канале

Дежурный в личке или FA-чате:

/status FA-1000 или: Управлять → Сбои → FA-1000 → Статус

После опроса бот собирает карточку и пишет срок следующего апдейта в state (для напоминания за 3 минуты):

# adapters/max/status_flow.py (фрагмент)

minutes = int(data.get("next_minutes") or 15)

next_at = now + timedelta(minutes=minutes)

text = MessageFormatter.format_logistics_status_text(

    alarm_id=alarm_id,

    now_hm=now.strftime("%H:%M"),

    next_hm=next_at.strftime("%H:%M"),

    what_broken=...,

    cause=data.get("cause"),

    doing=data.get("doing"),

    ...

)

await send_to_logistics_channel(text)

bot_state.active_alarms[alarm_id]["status_next_at"] = next_at

bot_state.active_alarms[alarm_id]["status_author_user_id"] = user_id

Шаблон карточки - отдельная функция, поля без ответа в опросе не попадают в текст:

# services/message_formatter.py (фрагмент)

lines = [

    f"📌 СТАТУС {alarm_id}",

    f"• Обновлено: {now_hm}",

    f"• След. обновление: {next_hm}",

]

if cause:

    lines.append(f"• Причина (предварительно): {cause}")

lines.append(f"• Что не работает: {what_broken or 'не указано'}")

# ...
Web UI
Web UI
MAX
MAX

Почему так:

  • бизнес получает предсказуемый ритм информации, а не тишину до момента закрытия;

  • интервал апдейта задаёт человек в момент статуса, а не жёсткий таймер в конфиге, потому что реальная частота зависит от характера сбоя;

Обратная связь логистики

После нескольких недель использования канала и статус-карточек команда логистики ответила так (с разрешения, без привязки к конкретному человеку):

“Полностью устраивает, стало прозрачнее и понятнее, что происходит. Удобно следить за ситуацией и вносить корректировки в логистические процессы, чтобы доставка оставалась на высоте.”

Для нас это главный критерий: не «красивый бот», а управляемая коммуникация - бизнес видит сбой в своём формате и может реагировать на процессы.

Часть 2. Alerta → LifePOS: мониторинг в контур инцидента

Зачем

Все предыдущие сценарии начинались с того, что кто-то - дежурный или сотрудник техподдержки - вручную инициировал создание сбоя. Для сервиса LifePOS решили попробовать другое: сигнал приходит из системы мониторинга Alerta, а бот предлагает завести инцидент.

Alerta - открытая платформа для консолидации и дедупликации алертов из разных источников мониторинга. У нас она уже используется для отслеживания состояния сервисов; новый шаг - научить «Дежурного» слушать её и превращать открытые алерты в инциденты ITSM.

Минимальный конфиг пилота

Переменная

Значение

Назначение

ALERTA_URL

адрес инстанса Alerta

Подключение к API мониторинга

ALERTA_API_KEY

ключ доступа

Авторизация запросов к Alerta

ALERTA_WATCH_SERVICE

delivery_payment

Какую группу алертов слушать

ALERTA_POLL_INTERVAL_SEC

30

Интервал опроса, подобран эмпирически

ALERTA_CREATE_JIRA

0 (на пилоте)

Отключает создание тикетов в Jira

ALERTA_PUBLISH_PETLOCAL

0 (на пилоте)

Отключает публикацию в Петлокал

Как это работает

Бот раз в ~30 секунд опрашивает Alerta на предмет открытых алертов по группе delivery_payment. Интервал выбрали без специальных расчетов - взяли разумное значение для тестового периода, дальше подстраиваем через конфиг и админ-команды, без релиза кода.

Когда находится новый открытый алерт, администраторам бота (MAX_ADMIN_IDS) приходит сообщение с двумя кнопками: «Завести сбой» и «Пропустить». Полная автоматизация здесь намеренно не сделана - на этапе пилота решение всё ещё принимает человек, бот только избавляет его от необходимости искать источник и оформлять сбой руками.

При подтверждении бот создаёт сбой с заранее заданными дефолтами для этого сервиса:

  • тип «Другое: LifePOS»;

  • домен FAL1 - логистика (сообщение дублируется в канал логистики по описанной выше логике);

  • ETA - тот же P85, посчитанный по FAL1;

  • ответственный - «Ведущий специалист технической поддержки LifePOS» (роль в сообщениях), потому что LifePOS пока не принят на официальную поддержку и не описан в общей таблице эскалации. Как только сервис туда попадёт, ответственный будет подставляться по общим правилам, как для остальных сервисов.

Что улучшили после первого пилота

На старте было достаточно «алерт → кнопки → сбой». В боевом использовании всплыли два типичных шума:

  • повторные алерты, пока сбой LifePOS уже открыт - бот снова предлагал «Завести сбой»;

  • ложное ощущение автоматики - один и тот же алерт мог дёргать несколько раз.

Было

Стало

Каждый новый алерт из Alerta предлагал завести сбой

Пока сбой LifePOS активен, повторные алерты delivery_payment не предлагаются

Опрос Alerta продолжался без учёта уже открытого сбоя

При закрытии сбоя опрос возобновляется автоматически

Дежурного могло дёргать несколько раз одним и тем же алертом

Человек-шлагбаум сохранён, но без спама

При создании сбоя из Alerta pending-записи помечаются как подавленные; при закрытии - снимается mute:

# services/alerta_watch_worker.py (упрощённо)

def on_alerta_lifepos_alarm_closed(alarm_id: str, alarm_info: dict | None = None) -> int:

    """После закрытия сбоя LifePOS снова слушаем Alerta."""

    for aid, entry in list(known.items()):

        if entry.get("status") in ("suppressed_active_alarm", "accepted"):

            known.pop(aid, None)

    # лог: «снова слушаем delivery_payment»

Дефолты сбоя LifePOS задаются при accept - FAL1 логистика, ETA = P85, в поле ответственного - роль «Ведущий специалист технической поддержки LifePOS»:

# services/alerta_watch_worker.py (фрагмент create_alarm data)

data = {

    "service": "Другое",

    "service_other_spec": "LifePOS",

    "fal1": FAL1_LOGISTICS,

    "responsible_person_name": "Ведущий специалист технической поддержки LifePOS",

    "alerta_alert_id": alert_id,

    "alerta_snapshot": snapshot,

    ...
}

Формат сообщения: суть сверху, техника снизу

Отдельно провозились с тем, как показать алерт из Alerta человеку, который должен принять решение за секунды. Сырой алерт содержит служебные поля (service, resource, event), которые ничего не говорят о сути проблемы с первого взгляда. Разделили сообщение на две части - понятную сверху и техническую снизу, для тех, кто хочет свериться с первоисточником:

🚨 Технический сбой · LifePOS

• Что случилось: TEST Confluence UP status TEST

• Сервис: Другое: LifePOS

• Исправим до: 06.08.2026 22:51 (P85 ≈ 0.4 ч)

• Ответственный: Ведущий специалист технической поддержки LifePOS

Alerta Service: delivery_payment

• Resource: Test2host

• Event: TESTConfluenceUPstatus

MAX
MAX

Формат ещё не финальный - сейчас подбираем баланс, который одинаково удобен и IT, и бизнесу.

Пилот: осторожно, без побочных эффектов

На время тестирования запись в Jira и публикация в Петлокал для этого сценария могут быть отключены флагами (ALERTA_CREATE_JIRA=0, ALERTA_PUBLISH_PETLOCAL=0). Логика сбоя при этом полностью отрабатывает - просто без создания внешних артефактов, пока проверяем корректность самого сценария.

Почему так:

  • фича-флаги позволяют гонять реальную логику на реальных алертах, не создавая шума во внешних системах;

  • когда логика подтвердится на практике, включение Jira и Петлокала - смена переменных в .env и перезапуск контейнера.

Опрос Alerta целиком можно отключить без деплоя:

/feature set ALERTA_ENABLED 0

Первые цифры

Статистики по Alerta пока минимум. В среднем от появления алерта до фактического заведения инцидента проходит 15 секунд - это время цикла опроса и нажатия «Завести сбой». Делать выводы рано, но путь «алерт → инцидент» уже укладывается в секунды вместо ручного оформления.

Куда движемся: сейчас человек подтверждает каждый алерт кнопкой. Долгосрочная цель полное автозаведение сбоя без подтверждения.

Часть 3. Postmortem: разбор после закрытия без потери контекста

Проблема, которую не видели на старте

Когда сбой закрывается, FA-чат в MAX нужно очистить - иначе следующий инцидент попадёт в чат, где ещё обсуждают последствия предыдущего.Но postmortem часто идёт после формального закрытия: причины, хронология, action items. Два крайних варианта были плохими:

  • очищать сразу - теряется живой контекст разбора;

  • не очищать - операционный чат смешивается с разбором, новый сбой открывается «поверх» старого.

Jira для разбора - формально правильно, но медленнее чата: люди быстрее отвечают в МАХ, чем в задаче.

Что сделали

После закрытия сбоя FA-чат переводится в режим postmortem:

  1. Первичный архив - в Jira (как раньше).

  2. Чат не очищается, переименовывается в 📋 FA-XXXX · разбор.

  3. Бот шлёт краткое вводное: что закрыли, проблема, ссылка на Jira.

  4. Чат исключается из пула операционных FA-чатов - новый сбой получит другой чат (сейчас до шести FA-чатов в ротации).

  5. Когда разбор завершён - «Управлять → Постмортем → Закончить разбор» (или /postmortem done): дополнительный архив в Jira, очистка чата, возврат в пул.

MAX
MAX
WebUI
WebUI

Как это выглядит в коде

Postmortem-чаты хранятся в state.json отдельно от активных сбоев:

{

  "postmortem_chats": {

    "-71063779478219": {

      "alarm_id": "FA-3700",

      "closed_at": "2026-08-31T09:19:00",

      "jira_key": "FA-3700",

      "issue": "Тест 1"

    }

  }

}

При выборе FA-чата для нового сбоя postmortem-чаты исключаются из пула:

# services/postmortem_service.py

def occupied_fa_chat_ids() -> set[str]:

    used = {a["max_chat_id"] for a in bot_state.active_alarms.values() if a.get("max_chat_id")}

    used |= set(bot_state.postmortem_chats.keys())

    return used

# core/creation.py

fa_chat_id = get_next_max_fa_chat_id(occupied_fa_chat_ids())

При закрытии сбоя архив в Jira выполняется сразу, очистка чата - только после явного завершения разбора:

# services/max_archive.py

await process_max_chat_on_alarm_close(..., postmortem=True)   # архив, чат не трогаем

# ...

await process_max_chat_on_alarm_close(..., postmortem=False)  # /postmortem done → очистка

Почему так:

  • один FA-чат = одна фаза жизни инцидента (операционка или разбор, не оба сразу);

  • контекст разбора остаётся там, где шла работа по сбою;

  • Jira остаётся системой записи и постановки реальных задач, MAX - средой быстрого обсуждения;

  • завершение разбора явное действие, а не «когда-нибудь почистим».

Postmortem включается флагом MAX_ALARM_POSTMORTEM_ON_CLOSE (по умолчанию включен); при необходимости откатывается через /feature set без рестарта.

Часть 4. Web UI: те же сценарии без мессенджера

Появился веб-интерфейс (FastAPI): те же кнопки «Сообщить», «Управлять», «Постмортем»

Зачем:

  • Не нужно искать чат с ботом

  • просмотр MAX-чатов и отправка сообщений из браузера

  • страница статистики «время без сбоев»;

  • запасной канал, если с MAX что-то не так.

Web UI не дублирует бизнес-логику - он ходит в тот же core/ и те же сценарии, что MAX-бот. Это не «вторая версия продукта», а второй вход в один процесс.

# adapters/web/gateway.py

class WebChatGateway:

    """Один операторский чат: те же сценарии, что в MAX"""

async def handle_callback(payload: str, ...):

    if payload == "manage_postmortem":

        items = list_postmortem_items()

        await reply_fn("📋 Выберите сбой в разборе:", postmortem_list_keyboard(items))

    if payload.startswith("action_pm_finish_"):

        ok, msg = await finish_postmortem(alarm_id=item_id)

Payload кнопок (manage_postmortem, select_pm_FA-3700, ...) те же, что в MAX - один обработчик сценариев, два транспорта.

Уроки на середине пилота

  1. Не жди готового решения от бизнеса - спрашивай, что мешает. Запрос на отдельный канал логистики появился именно потому, что мы спросили, а не додумали сами

  2. Автоматизация не обязана быть полной сразу. Кнопки «Завести сбой» / «Пропустить» - осознанный компромисс, пока не набралось доверие к автоматике

  3. Статистические ориентиры лучше произвольных обещаний. P85 честнее фиксированного SLA, потому что опирается на реальную историю

  4. Фича-флаги - про безопасность пилотов, а не только про быстрый релиз: логика на проде, внешние системы - по переключателю

  5. Жизненный цикл важнее «создали сбой». Postmortem родился из конфликта «очистить чат vs сохранить разбор» - типичный симптом зрелости процесса

  6. Один сценарий - несколько клиентов. Web UI имеет смысл только как второй вход в ту же логику, а не как отдельный продукт.

Что дальше

  • Новый отдельный канал, возможно для контакт-центра

  • Alerta: накопить статистику ложных срабатываний, исправить. Добавить алерты и реакции бота

  • Postmortem: сколько разборов одновременно, хватает ли шести FA-чатов

  • Web: закрепить как рабочий инструмент смены, а не только запасной инструмент

Если делали похожее (разделение бизнес/IT каналов, postmortem-чаты, полуавтомат Alerta) - напишите в комментариях, особенно интересно, как закрывали разбор после закрытия без потери контекста.