Мне нужна была observability для собственных сервисов, и хотелось держать её у себя: логи ошибок — часто самые чувствительные данные в системе, и отдавать их наружу не хотелось. Официальный self-hosted-вариант знакомого стека — серьёзная инсталляция: приёмник, брокер, воркеры, кэш, отдельное аналитическое хранилище — в референсном docker-compose набегает под два десятка контейнеров. Для одного человека и нескольких проектов это несоразмерно: такое надо не только поднять, но и обновлять, чинить и держать в памяти.

Тогда я задал себе вопрос: сколько из этого действительно необходимо, если цель — self-hosted-инстанс на небольшой и средний объём? Ответ оказался неожиданным — почти ничего. Так появилась Gotcha: один Go-бинарник поверх двух баз, который принимает ошибки, трейсы, метрики, профили и аптайм.

Дальше — как это устроено и почему именно так.

Из чего состоит система

Вся инсталляция — три контейнера в штатном docker-compose.yml: приложение gotcha, postgres и clickhouse. Два хранилища делят ответственность по природе данных:

Хранилище

Что держит

PostgreSQL

Реляционное состояние: организации, проекты, пользователи, правила оповещений, инциденты

ClickHouse

Высокочастотная телеметрия: события (ошибки), спаны трейсов, метрики, профили, результаты аптайм-проверок

Разделение естественное: метаданные — это транзакционная реляционка, а поток телеметрии — колоночная аналитика с большими объёмами вставки и retention по TTL. Каждой задаче — своя база, без попытки натянуть одно на другое.

Почему без Kafka и Redis

Это главный архитектурный вопрос, поэтому отвечу подробно.

Брокер (Kafka) обычно ставят между приёмом и хранилищем — чтобы сгладить всплески и развязать компоненты. Но ClickHouse сам рассчитан на высокую пропускную способность вставки, если писать батчами. Поэтому режим приёма (--mode=ingest) батчит события, спаны, метрики и профили и пишет прямо в ClickHouse. Для self-hosted-объёмов отдельному брокеру между этими двумя шагами просто нечего делать — он решал бы проблему, которой на этих нагрузках нет.

Кэш (Redis) обычно держит сессии и промежуточные вычисления. Здесь состояние сессий и вычисление оповещений живут в PostgreSQL и в самом процессе — внешний кэш не нужен.

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

Один бинарник, флаг --mode

Тот же бинарник gotcha флагом --mode включает часть функций. Это даёт две вещи сразу: запустить всё в одном процессе на старте — и разнести по процессам, когда вырастет нагрузка.

Режим

Что делает

--mode=ingest

HTTP-приём: эндпоинты Sentry envelope и OTLP-метрики, батчинг событий/спанов/метрик/профилей, вычисление оповещений

--mode=web

SSR-интерфейс (templ + htmx): аутентификация, администрирование, дашборды, публичные статус-страницы

--mode=uptime

Раннер аптайм-проверок, watchdog инцидентов, детекторы регрессий производительности и порогов по метрикам

--mode=probe

Выносная проба: общается только с центральным инстансом по HTTP, без прямого доступа к базам

--mode=all

Всё перечисленное в одном процессе — режим по умолчанию для небольшой установки

Никаких микросервисов на старте: --mode=all, три контейнера, поехали.

Как это масштабируется

Пока нагрузка небольшая — --mode=all держит всё в одном процессе. Когда приём начинает конкурировать с веб-интерфейсом за ресурсы, те же функции разносятся по отдельным процессам: несколько ingest за балансировщиком, отдельный web, отдельный uptime. А --mode=probe разворачивается в другом регионе и проверяет доступность ваших сервисов «снаружи» — проба не открывает ни PostgreSQL, ни ClickHouse, только ходит к центральному инстансу по HTTP.

Ключевая мысль: масштабирование — это не смена архитектуры, а запуск того же бинарника с другим --mode. Не нужно переезжать на другой стек, когда вырастешь, — просто раскладываешь по процессам то, что уже есть.

Приём данных: совместимость с существующими SDK

Чтобы не заставлять никого переписывать инструментацию, приёмник понимает протокол Sentry envelope и OTLP для метрик. На практике это значит, что перенос сводится к смене DSN: берёте свой уже подключённый официальный Sentry SDK и указываете его на свой инстанс.

# было
sentry_sdk.init(dsn='https://<key>@sentry.io/<project>')

# стало — тот же SDK, свой DSN
sentry_sdk.init(dsn='https://<key>@gotcha.your-infra.ru/<project>')

Код приложения не меняется — меняется одна строка конфигурации. Метрики можно слать по OTLP.

Вот как это выглядит после смены DSN — события приходят и группируются в проблемы, с трендом за 24 часа, статусами и ответственными:

Список проблем в Gotcha: сгруппированные ошибки с уровнями, трендом и статусами
Список проблем в Gotcha: сгруппированные ошибки с уровнями, трендом и статусами

Retention и приватность — что заложено в архитектуру

