Мониторинг самого 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 и версия взята из исходников, а не из тега релиза. Отдаёт точную версию анониму без аутентификации — стоит закрыть так же, как /metrics (Усиление установки).

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

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

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

Штатный compose-файл даёт контейнеру gotcha healthcheck на /readyz. Проверка — подкоманда самого бинаря, gotcha --healthcheck: она переживёт переход на distroless-базу, где нет curl. Целевой URL строится из GOTCHA_LISTEN_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_COMPOSE_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_queue_depth / gotcha_pipeline_queue_capacity — глубина и размер очереди приёма между HTTP-обработчиком и воркерами. Глубина, подолгу близкая к вместимости, означает, что воркеры не успевают — обычно из-за медленного PostgreSQL (каждая задача делает апсерт issue).

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

Канон имени очереди — gotcha_<подсистема>_queue_depth / _queue_oldest_seconds / _queue_failed / _queue_capacity / _queue_bytes (по необходимости): та же форма у gotcha_purge_queue_* ниже и у gotcha_export_*/gotcha_notify_* дальше по странице. До версии, где написан этот абзац, три подсистемы называли одну и ту же пару «глубина + возраст самого старого» тремя разными словами (_pending_jobs/_oldest_pending_age_seconds у export и notify, _queued_tasks/_queued_bytes у pipeline) — расхождение подряд ловилось только на глаз при чтении дашборда.

gotcha_ingest_key_rejections_total{reason="…"} — запросы, отклонённые на этапе аутентификации по ключу, ещё до квот и до разбора тела. Такой запрос вообще не дошёл ни до одного проекта, поэтому в счётчиках отказов самого проекта он не виден — это единственное место, где он вообще отображается. Метка reason разделяет ошибки на стороне клиента:

reasonЧто произошло
missing_keySentry-запрос пришёл вовсе без sentry_key
invalid_keysentry_key прислан, но не резолвится ни в один проект (опечатка, отозванный ключ)
project_mismatchключ резолвится нормально, но в другой проект относительно того, что указан в пути запроса — обычно DSN, скопированный не туда
missing_bearerOTLP-запрос пришёл без заголовка Authorization: Bearer
invalid_dsn_keyOTLP bearer-токен не резолвится ни в один DSN
scopeключ резолвится и принадлежит проекту, но его тип не допущен к этому эндпойнту (см. Ключи приёма); строка в логе несёт путь эндпойнта

Ровный небольшой поток — норма: на это натыкаются сканеры и устаревшие настройки SDK. Скачок сразу после деплоя обычно означает, что DSN или ID проекта поменялись, а один из отправителей не обновили.

gotcha_ingest_rejected_total{reason="…",signal="…"} — тот же приём, но огрублённо и по ВСЕМ причинам отказа сразу, с меткой вида телеметрии (signal: event, transaction, metric, profile, log, deploy). Метрика выше детализирует ТОЛЬКО отказ по ключу и не говорит, какой из шести входов он касается; частота (rate_limit), квота организации (quota) и размер тела (too_large) не были видны в метриках вовсе — только в логе конкретного эндпойнта. reason — закрытый набор:

reasonЧто произошло
key_unknownключ приёма не резолвится: отсутствует, опечатан, отозван, либо резолвится в чужой проект (все три случая gotcha_ingest_key_rejections_total выше схлопнуты в одну причину — детали смотрите там)
key_revokedзарезервировано, сегодня не встречается: резолвер ключа не различает «ключа никогда не было» от «ключ отозван» (обе ветки — org.ErrNotFound)
rate_limitпревышен per-DSN лимит частоты (см. GOTCHA_INGEST_RATE_PER_SEC)
quotaорганизация исчерпала месячную квоту этого вида телеметрии — запрос ПОЛНОСТЬЮ отклонён (429); частичное списание квоты смешанного envelope сюда не попадает, только полный отказ
too_largeтело превысило лимит размера
malformedтело не разобралось: битый JSON/protobuf, повреждённый gzip/zstd
key_scopeтот же отказ по типу ключа, что и scope у gotcha_ingest_key_rejections_total выше, но с меткой signal — какой именно вид телеметрии был закрыт этому типу ключа

