Повторяющиеся падения (clusters)
Каждое падение CI, которое случилось больше одного раза, автоматически объединяется в кластер. Вы видите его в дашборде, можете запросить через API, а cost-saver-режим пропускает повторные вызовы LLM.
Exlogare автоматически группирует одинаковые падения в кластеры. Каждый кластер — это одно повторяющееся падение в CI вашей команды: счётчик, время первого и последнего срабатывания, и статус, которым можно управлять из дашборда или API.
Кластеры доступны на любом плане. Cost-saver (переиспользование предыдущего анализа вместо повторного вызова LLM) включён по умолчанию.
Зачем нужна кластеризация
Падения CI обычно приходят пачками: один флаки-тест валит пять подряд, один битый Docker-image ломает все билды, одна плохая миграция блокирует команду. Без кластеризации в дашборде вы видите пять одинаковых строк. С кластеризацией — одну строку с бейджем ×5.
Три прямых выгоды:
- Меньше шума. На странице «Анализы» рядом с причиной падения появляется маленький бейдж
×N, если падение случалось больше одного раза. Подтверждённые кластеры скрывают бейдж, чтобы команда не спамилась во время известного инцидента. - Детекция регрессий. Когда кластер, который вы пометили Решено, повторяется снова — он автоматически переходит обратно в Активные, чтобы регрессия не утонула в списке «исправленного».
- Cost-saver-режим. Если то же падение повторяется в течение 6 часов, следующий ingest переиспользует предыдущий анализ вместо нового вызова LLM. Эта запись попадает в Usage как нетарифицируемое событие.
Как это работает
Каждый успешный анализ пишет строку в таблицу кластеров с двумя связанными хешами:
lookup_hash— считается до вызова LLM.sha1(version | provider | service_tag | normalised_log_tail). Используется cost-saver-ом, чтобы ответить на вопрос «видел ли я только что такую же форму лога на этом CI-провайдере для этого сервиса?» — и если да, переиспользовать предыдущий разбор.fingerprint_hash— идентичность кластера, считается после LLM.sha1(lookup_hash | severity). Кластер уникален в паре(tenant, fingerprint_hash).
Хеш учитывает только форму падения: таймстемпы, UUID-ы, номера билдов, hex-блобы и временные пути перед хешированием маскируются. Поэтому «build #4321 timed out» и «build #4322 timed out» попадают в один кластер, а «OOM in pytest» и «connection refused» — нет.
Каждый ingest:
- Чистит лог, определяет CI-провайдер и
service_tag, считаетlookup_hash. - Ищет последний кластер с этим
lookup_hashдля текущего tenant. - Если кластер уже есть и
count > 1, аlast_seen_atнаходится в окнеCOST_SAVER_TTL_HOURS(по умолчанию 6 часов) — переиспользует предыдущий анализ (severity и весь RCA берутся из него). - Иначе вызывает LLM, сохраняет анализ, считает
fingerprint_hash = sha1(lookup_hash | severity)и UPSERT-ит кластер по этому ключу. - В обоих случаях отправляются уведомления в мессенджеры и outbound-webhooks для этого запуска.
Жизненный цикл кластера
У кластера всегда один из трёх статусов:
| Статус | Что значит |
|---|---|
active | Открытый, повторяющийся. Виден в дашборде и триггерит recurring-бейдж. |
acknowledged | «Знаем, работаем над этим». Бейдж повторений скрывается, чтобы не спамить; счётчики продолжают расти. |
resolved | Помечен как исправленный. Новое срабатывание автоматически возвращает статус в active (детекция регрессии). |
Сменить статус можно во вкладке Повторения в дашборде или через API.
Дашборд
Вкладка Повторения в боковом меню показывает три подвкладки (Активные / Подтверждённые / Решённые) с количеством. Каждая строка ведёт к последнему анализу. Кнопки в строке позволяют подтвердить, отметить решённым или вернуть в активные.
На странице Анализы рядом с root cause появляется бейдж ×N, если у падения больше одного срабатывания и кластер не подтверждён. Клик по бейджу переводит в раздел Повторения.
API
Все эндпоинты для чтения кластеров — под /api/v1/clusters и требуют API-токен со scope read.
Список кластеров
curl "https://app.exlogare.com/api/v1/clusters?status=active&limit=50" \
-H "Authorization: Bearer exl_…"
Параметры:
status—active(по умолчанию во вкладке),acknowledged,resolved. Можно опустить, чтобы получить все.limit— 1..500 (по умолчанию 100).offset— пагинация.
Ответ:
{
"items": [
{
"id": "9f7c…",
"fingerprint_hash": "5d0e71c2…",
"last_root_cause": "Connection refused on 5432",
"last_severity": "high",
"count": 7,
"first_seen_at": "2026-04-20T10:00:00+00:00",
"last_seen_at": "2026-04-25T16:42:00+00:00",
"status": "active",
"last_analysis_id": "abc1…",
"acknowledged_at": null,
"resolved_at": null
}
],
"total": 1,
"limit": 50,
"offset": 0
}
Конкретный кластер
curl "https://app.exlogare.com/api/v1/clusters/9f7c…" \
-H "Authorization: Bearer exl_…"
Возвращает запись той же формы, что и в списке.
Смена статуса
Acknowledge / resolve / reopen доступны только через сессионную авторизацию (cookie или admin JWT) и живут под /api/clusters/..., а не в публичном /api/v1/.... Это сделано осознанно: давать ingest-токену право переключать статусы — слишком большой blast radius.
Cost-saver-режим
Cost-saver включён по умолчанию для каждого tenant. Когда дублирующее падение приходит в окне COST_SAVER_TTL_HOURS (по умолчанию 6 часов), Exlogare:
- Пропускает вызов LLM.
- Переиспользует предыдущий анализ (та же причина, тот же fix).
- Сохраняет ingestion event, чтобы дашборды оставались точными.
- Записывает usage event типа
clustered_reuse— он виден на странице Usage как «сэкономлено cost-saver-ом», но не списывается с плана и не съедает prepaid. - Всё равно отправляет уведомления в мессенджеры и outbound-webhooks (вам же важно знать, что падение повторилось).
Чтобы выключить cost-saver, обратитесь в поддержку — UI-переключателя пока нет, потому что текущий дефолт устраивает всех клиентов, с которыми мы общались.
Что схлопывается, а что нет
Два падения попадают в один кластер, если после нормализации форма очищенного лога совпадает и провайдер CI с service_tag совпадают. Нормализация маскирует:
- Таймстемпы (
2026-04-25T10:00:00Z→<TS>). - UUID-ы (
123e4567-…→<UUID>). - Hex-блобы длиной ≥ 8 символов (хеши коммитов, build ID).
- Временные пути (
/tmp/…). - Длинные числа (build ID, порты, длительности).
Что не маскируется и продолжает разводить кластеры:
- Названия сервисов и БД:
postgres,mssql,mysql,redis,kafkaи т.п. — слова сохраняются в нормализованном тексте, поэтому таймаут к Postgres и таймаут к MSSQL дают разныеlookup_hash. - CI-провайдер: одинаковый по форме лог из Jenkins и из CircleCI попадёт в разные кластеры.
- Severity от LLM: один и тот же
lookup_hashс разной severity (medium→high) даёт два разных кластера. Это сделано осознанно: если флаки-тест переродился в реальный инцидент, оператор должен видеть новый кластер, а не молчаливый апгрейд старого.
Пример: таймаут к Postgres против таймаута к MSSQL
Допустим, две похожие ошибки:
build #4321: psycopg2.OperationalError: timeout expired connecting to db.prod.local:5432
build #4322: pyodbc.OperationalError: Login timeout expired (mssql at sqlserver.prod.local:1433)
Оба пройдут через нормализацию (4321/4322/5432/1433 уйдут в <N>), но детектор сервисов увидит psycopg в первом логе → service_tag=postgres, а pyodbc/mssql во втором → service_tag=mssql. lookup_hash получаются разные → два разных кластера, у каждого свой счётчик и своя история.
Если бы оба падения были по Postgres, но с разной severity (LLM прозвал первое medium «флаки», а второе high после повторов) — lookup_hash бы совпал, но fingerprint_hash различался, и снова получились бы два кластера: «нерешённый флак» и «активный инцидент».
Какие service_tag распознаются автоматически
Эвристика смотрит последние 2000 символов лога (там обычно живёт сама ошибка) и подбирает первый подходящий тег из списка. Порядок — от самого специфичного к общему:
- Базы данных:
postgres,mssql,oracle,mysql,mongodb,redis,elasticsearch. - Очереди и брокеры:
kafka,rabbitmq. - Контейнеры и оркестрация:
kubernetes,docker. - Менеджеры пакетов:
npm,yarn,pip,poetry,gradle,maven,cargo,go-modules. - Тест-фреймворки:
pytest,jest,junit,rspec,go-test.
Если ничего не подошло — service_tag пуст, и кластеризация работает как раньше (только по форме лога и провайдеру). Это безопасный дефолт: пустой тег не «склеивает» неподходящие падения, а лишь не разделяет их сильнее.
Что fingerprint всё-таки может пропустить
- Очень длинные логи (> 4000 нормализованных символов). Хеш считается только по хвосту в 4000 символов после нормализации. Если разница между двумя падениями уехала в начало (например, упоминание сервиса в самом начале setup-секции, а в конце универсальный stack trace),
lookup_hashможет совпасть. На практике редко: в большинстве CI-логов конкретика по сервису встречается ближе к концу — там, где падает шаг. - Сервис в логах отсутствует. Если в хвосте нет ни одного знакомого ключевого слова,
service_tagбудет пустым. Два разных падения, не упоминающих ни Postgres, ни Redis, ни pytest — попадут в один кластер. Это компромисс в сторону стабильности кластеризации; помогает явный resolve вручную.
Действительно разные ошибки остаются в своих кластерах. Если анализатор всё-таки выдал одинаковый root cause для двух разных падений — пометьте один кластер как resolved, и при следующем срабатывании запись пересоздастся.
Приватность
Кластеры живут внутри той же tenant-границы, что и всё остальное: строки чужих tenant-ов никогда не возвращаются, даже при случайном совпадении fingerprint-ов. Сам fingerprint — это hash очищенного лога, и хранится он рядом с анализом, который сам по себе не сохраняет сырой лог (см. data privacy для полного списка инвариантов).