ClickHouse хранит каждый тип телеметрии заданное число дней и удаляет старое по TTL. Дефолты подобраны по «весу» данных: события — 90 дней, спаны и метрики — 30, профили — 7 (самые тяжёлые). Каждое значение настраивается переменной окружения.

Раз всё self-hosted, событийные данные не покидают вашу инфраструктуру. Плюс несколько защит встроены на уровне архитектуры:

  • SSRF-защита исходящих запросов (аптайм-проверки, webhook-алерты): по умолчанию они не ходят на приватные/loopback-адреса, чтобы проверку нельзя было превратить в сканер внутренней сети.

  • Контроль текста ошибок во внешних каналах: в Telegram/webhook можно слать только обезличенную ссылку, без тела ошибки (в нём бывают персональные данные).

  • Подпись сессионных cookie отдельным секретом; на не-localhost адресе приложение отказывается стартовать в web/all без своего ключа.

Где проект сейчас

Проект открытый, лицензия Apache-2.0, текущая версия — v0.2.1. Работают: приём ошибок и трейсов, метрики по OTLP, профилирование, аптайм и статус-страницы, оповещения. Это активная фаза — что-то ещё шершавое, и я честно это отмечаю в репозитории.

Если вам близка идея «одного скучного бинарника» вместо распределённого стека — буду рад ранним пользователям и любому фидбэку: что не хватает, что неудобно, что сломалось. Ставится это так:

git clone https://github.com/OtezVikentiy/gotcha
cd gotcha
docker compose up -d

Исходники: GitHub. Документация и гайд по переносу — на getgotcha.ru. Вопросы и замечания — в issues или в комментариях, отвечу.

