Мониторинг самого gotcha

gotcha следит за вашими сервисами. Эта страница — о том, как следить за самим gotcha: что он рассказывает о своём здоровье и куда смотреть, если вы подозреваете, что он теряет данные.

Эндпойнты

ПутьДля чего
/metricsСчётчики буферов, потерь и отказов вставки в формате Prometheus. Не обращается к базам вообще, поэтому отвечает даже когда PostgreSQL или ClickHouse лежит — а именно тогда он и нужен.
/healthzЖивость: отвечает 200, пока процесс обслуживает HTTP. Состояние баз есть в теле (postgres, clickhouse, version), но на код ответа не влияет — вешайте сюда liveness-пробу.
/readyzГотовность: те же поля плюс status, но 503, пока PostgreSQL или ClickHouse недоступны. Сюда — readiness-пробу и healthcheck контейнера.
/versionМетаданные сборки: version, commit, date, go (версия Go, которой собран бинарь) и stamped — вшиты ли в сборку git-метаданные. stamped: false означает, что образ собран мимо make и версия взята из исходников, а не из тега релиза.

Разделение важно: liveness-проба на ручке, которая падает при сбое хранилища, перезапускает живой процесс, а каждый перезапуск выбрасывает буферы — то есть ровно ту телеметрию, которую они копили, дожидаясь возвращения хранилища.

Ни один не требует аутентификации и ни один не отдаёт персональных данных: /metrics содержит только счётчики, никогда — содержимое событий. Если инстанс смотрит в интернет, ограничьте /metrics на реверс-прокси: числа выдают объём вашего трафика, а его не всегда стоит публиковать.

Когда контейнер стал unhealthy

Штатный compose-файл даёт контейнеру gotcha healthcheck на /readyz. Проверка — подкоманда самого бинаря, gotcha --healthcheck: она переживёт переход на distroless-базу, где нет curl. Целевой URL строится из GOTCHA_ADDR (хост всегда 127.0.0.1 — проверка ходит к себе), поэтому смена порта прослушивания не оставляет проверку стучаться в мёртвый :8080; для нестандартных схем — TLS внутри контейнера, другой путь — URL переопределяется флагом --healthcheck-url=<url>.

Но важно понимать, что даёт healthcheck: docker compose не перезапускает unhealthy контейнер. Проваленная проверка только меняет метку в docker ps (реакция на unhealthy есть у Swarm/Kubernetes); политика restart ловит упавший процесс, но не зависший. Посмотреть состояние и вывод последних проверок:

docker ps                                                # колонка STATUS: (healthy) / (unhealthy)
docker inspect --format '{{json .State.Health}}' gotcha-gotcha-1

Чтобы состояние было видно снаружи, следите не за меткой, а за сервисом: направьте на /readyz аптайм-монитор с другого инстанса gotcha (мониторинг HTTP-ручки — ровно то, что продукт умеет штатно) или заведите алерт на метрику gotcha_up вашего сборщика.

Автолекаря с доступом к docker-сокету в поставке нет намеренно: доступ к сокету — это root на хосте, и такая поставка обменяла бы безопасность всей установки на сценарий, который лучше решает супервизор.

Как собирать

Подойдёт любой агент, понимающий формат Prometheus, — Prometheus, VictoriaMetrics, Grafana Agent, OpenTelemetry Collector:

scrape_configs:
  - job_name: gotcha
    static_configs:
      # 8080 — порт ВНУТРИ контейнера; наружу штатный compose публикует 59080
      # (GOTCHA_PORT). Укажите тот порт, по которому инстанс доступен вам.
      - targets: ["gotcha.example.com:59080"]

Что означают метрики

gotcha_writer_buffered_rows{writer="…"} — строки, лежащие в памяти в ожидании записи в ClickHouse. Писатели: events, spans, metrics, profiles, logs, uptime_results. На здоровом инстансе значение держится около нуля и сбрасывается за секунду-другую. Число, которое растёт и не падает, означает, что ClickHouse не принимает записи.

gotcha_writer_insert_failures_total{writer="…"} — сколько батчей не записалось. Само по себе это не потеря: батч возвращается в буфер и повторяется. Рост счётчика при стабильном буфере означает, что транзиентные ошибки поглощаются; рост вместе с растущим буфером — что повторы не проходят, и дело идёт к потере.

gotcha_writer_dropped_rows_total{writer="…"} — строки, выброшенные из-за переполнения буфера. Они потеряны безвозвратно. Любое ненулевое значение заслуживает внимания; растущее означает, что телеметрия теряется прямо сейчас.

