Мне нужна была 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 включает часть функций. Это даёт две вещи сразу: запустить всё в одном процессе на старте — и разнести по процессам, когда вырастет нагрузка.
Режим |
Что делает |
|---|---|
|
HTTP-приём: эндпоинты Sentry envelope и OTLP-метрики, батчинг событий/спанов/метрик/профилей, вычисление оповещений |
|
SSR-интерфейс (templ + htmx): аутентификация, администрирование, дашборды, публичные статус-страницы |
|
Раннер аптайм-проверок, watchdog инцидентов, детекторы регрессий производительности и порогов по метрикам |
|
Выносная проба: общается только с центральным инстансом по HTTP, без прямого доступа к базам |
|
Всё перечисленное в одном процессе — режим по умолчанию для небольшой установки |
Никаких микросервисов на старте: --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 часа, статусами и ответственными:

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)

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 — обе базы чувствительны к латентности.

azk
24.07.2026 10:10https://victoriametrics.com/products/
Почему не подошёл данный стек? Если брать не кластерную версию, как раз 3 бинарника + grafana и модули к ней

otezvikentiy Автор
24.07.2026 10:10Согласен, в некластерном виде он действительно компактный, по ресурсам тут спорить не с чем.
Разница для меня была не в весе, а в задаче.
Насколько я понимаю их продукты, это прежде всего хранилища телеметрии плюс Grafana как слой визуализации. Мне же нужен был в первую очередь воркфлоу по ошибкам: исключения, сгруппированные в проблемы по сигнатуре, со стектрейсом и дедупликацией, со статусами (unresolved / ignored / resolved), назначением ответственного и историей по каждой проблеме. Это ближе к трекеру задач над потоком исключений, чем к дашборду, и на графане такое собирается плохо.
Второе, и для меня решающее: приём по протоколу Sentry. В проектах уже стояли официальные Sentry SDK, и условие было - не трогать инструментацию вообще. Перенос свёлся к смене DSN в конфиге. Ради этого пункта всё, собственно, и затевалось.
Третье - свой интерфейс из коробки, без отдельного слоя визуализации, который нужно конфигурировать. Это осознанный размен: меньше гибкости в обмен на отсутствие настройки.
Если задача - метрики и логи, ваш вариант её закрывает, и городить своё смысла нет. У меня отправной точкой были ошибки с уже подключённых Sentry SDK - отсюда и другой выбор.

denaspireone
24.07.2026 10:10Очень похоже на то, что вы из стека Sentry community self hosted сделали его же версию но на минималках. Как вариант для себя вполне пойдет, как вариант не для себя - нет.
свой интерфейс из коробки, без отдельного слоя визуализации, который нужно конфигурировать
Это вы просто будете пепеписывать несколько раз для себя, если не считать как конфигурирование, то да, возможно так и есть. Вам стоило взглянуть на perses.dev, он более гибок и можно так же навайбкодить свой плагин/панель под ваши нужды.

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-панели у них любопытные - для метрик-дашбордов идея интересная.

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

otezvikentiy Автор
24.07.2026 10:10По объёмам - вы правы, и с запасом: на стенде весь PostgreSQL со всей реляционкой (организации, проекты, пользователи, правила алертов, инциденты) занимает 10 МБ данных и ~25 МБ памяти в покое. SQLite бы это унёс не заметив.
Причина не в объёмах, а в режимной модели. Тот же бинарник флагом
--modeразносится на отдельные процессы -ingest,web,uptime- и вся их координация сознательно живёт в базах: сессии, правила, инциденты, вычисление алертов - в PostgreSQL. Процессы могут стоять на разных машинах, и тогда им нужна одна сетевая база с нормальной конкурентной записью: ingest пишет инциденты, web правит правила, uptime обновляет статусы - одновременно. SQLite - встраиваемый однофайловый движок: по сети он не работает, а с несколькими пишущими процессами живёт плохо даже локально. Выбросить его пришлось бы ровно в тот момент, когда single-node перестаёт хватать - то есть в самый неудобный.Но для строго одноузлового режима мысль здравая, признаю: embedded-вариант без контейнера Postgres сделал бы
--mode=allещё легче. В бэклоге такое есть, обещать сроки не буду. А экономия по памяти — да, вы сами это отметили: рядом с ClickHouse (~730 МБ в покое) эти 25 МБ погоды не делают.

jingvar
24.07.2026 10:10Есть описание задачи ? Во что уперлись и как не подошли известные опенсорц решения и вы такие решили во все тяжкие?
otezvikentiy Автор
Автор здесь, буду в комментариях ближайшие часы — с радостью отвечу на вопросы по архитектуре. Особенно на такие: почему именно PostgreSQL + ClickHouse, а не одна база; где проходит граница, за которой --mode=all перестаёт справляться и пора разносить процессы; что оказалось самым неприятным при реализации приёма Sentry-протокола.
И встречный вопрос, ради которого во многом и написано: если вы держите self-hosted-observability — какой у вас объём событий в сутки и на чём оно крутится? Мне сейчас не хватает именно этого фидбэка, чтобы понимать, на какие нагрузки целиться дальше.