Комментарии (9)


  1. otezvikentiy Автор
    24.07.2026 10:10

    Автор здесь, буду в комментариях ближайшие часы — с радостью отвечу на вопросы по архитектуре. Особенно на такие: почему именно PostgreSQL + ClickHouse, а не одна база; где проходит граница, за которой --mode=all перестаёт справляться и пора разносить процессы; что оказалось самым неприятным при реализации приёма Sentry-протокола.

    И встречный вопрос, ради которого во многом и написано: если вы держите self-hosted-observability — какой у вас объём событий в сутки и на чём оно крутится? Мне сейчас не хватает именно этого фидбэка, чтобы понимать, на какие нагрузки целиться дальше.


  1. otezvikentiy Автор
    24.07.2026 10:10

    Справедливо было бы спросить "а где цифры" - статья без них действительно неполная. Снял с рабочего стенда, привожу как есть, с оговорками.

    Потребление в покое. Инстанс поднят 20 часов, --mode=all, три контейнера:

    - сам бинарник gotcha: 6.3 МБ RAM, ~0% CPU

    - PostgreSQL: 25 МБ, 3.7% CPU

    - ClickHouse: 731 МБ, 3.5% CPU

    Итого около 760 МБ. Львиную долю занимает ClickHouse - это его обычный базовый аппетит, и под нагрузкой он будет расти. Сам Go-бинарник в покое держится в пределах 6-7 МБ.

    Сколько занимают данные. За 14 дней на стенде накопилось (строк / на диске / сжатие):

    - спаны трейсов: 108 280 / 3.96 МиБ / 3.7x

    - транзакции: 22 620 / 2.22 МиБ / 1.9x

    - события (ошибки): 1 578 / 69 КиБ / 14x

    - точки метрик: 3 840 / 44 КиБ / 10.6x

    - сэмплы профилей: 1 620 / 29 КиБ / 6.9x

    В пересчёте на запись: спан - около 38 байт на диске, точка метрики - около 12, событие - около 45. PostgreSQL со всей реляционкой (организации, проекты, пользователи, правила алертов, инциденты) - 10 МБ.

    Важная оговорка. Стенд не продакшен: событий всего полторы тысячи, и сжатие 14x на таком маленьком однородном наборе почти наверняка оптимистичное. Цифре по спанам (108 тысяч строк) я доверяю заметно больше. Считайте это порядком величины, а не обещанием.

    Чего я не мерил - пропускную способность. Нагрузочного теста не делал, поэтому называть события в секунду не буду. Если интересно - соберу и опубликую отдельно, уже с методикой.

    Задокументированные требования, из которых я исхожу: минимум 2 vCPU / 2 ГБ RAM / 20 ГБ SSD, рекомендуемо 4 vCPU / 4 ГБ / 40 ГБ. Диск обязательно SSD — обе базы чувствительны к латентности.


  1. azk
    24.07.2026 10:10

    https://victoriametrics.com/products/

    Почему не подошёл данный стек? Если брать не кластерную версию, как раз 3 бинарника + grafana и модули к ней


    1. otezvikentiy Автор
      24.07.2026 10:10

      Согласен, в некластерном виде он действительно компактный, по ресурсам тут спорить не с чем.

      Разница для меня была не в весе, а в задаче.

      Насколько я понимаю их продукты, это прежде всего хранилища телеметрии плюс Grafana как слой визуализации. Мне же нужен был в первую очередь воркфлоу по ошибкам: исключения, сгруппированные в проблемы по сигнатуре, со стектрейсом и дедупликацией, со статусами (unresolved / ignored / resolved), назначением ответственного и историей по каждой проблеме. Это ближе к трекеру задач над потоком исключений, чем к дашборду, и на графане такое собирается плохо.

      Второе, и для меня решающее: приём по протоколу Sentry. В проектах уже стояли официальные Sentry SDK, и условие было - не трогать инструментацию вообще. Перенос свёлся к смене DSN в конфиге. Ради этого пункта всё, собственно, и затевалось.

      Третье - свой интерфейс из коробки, без отдельного слоя визуализации, который нужно конфигурировать. Это осознанный размен: меньше гибкости в обмен на отсутствие настройки.

      Если задача - метрики и логи, ваш вариант её закрывает, и городить своё смысла нет. У меня отправной точкой были ошибки с уже подключённых Sentry SDK - отсюда и другой выбор.


      1. denaspireone
        24.07.2026 10:10

        Очень похоже на то, что вы из стека Sentry community self hosted сделали его же версию но на минималках. Как вариант для себя вполне пойдет, как вариант не для себя - нет.

        свой интерфейс из коробки, без отдельного слоя визуализации, который нужно конфигурировать

        Это вы просто будете пепеписывать несколько раз для себя, если не считать как конфигурирование, то да, возможно так и есть. Вам стоило взглянуть на perses.dev, он более гибок и можно так же навайбкодить свой плагин/панель под ваши нужды.


        1. otezvikentiy Автор
          24.07.2026 10:10

          Тут важно поправить фактическую часть: Gotcha - не "версия Sentry на минималках" и вообще не их стек. В кодовой базе нет ни строчки из Sentry: их сервер написан на Python (+TypeScript в интерфейсе), приём - Relay на Rust, слой над ClickHouse - Snuba на Python. Gotcha целиком написана на Go, с нуля. Общее между нами - только wire-протокол SDK, и он реализован заново именно для того, чтобы не трогать инструментацию в приложениях (в статье честно написано, что это и было самым неприятным куском работы).

          Разница и в целевом классе. Официальная документация self-hosted Sentry называет минимум 4 CPU и 16 ГБ RAM (+16 своп) - для их масштаба и функциональности это оправданная архитектура. Gotcha документирует минимум 2 vCPU / 2 ГБ и реально живёт на таком VPS (замеры - в комментарии выше). Это не "то же самое, ужатое", а другой инженерный компромисс под другой класс задач: маленькие и средние инсталляции, где выделять отдельную машину под мониторинг не хочется.

          Про "переписывать интерфейс несколько раз" - риск честный, я его понимаю. Пока панели узкие и их немного (issues, трейсы, метрики, профили, аптайм), и они привязаны к нашим же данным, так что конструктор дашбордов строить не планирую.

          За наводку на Perses спасибо, посмотрел: это CNCF-проект дашбордов поверх Prometheus/Tempo/Loki/Pyroscope - слой визуализации над чужими бэкендами. Gotcha же в первую очередь сам бэкенд: приём по Sentry-протоколу и OTLP, хранение, алерты; UI - тонкий слой над этим. Так что напрямую он наш интерфейс не заменит, но embeddable-панели у них любопытные - для метрик-дашбордов идея интересная.


  1. devoln
    24.07.2026 10:10

    А зачем Postgres, разве SQLite недостаточно? По описанию кажется, там записей очень мало, справится с запасом. Минус один контейнер, но по памяти конечно небольшая экономия рядом с ClickHouse.


    1. otezvikentiy Автор
      24.07.2026 10:10

      По объёмам - вы правы, и с запасом: на стенде весь PostgreSQL со всей реляционкой (организации, проекты, пользователи, правила алертов, инциденты) занимает 10 МБ данных и ~25 МБ памяти в покое. SQLite бы это унёс не заметив.

      Причина не в объёмах, а в режимной модели. Тот же бинарник флагом --mode разносится на отдельные процессы - ingestwebuptime - и вся их координация сознательно живёт в базах: сессии, правила, инциденты, вычисление алертов - в PostgreSQL. Процессы могут стоять на разных машинах, и тогда им нужна одна сетевая база с нормальной конкурентной записью: ingest пишет инциденты, web правит правила, uptime обновляет статусы - одновременно. SQLite - встраиваемый однофайловый движок: по сети он не работает, а с несколькими пишущими процессами живёт плохо даже локально. Выбросить его пришлось бы ровно в тот момент, когда single-node перестаёт хватать - то есть в самый неудобный.

      Но для строго одноузлового режима мысль здравая, признаю: embedded-вариант без контейнера Postgres сделал бы --mode=all ещё легче. В бэклоге такое есть, обещать сроки не буду. А экономия по памяти — да, вы сами это отметили: рядом с ClickHouse (~730 МБ в покое) эти 25 МБ погоды не делают.


  1. jingvar
    24.07.2026 10:10

    Есть описание задачи ? Во что уперлись и как не подошли известные опенсорц решения и вы такие решили во все тяжкие?