OpenTelemetry стек в Go: метрики, трейсы и логи в одном месте
В этой статье разберём практический пример использования стека OpenTelemetry (OTel) в Go-проектах. Цель — показать минимальный, но рабочий шаблон, который можно взять за основу и дорастить под свои задачи.
Мы будем собирать три вида телеметрии:
- Tracing → Tempo
- Metrics → Prometheus
- Logs → Loki
В качестве единой точки приёма данных используем otel-collector, а для визуализации — Grafana. Теории будет минимум: основной упор на то, как всё связать и увидеть результат.
Состав стека
Поднимаем следующие сервисы в Docker:
- `app-1` — клиентское приложение (условный потребитель API).
- `app-2` — сервер (обрабатывает запросы от `app-1`).
- `otel-collector` — центральный узел, куда прилетают все трейсы, метрики и логи.
- `prometheus` — хранение и запросы к метрикам.
- `tempo` — хранилище распределённых трасс.
- `loki` — система сбора и хранения логов.
- `grafana` — единый интерфейс для просмотра всего этого добра.
Логика такова:
`app (traces/metrics/logs exporter) → otel-collector → prometheus / loki / tempo (хранят данные) → grafana (отдаёт графики, таблицы, трейс-вьюер)`.
Все сервисы можно описать в одном `docker-compose.yml`: сети, тома для хранения, порты, переменные окружения. Главное — чтобы otel-collector был доступен из приложений по сети и принимал OTLP-трафик (gRPC или HTTP).
Конфигурация otel-collector: общая идея
Collector выполняет три ключевые задачи:
1. Принимает данные — `receivers` (OTLP для метрик, трейсов и логов).
2. Обрабатывает данные — `processors` (batching, добавление тегов, фильтрация и т.п.).
3. Отправляет дальше — `exporters` (в Prometheus, Tempo, Loki и т.д.).
Типичная схема:
- один OTLP receiver для всех типов сигналов;
- batch-процессор, чтобы не слать каждый спан/метрику/лог по отдельности;
- экспортеры:
- в Tempo — для трейсов,
- в Prometheus — для метрик (через режим `prometheus` / `prometheusremotewrite`),
- в Loki — для логов.
В результате приложения ничего не знают о Tempo или Loki напрямую — они общаются только с otel-collector.
Архитектура Go-приложений
Будем работать с двумя Go-сервисами:
- `app-1` — делает HTTP-запросы на `app-2`, измеряет время ответа, считает количество успешных и неуспешных запросов, логирует события.
- `app-2` — принимает запросы, выполняет простую бизнес-логику, ходит в условную "базу данных" (репозиторий) и также пишет трейсы, метрики и логи.
Код `app-2` логически разделён на слои:
- Server (adapter) — HTTP-слой, приём запросов, привязка к фреймворку.
- Service — бизнес-логика, правила обработки, валидация и т.п.
- Repository — слой работы с БД или внешними ресурсами.
Такое деление удобно, потому что можно добавить трейсинг и метрики на каждом уровне:
от HTTP-эндпоинта до конкретного запроса в БД.
Инициализация OTel в Go: провайдеры
В каждом приложении сначала настраиваем SDK и провайдеры:
1. TracerProvider — для создания спанов.
2. MeterProvider — для метрик (счётчики, гистограммы и т.д.).
3. Logger / Log Handler — для интеграции логгера с OTel.
Провайдеры настраиваются на отправку данных в otel-collector по протоколу OTLP. Обычно это:
- адрес вида `otel-collector:4317` (gRPC) или `otel-collector:4318` (HTTP);
- ресурсные атрибуты: имя сервиса, версия, окружение (`service.name`, `service.version`, `deployment.environment` и пр.);
- установка глобальных провайдеров через OTel API, чтобы их можно было использовать по всему приложению.
После инициализации можно в любом месте кода получать:
- `tracer := otel.Tracer("название-пакета")`;
- `meter := global.Meter("название-пакета")`;
- логгер с привязкой к OTel-контексту.
Трейсинг: связность контекста и Tempo
Ключевой момент распределённого трейсинга — контекст. Чтобы цепочка спанов не рвалась, нужно:
- распространять `context.Context` по всем вызовам: от HTTP-обработчика до репозитория;
- использовать propagator `TraceContext`, чтобы заголовки с trace-id попадали в исходящие HTTP-запросы.
Схема работы:
1. `app-1` получает запрос или стартует процесс и создаёт корневой спан.
2. При запросе к `app-2` пропагатор добавляет заголовки с trace-id/span-id.
3. `app-2` считывает эти заголовки и продолжает тот же трейс, создавая дочерние спаны.
4. Внутри `app-2` можно создавать спаны на уровне Service и Repository, чтобы видеть весь путь запроса.
В Tempo это отобразится как одна цепочка:
запрос в `app-1` → вызов в `app-2` → запрос в БД и т.д.
Логи, связанные с трейсом
Чтобы в трейсах были видны логи, логгер должен:
- получать `context.Context` при логировании;
- доставать из контекста `trace_id` и `span_id`;
- добавлять эти значения как поля лога.
Тогда в Grafana/Tempo:
- при просмотре конкретного трейса можно нажать на иконку логов;
- увидеть логи, связанные с этим трейсом и принадлежащие конкретным спанам.
Важно, чтобы и трейс, и лог использовали один и тот же контекст — иначе связка потеряется.
Метрики: какие и для чего
В примере используются:
- Counter — для подсчёта числа запросов:
- успешных;
- завершившихся ошибкой.
- Histogram — для измерения latency (времени обработки запросов).
Почему не Gauge
В демонстрации мы измеряем именно события (запрос выполнен, запрос завершился с ошибкой, время выполнения запроса), а не состояние системы в текущий момент.
Gauge лучше подходит для:
- текущего числа активных соединений;
- загрузки памяти;
- количества элементов в очереди.
То есть для вопроса: «Сколько сейчас?»
В нашем же случае интереснее: «Сколько было?» и «Как распределено время выполнения?».
Почему Histogram, а не Summary
`Histogram`:
- собирает данные по бакетам;
- отлично агрегируется Prometheus-ом между несколькими инстансами;
- позволяет считать p95/p99 across instances.
`Summary`:
- считается локально на каждом экземпляре;
- плохо агрегируется между инстансами;
- удобен, когда важна статистика по конкретному инстансу, а не по целому кластеру.
В продe чаще всего нужна общая картина по сервису в целом, а не по одному контейнеру, поэтому для latency запросов выбор — `Histogram`.
Краткое напоминание по типам метрик
- Counter
Монотонный счётчик, который только растёт и сбрасывается при рестарте процесса.
Примеры: количество запросов, ошибок, завершённых задач.
Вопрос: «Сколько произошло за период?».
- Gauge
Значение, которое может расти и падать.
Примеры: используемая память, число подключений.
Вопрос: «Сколько сейчас?» или «Сколько было в конкретный момент?».
- Histogram
Распределение значений по диапазонам (бакетам), хорошо агрегируется между инстансами.
Примеры: время ответа, размер ответа, объём трафика.
Вопрос: «Как распределены значения? Какой p95/p99?».
- Summary
Похожа на Histogram, но рассчитывается локально на экземпляре.
Примеры: задержки на конкретном инстансе.
Вопрос: «Как распределены значения на этом конкретном инстансе?».
Почему нельзя использовать UUID в лейблах метрик
Иногда хочется добавить в метрику что-то уникальное — например, `request_id` или UUID пользователя как label. Делать этого нельзя. Причина — высокая кардинальность:
- огромное число уникальных значений лейбла создаёт миллионы временных рядов в Prometheus;
- хранилище раздувается, запросы становятся тяжёлыми, графики ломаются;
- система мониторинга начинает работать нестабильно.
Лучшее место для UUID:
- в трейсах (trace_id / span_id);
- в логах (как поле записи).
Метрики же должны иметь ограниченное число значений лейблов: статус, тип операции, регион, сервис и т.п.
Просмотр результатов в Grafana
После запуска всего стека можно зайти в Grafana и настроить источники данных:
- Prometheus — для метрик;
- Tempo — для трейсов;
- Loki — для логов.
Датасорсы можно добавить и через конфигурационные файлы, и через UI — оба способа рабочие.
Далее:
1. В разделе Explore / Traces можно находить трейсы по `service.name`, операции или атрибутам.
2. Открыв трейс, видно структуру спанов, тайминг и дерево вызовов.
3. Через иконку логов отображаются все записи из Loki, связанные с этим трейсом.
4. В дашбордах по Prometheus можно:
- построить графики количества успешных/ошибочных запросов (counters);
- посмотреть p95/p99 latency на основе histogram.
Так получается единая картина: метрики показывают общую динамику, трейсы — конкретные медленные запросы, а логи — детали, что именно пошло не так.
Что в примере упрощено и почему
Пример предельно минималистичный, намеренно:
- код не претендует на идеальную архитектуру;
- нет сложной конфигурации ретраев, очередей, балансировки;
- шифрование, авторизация, продакшн-настройки Prometheus/Tempo/Loki не рассматриваются.
Задача — дать шаблон, на базе которого можно:
- расширять бизнес-логику;
- добавлять новые метрики и спаны;
- строить дашборды и алерты;
- переносить подход в реальный продакшн.
Практические рекомендации по развитию этого шаблона
Если вы хотите использовать подобный стек в боевом сервисе, имеет смысл:
1. Разделить окружения
Отдельные инстансы или хотя бы отдельные теги для dev, staging и prod, чтобы не смешивать данные.
2. Добавить алертинг
На базе Prometheus:
- алерты по ошибкам (ratio 5xx к 2xx);
- алерты по p95/p99 latency;
- алерты по количеству неожиданных паник.
3. Расширить трейсинг
Проставить спаны:
- вокруг внешних HTTP-запросов;
- вокруг вызовов БД;
- вокруг кэш-операций.
4. Нормализовать логи
Структурированные логи (JSON), единый формат уровней, поля с контекстом:
- `service.name`, `env`, `version`;
- `trace_id`, `span_id`;
- `user_id` (опционально, с учётом безопасности).
5. Следить за кардинальностью
Проверять лейблы метрик и теги логов, чтобы не допустить взрыва временных рядов и объёма логов.
Итог
Мы прошли полный путь:
- подняли стек: `otel-collector`, Prometheus, Tempo, Loki, Grafana и два Go-приложения;
- создали в приложениях провайдеры OTel для метрик, трейсов и логов;
- настроили экспорт в otel-collector и далее в системы хранения;
- посмотрели, как в Grafana выглядят:
- счётчики успешных/ошибочных запросов,
- гистограммы latency,
- распределённые трейсы,
- связанные с ними логи;
- разобрали, чем отличаются Counter, Gauge, Histogram и Summary, и почему в примере выбран именно Histogram;
- обсудили, почему нельзя использовать UUID в метриках как label и где ему место.
Этот шаблон намеренно сделан простым, но в нём есть главное — правильно выстроенный поток телеметрии и связность между метриками, трейсами и логами. На его основе уже можно строить полноценный мониторинг и отладку Go-сервисов в продакшене.