gotcha_pipeline_queued_tasks / gotcha_pipeline_queue_capacity — глубина и размер очереди приёма между HTTP-обработчиком и воркерами. Глубина, подолгу близкая к вместимости, означает, что воркеры не успевают — обычно из-за медленного PostgreSQL (каждая задача делает апсерт issue).

gotcha_pipeline_queued_bytes — байты, занятые задачами в этой очереди. У очереди помимо счётного есть байтовый бюджет (GOTCHA_MAX_QUEUE_BYTES): тысяча мелких событий и тысяча мегабайтных — очень разная нагрузка при одной глубине. Когда дропы идут с reason="queue_bytes", кончился именно этот бюджет.

gotcha_pipeline_dropped_tasks_total{reason="…"} — события и транзакции, выброшенные конвейером. Тоже безвозвратно. Метка reason говорит, что чинить:

reasonЧто произошлоЧто делать
queue_fullобработка не успевает за приёмомбольше воркеров, быстрее PostgreSQL
queue_bytesисчерпан байтовый бюджет очереди — задачи крупнее обычногосмотреть GOTCHA_MAX_QUEUE_BYTES и размер событий
storage_errorне удалось записать в PostgreSQL (обычно таймаут апсерта)чинить базу; очередь тут ни при чём
panicобработчик упал на конкретном элементеошибка продукта: сообщите её нам с логом
closedприём уже остановлен, событие пришло на излётенормально при выключении; постоянный рост означает рестарт-цикл

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

gotcha_cardinality_collapsed_total / gotcha_cardinality_tracked_values — работа защиты кардинальности: сколько значений открытых полей (имена транзакций, окружения, имена метрик, сервисы, операции) схлопнуто в переполненную корзину из-за достижения проектом GOTCHA_CARDINALITY_LIMIT, и сколько различных значений защита помнит прямо сейчас. Растущий счётчик схлопнутого означает, что часть имён пропала из списков — на затронутых экранах показывается предупреждение; обычная причина — идентификатор, попавший в имя. Gauge отслеживаемых значений — собственный расход памяти защиты.

gotcha_host_evaluator_last_tick_timestamp_seconds / gotcha_host_evaluator_tick_duration_seconds — момент последнего завершённого прохода оценщика хостов (диск, память, нагрузка, тишина) и его длительность. Наблюдать здесь надо не отказ, а продолжение работы: умерший оценщик снаружи выглядит ровно как «на хостах всё спокойно» — тишина и есть его обычный вывод. Разница между текущим временем и меткой, заметно превышающая GOTCHA_HOST_EVAL_INTERVAL, означает, что пороги хостов не вычисляются; длительность, подбирающаяся к интервалу, — что оценщик перестаёт укладываться в свой период (обычно из-за медленного ClickHouse или разросшегося парка).

Метку времени двигает только проход, дошедший до конца. Проход, оборванный собственным дедлайном (он успевает оценить часть парка и выходит), метку НЕ обновляет — иначе оценщик, который каждый раз обрывается на половине хостов, выглядел бы отсюда идеально здоровым. Длительность при этом публикуется всегда: по ней и виден упор в бюджет. Причина обрыва — строкой tick did not finish within its budget в журнале.

gotcha_slo_evaluator_last_tick_timestamp_seconds / gotcha_slo_evaluator_tick_duration_seconds — момент последнего завершённого прохода оценщика burn rate по всем включённым SLO и его длительность. Слепая зона та же, что у оценщика хостов: тишина — нормальный вывод, поэтому умерший оценщик выглядит ровно как «все бюджеты в порядке». Разница между текущим временем и меткой, заметно превышающая GOTCHA_SLO_EVAL_INTERVAL, означает, что burn rate не пересчитывается, а инциденты сжигания бюджета не открываются и не закрываются; длительность, подбирающаяся к интервалу, — что оценщик перестаёт укладываться в период.

gotcha_host_registration_failures_total — сколько фоновых записей реестра хостов не удалось выполнить. Пока счётчик растёт, last_seen хостов не обновляется, а значит инциденты «тишина» могут открываться по живым машинам; причина почти всегда в недоступности PostgreSQL.

gotcha_host_registrations_rejected_total — новые имена хостов, отброшенные потолком в 1000 хостов на проект. Ненулевое значение означает, что в разделе «Хосты» не появляются новые машины: либо парк действительно дорос до потолка, либо в имя хоста попал идентификатор (поды, автоскейл), и каждый экземпляр регистрируется как отдельная машина.

gotcha_notify_pending_jobs / gotcha_notify_oldest_pending_age_seconds — глубина очереди доставки и возраст самой старой ждущей задачи. Возраст важнее глубины: только он отличает «очередь пуста, потому что всё доставлено» от «очередь стоит». Растущий возраст при живом процессе означает, что доставка упирается в канал — смотрите gotcha_notify_retried_total и журнал доставок в интерфейсе.

