Observability в одном Go-бинарнике: как я собрал self-hosted без Kafka и Redis
- суббота, 25 июля 2026 г. в 00:00:08
Мне нужна была observability для собственных сервисов, и хотелось держать её у себя: логи ошибок — часто самые чувствительные данные в системе, и отдавать их наружу не хотелось. Официальный self-hosted-вариант знакомого стека — серьёзная инсталляция: приёмник, брокер, воркеры, кэш, отдельное аналитическое хранилище — в референсном docker-compose набегает под два десятка контейнеров. Для одного человека и нескольких проектов это несоразмерно: такое надо не только поднять, но и обновлять, чинить и держать в памяти.
Тогда я задал себе вопрос: сколько из этого действительно необходимо, если цель — self-hosted-инстанс на небольшой и средний объём? Ответ оказался неожиданным — почти ничего. Так появилась Gotcha: один Go-бинарник поверх двух баз, который принимает ошибки, трейсы, метрики, профили и аптайм.
Дальше — как это устроено и почему именно так.
Вся инсталляция — три контейнера в штатном docker-compose.yml: приложение gotcha, postgres и clickhouse. Два хранилища делят ответственность по природе данных:
Хранилище | Что держит |
|---|---|
PostgreSQL | Реляционное состояние: организации, проекты, пользователи, правила оповещений, инциденты |
ClickHouse | Высокочастотная телеметрия: события (ошибки), спаны трейсов, метрики, профили, результаты аптайм-проверок |
Разделение естественное: метаданные — это транзакционная реляционка, а поток телеметрии — колоночная аналитика с большими объёмами вставки и retention по TTL. Каждой задаче — своя база, без попытки натянуть одно на другое.
Это главный архитектурный вопрос, поэтому отвечу подробно.
Брокер (Kafka) обычно ставят между приёмом и хранилищем — чтобы сгладить всплески и развязать компоненты. Но ClickHouse сам рассчитан на высокую пропускную способность вставки, если писать батчами. Поэтому режим приёма (--mode=ingest) батчит события, спаны, метрики и профили и пишет прямо в ClickHouse. Для self-hosted-объёмов отдельному брокеру между этими двумя шагами просто нечего делать — он решал бы проблему, которой на этих нагрузках нет.
Кэш (Redis) обычно держит сессии и промежуточные вычисления. Здесь состояние сессий и вычисление оповещений живут в PostgreSQL и в самом процессе — внешний кэш не нужен.
Практический итог — меньше движущихся частей: меньше памяти, меньше того, что может сломаться в три часа ночи, и заметно проще эксплуатация. Это осознанный размен: я отказался от компонентов, которые окупаются на большой распределённой нагрузке, ради простоты на нагрузке self-hosted.
Тот же бинарник gotcha флагом --mode включает часть функций. Это даёт две вещи сразу: запустить всё в одном процессе на старте — и разнести по процессам, когда вырастет нагрузка.
Режим | Что делает |
|---|---|
| HTTP-приём: эндпоинты Sentry envelope и OTLP-метрики, батчинг событий/спанов/метрик/профилей, вычисление оповещений |
| SSR-интерфейс (templ + htmx): аутентификация, администрирование, дашборды, публичные статус-страницы |
| Раннер аптайм-проверок, watchdog инцидентов, детекторы регрессий производительности и порогов по метрикам |
| Выносная проба: общается только с центральным инстансом по HTTP, без прямого доступа к базам |
| Всё перечисленное в одном процессе — режим по умолчанию для небольшой установки |
Никаких микросервисов на старте: --mode=all, три контейнера, поехали.
Пока нагрузка небольшая — --mode=all держит всё в одном процессе. Когда приём начинает конкурировать с веб-интерфейсом за ресурсы, те же функции разносятся по отдельным процессам: несколько ingest за балансировщиком, отдельный web, отдельный uptime. А --mode=probe разворачивается в другом регионе и проверяет доступность ваших сервисов «снаружи» — проба не открывает ни PostgreSQL, ни ClickHouse, только ходит к центральному инстансу по HTTP.
Ключевая мысль: масштабирование — это не смена архитектуры, а запуск того же бинарника с другим --mode. Не нужно переезжать на другой стек, когда вырастешь, — просто раскладываешь по процессам то, что уже есть.
Чтобы не заставлять никого переписывать инструментацию, приёмник понимает протокол 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 часа, статусами и ответственными:

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 или в комментариях, отвечу.