Read in:
Русский

Алертам место в базе знаний

О чём это: мы написали alert-sink, небольшой открытый сервис, который превращает вебхуки Alertmanager в заметки об инцидентах в инстансе trip2g. Каждый алерт становится markdown-заметкой, заметки складываются в постоянную историю инцидентов с поиском, а база знаний может сама рассказывать про инциденты в Telegram-группе. Читать, если ваша история инцидентов сейчас живёт в скроллбэке чата.

Алерты сегодня работают по принципу «выстрелил и забыл». Prometheus замечает проблему, Alertmanager кидает сообщение в Telegram или на почту, кто-то реагирует или нет, и на этом запись заканчивается. Через месяц вы спрашиваете «а когда эта нода флапала в прошлый раз?», и честный ответ такой: листай чат и надейся. Сам Alertmanager показывает только то, что горит прямо сейчас. История живёт либо как time series в Prometheus, ограниченная retention, либо как сообщение, которое давно уехало вверх.

Меня это раздражало довольно долго, потому что решение тут не мониторинговый продукт. Это место, куда записывать.

Один инцидент, одна заметка

alert-sink состоит из одного Go-бинарника и стоит за Alertmanager как webhook receiver. Когда алерт срабатывает, sink создаёт markdown-заметку в базе знаний trip2g: incidents/YYYY/MM/<timestamp>-<alertname>-<fingerprint>.md, с полным набором лейблов и таймингами во frontmatter. Когда алерт резолвится, та же заметка переключается в resolved.

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

Что это даёт:

  • Постоянная история. Заметки не протухают вместе с retention Prometheus и никуда не уезжают.
  • Поиск. История инцидентов состоит из обычных заметок в базе знаний, так что полнотекстовый поиск по ней достаётся бесплатно.
  • Постмортемы со ссылками. Каждая заметка об инциденте несёт wikilink на соседнюю заметку-постмортем. Постмортем потом пишет человек или агент, и sink аккуратно никогда не трогает этот путь.
  • Рассказ в группу. trip2g и так умеет публиковать заметки в Telegram-чаты по тегам. Sink проставляет в каждую заметку об инциденте поля публикации, поэтому подключённая база знаний может запостить инцидент в группу и отредактировать то же сообщение, когда он резолвится. Один инцидент, одно сообщение, которое само переключается из firing в resolved.

Последний пункт мне и кажется самым интересным. База знаний тут не просто архив, в котором случайно лежат инциденты. Она и есть рассказчик. Sink пишет заметку, а всё дальше, страница-журнал, поиск, пост в Telegram, это база знаний, которая ведёт себя так же, как с любой другой заметкой.

Не архив: живой контекст для людей и агентов

Пункт «поиск» выше не про полнотекстовый поиск в привычном смысле. Поиск в trip2g гибридный: BM25 по тексту плюс семантический векторный поиск (эмбеддинги OpenAI, слияние через RRF). История инцидентов становится запрашиваемой по смыслу, а не только по ключевым словам. «Пул соединений Postgres упёрся в лимит» найдёт заметку про инцидент с too many clients, даже если этих слов в заголовке нет.

Интереснее другое: что это открывает для агентов. У trip2g есть MCP-сервер: подключите его к любому совместимому клиенту, и база знаний становится инструментом, который агенты могут вызывать напрямую. Для истории инцидентов это значит: агент делает search("cache latency high") и получает три наиболее релевантных прошлых инцидента, ранжированных по семантическому сходству. Дальнейший сценарий очевиден: агент, разбирающийся с текущим алертом, сначала поднимает собственную историю и только потом выдаёт рекомендацию.

Конкретный пример. Агент по деплою видит сработавший CacheLatencyHigh. Он делает search("cache latency high") по базе инцидентов, получает постмортем полугодовой давности, где тот же симптом привёл к неправильно настроенной политике вытеснения, и включает это в отчёт. Постмортем написал человек в заметку, которую sink никогда не трогает. База знаний просто сохранила её.