gotcha_notify_sent_total / gotcha_notify_failed_total / gotcha_notify_retried_total — доставлено, брошено после исчерпания попыток, отложено на повтор. gotcha_notify_failed_jobs — сколько таких брошенных задач лежит в очереди прямо сейчас.

gotcha_memory_limit_bytes — потолок кучи, который продукт вывел из лимита контейнера (80% лимита). Ноль означает, что лимита нет: буферы будут расти, пока не кончится память ХОСТА, и первым сработает OOM-killer ядра — а он выбрасывает всё накопленное, а не избыток. Если здесь ноль, задайте mem_limit контейнеру или GOMEMLIMIT вручную.

gotcha_entities_purged_total — строки, удалённые из PostgreSQL по истечении GOTCHA_RETENTION_DAYS: группы, закрытые инциденты, регрессии. Ожидаемое поведение, а не сбой; счётчик существует потому, что у любого исчезновения данных должно быть число, которое можно посмотреть. Устойчивый ноль при заданном сроке хранения означает, что чистка не работает — и список проблем показывает группы, событий которых уже нет.

gotcha_purge_queue_depth / gotcha_purge_queue_oldest_seconds — сколько проектов ждут очистки телеметрии в ClickHouse после удаления и сколько секунд ждёт самая старая заявка. Удаление проекта ставит заявку той же транзакцией, что удаляет строку, а выполняет её фоновый исполнитель — запрос не держит восемь тяжёлых мутаций. Величины две, потому что глубина одна ничего не говорит: одна заявка, висящая третьи сутки, по глубине неотличима от поставленной минуту назад. Растущий возраст означает невыполненное требование об удалении данных — причина последней попытки лежит в project_purge_queue.last_error.

gotcha_projects_purged_total — сколько проектов очищено. Как и gotcha_entities_purged_total, существует потому, что у исчезновения данных должно быть число.

gotcha_storage_free_bytes{store="…"} / gotcha_storage_total_bytes{store="…"} — свободно и всего байт на томе, где хранилище физически держит данные. Сегодня их отдаёт только store="clickhouse" (системная таблица дисков ClickHouse): у PostgreSQL через обычное соединение узнать объём ТОМА нельзя — СУБД знает размер своих данных, но не размер диска под собой, — поэтому под store="postgres" этих двух метрик нет и не будет; смотрите gotcha_storage_used_bytes ниже. Значение — NaN, а не 0, пока опрос ни разу не завершился успешно. Это не «подождите пару минут после запуска»: первый опрос синхронный и происходит прямо при регистрации метрик, до того как открывается порт, — к моменту, когда /metrics вообще можно прочитать, первый опрос уже случился. Поэтому NaN в выдаче означает ровно одно: опрос проваливается — например, у служебного пользователя нет доступа к системной таблице дисков ClickHouse, или запрос к PostgreSQL не укладывается в таймаут. Рядом в логе — запись storage metrics: poll failed с полями store и error, которая скажет, почему именно. Повтор — раз в 5 минут; ноль вместо NaN тут не подошёл бы: он читался бы как «диск заполнен», а не как «опрос ломается».

gotcha_storage_used_bytes{store="postgres"} — сколько места на диске уже занимают данные PostgreSQL (размер базы). Это не свободное место и не общий объём тома — см. предыдущий пункт про то, почему PostgreSQL их не отдаёт. Чтобы понять, сколько ещё есть запаса, сопоставьте это число с известным вам размером тома PostgreSQL (обычно один том на инстанс) — вручную: gotcha не может сделать это за вас, он не знает размер вашего диска.

gotcha_web_cross_origin_rejected_total — POST-запросы, отклонённые из-за несовпадения Origin/Referer с GOTCHA_BASE_URL (защита интерфейса от межсайтовых запросов). Редкие единицы — шум сканеров; постоянный рост от живых пользователей обычно означает, что GOTCHA_BASE_URL не совпадает с адресом, по которому реально открывается интерфейс — например, за прокси, переписывающим схему.

gotcha_build_info — всегда 1, версия и режим лежат в метках. Полезно, чтобы убедиться, что развёрнуто именно то, что вы думаете. Метка stamped говорит, несёт ли сборка git-метаданные: stamped="false" означает образ, собранный мимо make, — его строка версии взята из исходников, и сверить по ней «задеплоено именно то» нельзя.

Какие алерты стоит завести

# Данные теряются прямо сейчас.
increase(gotcha_writer_dropped_rows_total[5m]) > 0
increase(gotcha_pipeline_dropped_tasks_total[5m]) > 0