Обе метрики ключевого отказа намеренно сосуществуют: одна — узкая и точная про сам ключ, другая — широкая и сравнимая между видами телеметрии.

gotcha_ingest_deprecated_path_total{path="…"} — запросы, пришедшие на путь приёма, у которого появилась замена. Три входа переехали в собственный неймспейс gotcha /api/v1/*; старые пути продолжают работать, но каждый запрос к ним считается здесь и получает в ответ заголовки Deprecation и Link; rel="deprecation". Список путей, их замены и срок удаления — в Политике версионирования.

Смотрите на неё после обновления: ненулевой поток означает, что какой-то отправитель всё ещё ходит по пути, который будет удалён. Когда все ряды здесь устойчиво лежат на нуле — к 1.0 можно готовиться спокойно.

Метрика временная. Она существует ровно ради того, чтобы эта проверка была механической, и будет удалена в 1.0 вместе с самими устаревшими путями. Не стройте на ней долгоживущих дашбордов и алертов: в отличие от остальных метрик этой страницы, её имя не входит в замороженный контракт наблюдаемости 1.0.

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

reasonЧто произошлоЧто делать
queue_fullобработка не успевает за приёмомбольше воркеров, быстрее PostgreSQL
queue_bytesисчерпан байтовый бюджет очереди — задачи крупнее обычногосмотреть GOTCHA_MAX_INGEST_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_SECONDS, означает, что пороги хостов не вычисляются; длительность, подбирающаяся к интервалу, — что оценщик перестаёт укладываться в свой период (обычно из-за медленного 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_SECONDS, означает, что burn rate не пересчитывается, а инциденты сжигания бюджета не открываются и не закрываются; длительность, подбирающаяся к интервалу, — что оценщик перестаёт укладываться в период.

gotcha_trace_evaluator_last_tick_timestamp_seconds / gotcha_trace_evaluator_tick_duration_seconds — момент последнего завершённого прохода оценщика регрессий производительности по всем проектам и его длительность. Та же слепая зона, что у оценщика хостов выше: тишина — нормальный вывод, поэтому умерший оценщик выглядит ровно как «регрессий сейчас нет». Период фиксирован — 5 минут, без настройки в конфиге; разница между текущим временем и меткой, заметно превышающая этот интервал, означает, что алерты по регрессиям перестали срабатывать, а длительность, подбирающаяся к интервалу, — что ClickHouse не успевает отвечать.

gotcha_metric_evaluator_last_tick_timestamp_seconds / gotcha_metric_evaluator_tick_duration_seconds — момент последнего завершённого прохода оценщика пороговых правил по метрикам по всем правилам и его длительность. Та же слепая зона. Разница между текущим временем и меткой, заметно превышающая GOTCHA_METRIC_EVAL_INTERVAL_SECONDS (по умолчанию 60с), означает, что правила по метрикам не вычисляются; длительность, подбирающаяся к интервалу, — что оценщик перестаёт укладываться в период.

gotcha_profile_evaluator_last_tick_timestamp_seconds / gotcha_profile_evaluator_tick_duration_seconds — момент последнего завершённого прохода оценщика регрессий профилей по всем сервисам и его длительность. Разница между текущим временем и меткой, заметно превышающая GOTCHA_PROFILE_EVAL_INTERVAL_SECONDS (по умолчанию 300с), означает, что алерты по регрессиям профилей не вычисляются; длительность, подбирающаяся к интервалу, — что ClickHouse не успевает отвечать.

gotcha_escalation_scheduler_last_tick_timestamp_seconds / gotcha_escalation_scheduler_tick_duration_seconds — момент последнего завершённого прохода централизованного планировщика эскалации по всем шести источникам инцидентов (регрессии производительности, правила по метрикам, регрессии профилей, пороги хостов, burn rate SLO, аптайм) и его длительность. Умерший планировщик снаружи выглядит ровно как «эскалировать нечего» — каждая лесенка просто перестаёт двигаться. Разница между текущим временем и меткой, заметно превышающая GOTCHA_ESCALATION_INTERVAL_SECONDS (по умолчанию 60с), означает, что ступени эскалации и напоминания не срабатывают ни для одного источника; длительность, подбирающаяся к интервалу, — что PostgreSQL не успевает. Проход, упёршийся в бюджет на середине, пропускает оставшиеся источники в этом проходе, а не блокирует следующий — причина в журнале строкой escalation scheduler: tick did not finish within its budget.

gotcha_uptime_scheduler_last_tick_timestamp_seconds / gotcha_uptime_scheduler_tick_duration_seconds — момент последней завершённой постановки созревших проверок аптайма в очередь и её длительность. Устаревшая метка означает, что мониторы показаны включёнными, но фактически ничего не проверяется — состояние замирает на последнем известном, а пропуски heartbeat и истечение сертификатов не считаются. Планировщик крутится в любом процессе с сервисом аптайма, не только в --mode=uptime; в --mode=web в журнал пишется предупреждение, что проверки ставятся в очередь, но не исполняются.

gotcha_uptime_runner_last_tick_timestamp_seconds / gotcha_uptime_runner_tick_duration_seconds — момент последнего завершённого прохода раннера аптайма (--mode=uptime/all), забирающего и исполняющего созревшие проверки, и его длительность. Устаревшая метка означает, что планировщик выше ставит проверки в очередь, но исполнять их некому.

gotcha_uptime_watchdog_last_tick_timestamp_seconds / gotcha_uptime_watchdog_tick_duration_seconds — момент последнего завершённого прохода сторожа heartbeat и напоминаний и его длительность. Устаревшая метка означает, что пропуски heartbeat-проверок и напоминания по инцидентам не оцениваются — heartbeat-монитор может молчать сколь угодно долго, ни разу не подняв инцидент. Интервал по умолчанию — 1 минута; длительность, подбирающаяся к нему, означает, что сторож перестаёт укладываться в период.

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

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

gotcha_host_registrations_scope_skipped_total — экспорты метрик с атрибутами host.* от ключа типа, которому регистрация хоста не разрешена (регистрировать хост может только ключ типа agent). Сам экспорт принимается, точки записываются — пропускается только регистрация хоста. Ненулевое значение на ключе server или browser ожидаемо и безвредно: такие SDK часто проставляют host.name резурс-детектором по умолчанию, — а вот растущий счётчик на ключе, который должен быть agent, указывает на неверно заданный тип ключа.

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

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

gotcha_export_queue_depth / gotcha_export_queue_oldest_seconds — глубина очереди выгрузок ошибок/событий (заявки в статусе queued или running) и возраст самой старой из них. Возраст важнее глубины — он единственный отличает «очередь пуста, потому что все заявки досчитаны» от «очередь стоит, потому что воркер не поднят или упирается в диск». Метрики снимаются только там, где очередь вообще обслуживается: в --mode=ingest воркера выгрузок нет (файл там отдавать некому), поэтому там же нет и этих метрик — их отсутствие в этом режиме ожидаемо, а не сбой.

gotcha_export_queue_failed — заявок на выгрузку, исчерпавших все попытки и добитых в failed. Ненулевое и растущее значение — тот самый сценарий закрытого P0 (массовые отказы заявок были видны только по slog.Warn на тик воркера, дежурный узнавал о них лишь если специально смотрел лог): каждая заявка падает на одной и той же причине — обычно на правах каталога выгрузок или исчерпанном GOTCHA_EXPORT_DISK_BUDGET_BYTES (см. последнюю попытку в интерфейсе, разделе «Выгрузки»).

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

gotcha_entities_purged_total — строки, удалённые из PostgreSQL по истечении GOTCHA_EVENT_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_storage_used_bytes{store="exports"} — сколько байт занимают файлы каталога выгрузок ошибок/событий (GOTCHA_EXPORT_DIR) прямо сейчас. В отличие от store="postgres", это не абстрактный размер БД, а тот же каталог, чей бюджет проверяет воркер выгрузок перед каждой заявкой (GOTCHA_EXPORT_DISK_BUDGET_BYTES) — до этой метрики единственный кусок диска, которым распоряжается само приложение, был совершенно неизмеряем снаружи. Опрашивается раз в 5 минут, как и соседние gotcha_storage_*; регистрируется только там, где очередь выгрузок вообще обслуживается (см. gotcha_export_queue_depth выше).

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

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

gotcha_uptime_heartbeat_ignored_total{reason="…"} — пинги на /uptime/hb/{token}, полученные, но НЕ засчитанные как признак жизни монитора. Ссылка на пинг — простой URL, который регулярно дёргают не люди: prefetch_header — сам запрос несёт протокольный заголовок (Sec-Purpose, Purpose, X-Purpose, X-Moz), которым клиент явно помечает себя предварительной выборкой; bot_user_agent — User-Agent совпадает с известным ботом построения превью ссылок в мессенджере/соцсети. Ответ на такой пинг — всё равно 204 (чтобы бот не ретраил), но last_seen монитора не двигается. Растущий счётчик — не сбой, это ожидаемый шум там, где ссылка на пинг разослана в канал, который её разворачивает (Slack, Telegram).

gotcha_i18n_missing_key_total{locale="…",stage="…"} — промах поиска ключа перевода. Рендер никогда не падает на промахе — см. Конфигурацию (раздел про добавление локали) — но и не проходит незамеченным: stage="fallback" означает, что ключа нет в запрошенной локали, но нашёлся в локали по умолчанию (страница молча показывает чужой язык); stage="missing" — ключа нет нигде, и на странице виден сырой идентификатор ключа. Ненулевой и растущий missing почти всегда означает недавно добавленный в код ключ перевода без пары в JSON-каталоге локали; fallback на локали, отличной от английской, — обычно перевод существующего ключа, который ещё не перенесли в третью локаль.

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

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

# Хранилище не успевает — потери впереди.
gotcha_writer_buffered_rows > 5000
gotcha_pipeline_queue_depth / 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_EVENT_RETENTION_DAYS; устойчивый ноль означает, что чистка PostgreSQL не работает, хотя должна (см. выше). Для ClickHouse TTL применяется автоматически, но у каждого вида данных свой срок хранения — см. Конфигурацию.
  3. Определите, что растёт быстрее всего. По умолчанию самые тяжёлые по объёму — профили (GOTCHA_PROFILE_RETENTION_DAYS, поэтому его дефолт и короче остальных — 7 дней). Если вы отправляете continuous profiling, но не смотрите флеймографы регулярно, это первый кандидат на сокращение срока хранения или остановку на стороне SDK.
  4. Освободите место сейчас, а не только через TTL. Удаление проекта («Настройки проекта» → «Опасная зона» → «Удалить проект») стирает его телеметрию из ClickHouse сразу, а не постепенно — годится для тестовых или заброшенных проектов, накопивших данные впустую.
  5. Если места мало систематически — уменьшите срок хранения. Сроки задаются отдельно на каждый вид данных (GOTCHA_EVENT_RETENTION_DAYS, GOTCHA_SPAN_RETENTION_DAYS, GOTCHA_METRIC_RETENTION_DAYS, GOTCHA_PROFILE_RETENTION_DAYS — см. Конфигурацию). Изменение действует со следующего старта и не восстанавливает уже удалённое задним числом; и место при этом освобождается не мгновенно — ClickHouse удаляет просроченные данные фоновыми слияниями по обычному расписанию, а не в момент правки конфигурации.

Что дальше