Семантический поиск включается по желанию. По умолчанию он выключен: вы включаете его, указав trip2g свой сервер эмбеддингов (любой OpenAI-совместимый эндпоинт, локальный или внешний) через флаг vector_search, см. поиск. В демо он выключен. Обычный BM25-поиск по тексту истории инцидентов работает вообще без настройки, семантическое ранжирование -- это дополнительный шаг. А вот MCP-интерфейс -- это тот же интерфейс, что предоставляет любой инстанс trip2g, поэтому search() из агента доступен в любом случае, просто использует тот поиск, что вы включили.

Чтобы не гадать, я это прогнал. Поднял демо-стек, отправил три инцидента через Alertmanager, направил инстанс на локальный сервер эмбеддингов bge-m3 и поискал. Запросы ниже не имеют ни одного общего слова с заметками инцидентов, а два из них -- русские против английских заметок. Реальные результаты того прогона:

Запрос (человеческий язык) Первый результат (реальная заметка) Почему keyword его бы не нашёл
"database slow" HighPostgresLatency -- "p99 latency on postgres spiked to 4s" в заметке нет ни «database», ни «slow»
"база тормозит" HighPostgresLatency -- та же заметка, на английском другой язык, ноль общих токенов
"закончилось место" DiskFillingNode2 -- "root filesystem 96% on node-2" в заметке «filesystem», а не «место»
"агент упал по памяти" HermesOOM -- "hermes-agent killed by OOM" «OOM» vs «по памяти», английский vs русский

Каждый запрос вернул нужный инцидент первым. Что именно сработал векторный слой, видно по одной детали: пока эмбеддинги ещё не загрузились, тот же запрос "база тормозит" возвращал только общий always-firing демо-алерт (шум BM25); как только заметки проэмбеддились, HighPostgresLatency встал на первое место. Реальные совпадения зависят от того, как у вас называются инциденты. Суть в другом: запрос на человеческом языке находит инциденты, названные на языке мониторинга -- а это почти всегда разные слова и нередко разные языки.

git clone https://github.com/trip2g/alert-sink
cd alert-sink/demo
docker compose up --build

Подождите минуту и откройте http://localhost:8080/incidents. Это журнал инцидентов, страница trip2g со списком всех инцидентов от новых к старым. Демо-алерт там уже есть. Чтобы посмотреть полный жизненный цикл:

docker compose stop demo-target    # DemoTargetDown срабатывает через ~1 минуту
docker compose start demo-target   # та же заметка переключается в resolved

Немного инженерных заметок

Три решения, которые я готов защищать.

Две заметки, чёткое разделение. Заметка об инциденте принадлежит машине: sink пишет её и патчит целиком, людям в неё лезть не нужно. Постмортем это отдельный файл, который человек или агент создаёт потом. В заметке инцидента лежит wikilink на путь постмортема; незаполненная ссылка служит «создать» кнопкой в Obsidian и trip2g. Sink этот путь никогда не пишет и не читает.

При резолве sink находит один непрерывный блок в конце frontmatter и строку видимого статуса сразу после него, и заменяет всё это одной операцией. Frontmatter меняется с status: firing на status: resolved, а строка статуса переключается с 🔴 FIRING на ✅ RESOLVED. Всё остальное в заметке остаётся нетронутым. Отсюда и одно сообщение в Telegram: trip2g редактирует тот же пост на месте, без нового сообщения.

Auth через self-mint. В trip2g нет долгоживущих scoped-токенов на запись, поэтому sink держит JWT-секрет инстанса и на каждую запись сам подписывает себе свежий токен на 5 минут. Ничего долгоживущего за пределами самого секрета не существует. Честная оговорка: такой токен даёт админа на своём инстансе, path-level скоупинга в trip2g нет. Least privilege тут достигается радиусом поражения: направьте sink на отдельный инстанс, в котором лежит только история алертов.

Alertmanager никогда не ждёт. Webhook-хендлер валидирует, кладёт в очередь и отвечает 200 за миллисекунды. Реальные записи делает воркер-горутина с экспоненциальным бэкоффом, так что лежащий trip2g не создаёт back-pressure на пайплайн алертинга. А это важно: моменты, когда trip2g может лежать, это ровно те моменты, когда алерты и сыпятся.

Внутри Go stdlib плюс одна JWT-библиотека. Ни базы данных, ни состояния за пределами самой базы знаний. Лицензия MIT: https://github.com/trip2g/alert-sink