Opentelemetry стек в go: как собрать метрики, трейсы и логи в одном месте

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-сервисов в продакшене.

Прокрутить вверх