# Хранилище не успевает — потери впереди.
gotcha_writer_buffered_rows > 5000
gotcha_pipeline_queued_tasks / gotcha_pipeline_queue_capacity > 0.5

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

Место на диске — отдельная пара правил для каждого хранилища, не одна на оба: ClickHouse отдаёт настоящую долю, а PostgreSQL — нет (см. gotcha_storage_used_bytes выше), поэтому для него порог приходится строить на прогнозе роста, а не на проценте.

# ClickHouse: доля свободного места известна напрямую — free и total
# приходят из одной и той же строки system.disks, дробь честная.
gotcha_storage_free_bytes{store="clickhouse"} / gotcha_storage_total_bytes{store="clickhouse"} < 0.1

# PostgreSQL: gotcha не знает объём тома, поэтому вместо доли — прогноз по
# тренду: predict_linear экстраполирует used_bytes на сутки вперёд по
# последним 6 часам роста. Порог ниже — пример для тома 20 ГБ (минимум из
# требований к диску), 90% от него; подставьте 90% от ВАШЕГО известного
# размера тома в байтах.
predict_linear(gotcha_storage_used_bytes{store="postgres"}[6h], 24*3600) > 1.8e10

Сравнение с NaN не проходит, поэтому пока опрос не даёт значения, оба правила выше молчат — не срабатывают. Из этого следует то, о чём легко не подумать: само правило о сломанном опросе не сообщит. Оно рассчитано на случай, когда число ЕСТЬ и превышает порог, а не на случай, когда числа нет вообще. Значит на «опрос не проходит» нужен отдельный признак — не порог по значению, а слежение за самим фактом NaN в выдаче /metrics (или, что надёжнее, за логом: записи storage metrics: poll failed с полем store).

Если «часть событий не доходит»

  1. Посмотрите gotcha_writer_dropped_rows_total и gotcha_pipeline_dropped_tasks_total. Ненулевые значения означают, что выбросил их сам gotcha. Смотрите метку reason — она и говорит, что чинить.
  2. Потерь нет? Значит события не доехали. Проверьте DSN отправителя и не исчерпала ли организация квоту: отклонённое по квоте событие попадает в счётчик «отброшено» на странице настроек организации — это другой счётчик, не эти. Проверьте заодно /readyz: при недоступном PostgreSQL приём отвечает, но события до хранилища не доходят.
  3. Следите за буфером по ходу разбора. Ровный буфер без потерь означает, что приём здоров и проблема выше по течению, до gotcha.

Если место на диске подходит к концу

  1. Оцените, насколько заполнено. Для ClickHouse — доля gotcha_storage_free_bytes{store="clickhouse"} / gotcha_storage_total_bytes{store="clickhouse"}: ниже 10% значит близко к отказу. Для PostgreSQL готовой доли нет — сравните gotcha_storage_used_bytes{store="postgres"} с известным вам размером тома вручную; тренд роста важнее самого числа — он говорит, сколько ещё есть времени, а не только сколько занято сейчас.
  2. Проверьте, работает ли чистка. gotcha_entities_purged_total должен расти, если задан GOTCHA_RETENTION_DAYS; устойчивый ноль означает, что чистка PostgreSQL не работает, хотя должна (см. выше). Для ClickHouse TTL применяется автоматически, но у каждого вида данных свой срок хранения — см. Конфигурацию.
  3. Определите, что растёт быстрее всего. По умолчанию самые тяжёлые по объёму — профили (GOTCHA_PROFILE_RETENTION_DAYS, поэтому его дефолт и короче остальных — 7 дней). Если вы отправляете continuous profiling, но не смотрите флеймографы регулярно, это первый кандидат на сокращение срока хранения или остановку на стороне SDK.
  4. Освободите место сейчас, а не только через TTL. Удаление проекта («Настройки проекта» → «Опасная зона» → «Удалить проект») стирает его телеметрию из ClickHouse сразу, а не постепенно — годится для тестовых или заброшенных проектов, накопивших данные впустую.
  5. Если места мало систематически — уменьшите срок хранения. Сроки задаются отдельно на каждый вид данных (GOTCHA_RETENTION_DAYS, GOTCHA_SPAN_RETENTION_DAYS, GOTCHA_METRIC_RETENTION_DAYS, GOTCHA_PROFILE_RETENTION_DAYS — см. Конфигурацию). Изменение действует со следующего старта и не восстанавливает уже удалённое задним числом; и место при этом освобождается не мгновенно — ClickHouse удаляет просроченные данные фоновыми слияниями по обычному расписанию, а не в момент правки конфигурации.

Что дальше