Skip to content
Exlogare

Повторяющиеся падения (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:

  1. Чистит лог, определяет CI-провайдер и service_tag, считает lookup_hash.
  2. Ищет последний кластер с этим lookup_hash для текущего tenant.
  3. Если кластер уже есть и count > 1, а last_seen_at находится в окне COST_SAVER_TTL_HOURS (по умолчанию 6 часов) — переиспользует предыдущий анализ (severity и весь RCA берутся из него).
  4. Иначе вызывает LLM, сохраняет анализ, считает fingerprint_hash = sha1(lookup_hash | severity) и UPSERT-ит кластер по этому ключу.
  5. В обоих случаях отправляются уведомления в мессенджеры и 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_…"

Параметры:

  • statusactive (по умолчанию во вкладке), 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 (mediumhigh) даёт два разных кластера. Это сделано осознанно: если флаки-тест переродился в реальный инцидент, оператор должен видеть новый кластер, а не молчаливый апгрейд старого.

Пример: таймаут к 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 для полного списка инвариантов).