Bollywood HungamaEXCLUSIVE: Abhishek Varman to direct modern mythological superhero spectacle Gadadhari Bheem; film to be produced by Dharma ProductionsInquirerFoul odor prompts shoreline inspection in MamburaoPunchDenmark probes hack of national database affecting 8.8m peopleThe Jerusalem PostIndian police detain Israeli for staying without valid travel documents following drug chargesESPN DeportesPortugal da cuenta de Noruega, tras polémica de CristianoCNN TürkSAĞLIK OCAĞI ÇALIŞMA SAATLERİ 2026: Aile Hekimliği kaçta açılıyor, kaça kadar açık? Sağlık ocağı hafta sonu açık mı?한겨레계단·화장실·복사기 앞 …관객 선 자리가 ‘극, 장’ZDF heuteEntdecken Sie das ZDF-NachrichtenstudioBBC عربياجتماع للجان "اتفاقية مكة" في الرياض اليوم، والقوات اليمنية تنفّذ 1,122 عملية ضد الحوثيينسكاي نيوز عربية"واقعة البصق" تزيد الاحتقان في مباراة أيرلندا وإسرائيلDaily MailAnti-migrant protesters scuffle with police after nearly 150 small boat arrivals use 'new route' to land at historic naval port20 MinutenPfleger feuerte sechs Schüsse auf zwei Polizisten ab
The Daily Newsstand · Free, Always
Monday, October 5, 2026

Дежурный бот вырос еще раз: отдел логистики, алерты, постмортемы

Translate

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

Пилот решили начать с отдела транспортной логистики. Одна из киллер-фич большого “Петровича”, когда до 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) - напишите в комментариях, особенно интересно, как закрывали разбор после закрытия без потери контекста.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.