10 нояб. 2025 г.·6 мин

Сквозная трассировка Go с OpenTelemetry для полной видимости API

Объяснение OpenTelemetry в Go с практическими шагами — как коррелировать трассы, метрики и логи через HTTP‑запросы, фоновые задачи и внешние вызовы.

Сквозная трассировка Go с OpenTelemetry для полной видимости API

Что значит сквозная трассировка для Go API

Трасса — это временная шкала одного запроса по мере его прохождения через вашу систему. Она начинается, когда приходит API-вызов, и заканчивается, когда вы отправляете ответ.

Внутри трассы находятся спаны. Спан — это один измеряемый шаг, например «распарсить запрос», «выполнить SQL» или «вызвать платёжного провайдера». Спаны также могут нести полезные данные, например HTTP-статус, безопасный идентификатор пользователя или количество возвращённых строк запроса.

«Сквозная» означает, что трасса не останавливается на первом обработчике. Она следует за запросом через места, где обычно скрываются проблемы: middleware, запросы в базу, обращения к кэшу, фоновые задания, сторонние API (платежи, email, карты) и другие внутренние сервисы.

Трассировка особенно полезна при прерывистых проблемах. Если один из 200 запросов медленный, логи часто выглядят одинаково для быстрых и медленных случаев. Трасса сразу показывает различие: один запрос провёл 800 мс в ожидании внешнего вызова, был дважды повторён, затем инициировал фоновую задачу.

Логи тоже трудно связать между сервисами. Может быть одна строка лога в API, другая в воркере и ничего между ними. С трассировкой эти события разделяют один trace_id, так что вы можете проследить цепочку без догадок.

Трассы, метрики и логи: как они сочетаются

Трассы, метрики и логи отвечают на разные вопросы.

Трассы показывают, что произошло для одного реального запроса. Они говорят, куда ушло время в обработчике, вызовах к БД, обращениях к кэшу и сторонним сервисам.

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

Логи — это «почему» в простом тексте: ошибки валидации, неожиданные входные данные, крайние случаи и решения, которые принял ваш код.

Главная выгода — корреляция. Когда один и тот же trace_id появляется в спанах и в структурированных логах, вы можете перейти от строки лога к точной трассе и сразу увидеть, какой зависимый сервис замедлил выполнение или какой шаг упал.

Простая модель для понимания

Используйте каждый сигнал для того, в чём он силён:

  • Метрики говорят, что что-то не так.
  • Трассы показывают, куда ушло время в одном запросе.
  • Логи объясняют, какие решения принял код и почему.

Пример: ваш POST /checkout начинает таймаутиться. Метрики показывают всплеск p95. Трасса показывает, что большая часть времени уходит на вызов платёжного провайдера. Скооррелированная строка лога внутри того спана показывает повторные попытки из‑за 502, что указывает на настройки бэкоффа или инцидент у провайдера.

Перед добавлением кода: именование, семплинг и что отслеживать

Немного планирования заранее делает трассы удобными для поиска позже. Без этого вы всё ещё будете собирать данные, но базовые вопросы станут сложными: «Это staging или prod?» «Какой сервис начал проблему?»

Начните с единообразной идентичности. Выберите понятное service.name для каждого Go API (например, checkout-api) и одно поле окружения, например deployment.environment=dev|staging|prod. Держите эти значения стабильными. Если имена меняются посреди недели, графики и поиски начнут выглядеть как разные системы.

Далее решите вопрос семплинга. Трассировать все запросы удобно в разработке, но часто дорого в продакшне. Обычный подход — собирать небольшой процент обычного трафика и сохранять трассы для ошибок и медленных запросов. Если вы уже знаете, что некоторые эндпоинты высокочастотны (health check, polling), трассируйте их реже или не трассируйте вовсе.

Наконец, договоритесь, какие теги вы будете добавлять к спанам и что никогда не будете собирать. Держите короткий allowlist атрибутов, которые помогают связывать события между сервисами, и пропишите простые правила приватности.

Хорошие теги обычно включают стабильные ID и грубую информацию о запросе (шаблон маршрута, метод, код статуса). Избегайте чувствительных данных: паролей, платёжных данных, полных адресов email, токенов аутентификации и сырых тел запросов. Если нужно включать значения, связанные с пользователем, хешируйте или редактируйте их перед добавлением.

Пошагово: добавление OpenTelemetry в Go HTTP API

Вы настроите TracerProvider один раз при старте. Он решает, куда отправлять спаны и какие атрибуты ресурса прикреплять ко всем спанам.

1) Инициализация OpenTelemetry

Обязательно задайте service.name. Без него трассы разных сервисов смешаются, и графики станут неудобочитаемыми.

// main.go (startup)
exp, _ := stdouttrace.New(stdouttrace.WithPrettyPrint())

res, _ := resource.New(context.Background(),
	resource.WithAttributes(
		semconv.ServiceName("checkout-api"),
	),
)

tp := sdktrace.NewTracerProvider(
	 sdktrace.WithBatcher(exp),
	 sdktrace.WithResource(res),
)

otel.SetTracerProvider(tp)

Это основа для Go OpenTelemetry трассировки. Далее нужен спан на каждый входящий запрос.

2) Добавьте HTTP middleware и захватывайте ключевые поля

Используйте HTTP‑middleware, которое автоматически стартует спан и записывает код статуса и длительность. Назовите спан по шаблону маршрута (например, /users/:id), а не по сырому URL — иначе у вас будет тысячи уникальных путей.

Стремитесь к чистому базовому набору: по одному серверному спану на запрос, имена спанов по маршруту, запись HTTP‑статуса, отражение ошибок обработчика как ошибок спана и видимость длительности в просмотрщике трасс.

3) Делайте ошибки очевидными

Когда что‑то идёт не так, возвращайте ошибку и помечайте текущий спан как_failed_. Это выделит трассу ещё до просмотра логов.

В обработчиках можно делать так:

span := trace.SpanFromContext(r.Context())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())

4) Проверьте trace ID локально

Запустите API и вызовите эндпоинт. Пропишите в лог trace ID из контекста запроса один раз, чтобы подтвердить, что он меняется от запроса к запросу. Если он всегда пуст, значит middleware не использует тот же контекст, что и обработчик.

Передавайте контекст в DB и сторонние вызовы

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

Сквозная видимость рушится, как только вы теряете context.Context. Входящий контекст запроса должен быть нитью, которую вы передаёте в каждый вызов БД, HTTP‑запрос и вспомогательную функцию. Если вы заменяете его на context.Background() или забываете передать, ваша трасса распадается на отдельные, несвязанные куски работы.

Для исходящих HTTP‑вызовов используйте инструментированный транспорт, чтобы каждый Do(req) становился дочерним спаном текущего запроса. Прокидывайте W3C‑заголовки трассировки в исходящих запросах, чтобы downstream‑сервисы могли присоединять свои спаны к той же трассе.

С запросами в базу нужно сделать то же самое. Используйте инструментированный драйвер или оборачивайте вызовы спанами вокруг QueryContext и ExecContext. Записывайте только безопасные детали. Вы хотите находить медленные запросы, не сливая данные.

Полезные и малорискованные атрибуты: имя операции (например, SELECT user_by_id), имя таблицы или модели, количество строк (только счёт), длительность, количество повторов и грубый тип ошибки (timeout, canceled, constraint).

Таймауты — это часть истории, а не только ошибки. Устанавливайте их через context.WithTimeout для БД и сторонних вызовов и позволяйте отменам всплывать. Когда вызов отменён, помечайте спан как ошибку и добавляйте короткую причину, например deadline_exceeded.

Трассировка фоновых задач и очередей

Деплой туда, где нужно
Развертывайте в нужном вам облаке или в AppMaster Cloud, не меняя способа инструментирования сервисов.
Развернуть сейчас

Фоновая работа — место, где трассы часто обрываются. HTTP‑запрос завершается, затем воркер подхватывает сообщение позже на другой машине без общего контекста. Если ничего не делать, вы получите две независимые истории: трассу API и трассу задачи, которая как будто началась из ниоткуда.

Решение простое: когда вы ставите задачу в очередь, захватите текущий контекст трассы и сохраните его в метаданных задачи (пейлоад, заголовки или атрибуты, в зависимости от очереди). Когда воркер стартует, извлеките этот контекст и начните новый спан как дочерний от исходного запроса.

Безопасная пропагация контекста

Копируйте только контекст трассы, а не пользовательские данные.

  • Инжектируйте только идентификаторы трассы и флаги семплинга (в стиле W3C traceparent).
  • Держите это отдельно от бизнес‑полей (например, выделенное поле "otel" или "trace").
  • Обрабатывайте это как недоверённый ввод при чтении (валидируйте формат, обрабатывайте отсутствие данных).
  • Избегайте помещения токенов, email или тел запросов в метаданные задач.

Какие спаны добавлять (чтобы трассы не превратились в шум)

Читаемые трассы обычно имеют несколько значимых спанов, не десятки мелких. Создавайте спаны вокруг границ и «точек ожидания». Хорошая отправная точка — спан enqueue в обработчике API и спан job.run в воркере.

Добавляйте немного контекста: номер попытки, имя очереди, тип задачи и размер пейлоада (не содержимое). Если происходят повторы, фиксируйте их как отдельные спаны или события, чтобы видеть задержки бэкоффа.

Запланированные задачи тоже должны иметь родителя. Если входящего запроса нет, создавайте новый root‑span для каждого запуска и помечайте его именем расписания.

Корреляция логов с трассами (и безопасность логов)

Трассы показывают, куда ушло время. Логи объясняют, что произошло и почему. Самый простой способ связать их — добавлять trace_id и span_id в каждую запись лога как структурированные поля.

В Go извлеките активный спан из context.Context и дополните логгер один раз на запрос (или задачу). Тогда каждая строка лога будет указывать на конкретную трассу.

span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
logger := baseLogger.With(
  "trace_id", sc.TraceID().String(),
  "span_id",  sc.SpanID().String(),
)
logger.Info("charge_started", "order_id", orderID)

Этого достаточно, чтобы перейти от записи лога к точному спану, который выполнялся в момент события. Также это показывает отсутствие контекста: trace_id будет пуст.

Держите логи полезными, не сливая PII

Логи обычно живут дольше и распространяются дальше, чем трассы, поэтому будьте строже. Предпочитайте стабильные идентификаторы и исходы: user_id, order_id, payment_provider, status и error_code. Если нужно логировать ввод пользователя, предварительно редактируйте его и ограничивайте длину.

Делайте ошибки лёгкими для группировки

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

Добавьте метрики, которые действительно помогают находить проблемы

Создавайте трассируемые Go API
Постройте бэкенд на Go, который можно инструментировать сквозным образом по мере роста API.
Попробовать AppMaster

Метрики — ваша ранняя система оповещений. В системе с Go OpenTelemetry метрики должны отвечать: как часто, насколько плохо и с какого момента.

Начните с небольшого набора, подходящего почти для любого API: счётчик запросов, счётчик ошибок (по классам статусов), перцентили задержек (p50, p95, p99), количество одновременных запросов и задержки зависимостей для БД и ключевых сторонних вызовов.

Чтобы метрики соответствовали трассам, используйте те же шаблоны маршрутов и имена. Если спаны используют /users/{id}, метрики должны делать то же самое. Тогда при всплеске p95 для /checkout вы сможете сразу перейти к трассам, отфильтрованным по этому маршруту.

Будьте осторожны с лейблами (атрибутами). Одна плохая метка может взорвать расходы и сделать дашборды бесполезными. Шаблон маршрута, метод, класс статуса и имя сервиса обычно безопасны. User ID, email, полные URL и сырые сообщения об ошибках обычно — нет.

Добавьте несколько кастомных метрик для бизнес‑критичных событий (например, начало/успешное завершение checkout, неудачи платежей по группам кодов результата, успех/повтор фоновой задачи). Держите набор маленьким и удаляйте неиспользуемое.

Экспорт телеметрии и безопасный rollout

Экспорт — это момент, когда OpenTelemetry становится реальной системой. Сервис должен отправлять спаны, метрики и логи куда‑то надёжно, не замедляя запросы.

Для локальной разработки держите всё просто. Консольный экспортер (или OTLP к локальному коллектору) позволяет быстро увидеть трассы и проверить имена спанов и атрибутов. В продакшне предпочтительнее OTLP к агенту или OpenTelemetry Collector рядом с сервисом. Это даёт единое место для ретраев, маршрутизации и фильтрации.

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

Семплинг делает затраты предсказуемыми. Начните с head‑based семплинга (например, 1–10% запросов), затем добавьте простые правила: всегда семплировать ошибки и медленные запросы выше порога. Для фоновых заданий с большим объёмом снижайте ставки семплинга.

Внедряйте поэтапно: в dev — 100% семплинг, в staging — с реалистичным трафиком и более низким семплингом, затем в production — консервативный семплинг и алерты на сбои экспортера.

Частые ошибки, которые ломают сквозную видимость

Трассировать фоновые задания
Создайте воркер и распространите контекст, чтобы асинхронные задачи связывались с запросами.
Начать сейчас

Сквозная видимость чаще всего терпит неудачу по простым причинам: данные есть, но они не связаны.

Проблемы, которые ломают распределённую трассировку в Go, обычно такие:

  • Теряется контекст между слоями. Обработчик создаёт спан, но вызов БД, HTTP‑клиент или горутина использует context.Background() вместо контекста запроса.
  • Возврат ошибок без пометки спанов. Если не записывать ошибку и не выставлять статус спана, трассы выглядят «зелёными», даже когда пользователи получают 500.
  • Инструментирование всего подряд. Если каждый хелпер становится спаном, трассы превращаются в шум и дорожают.
  • Добавление атрибутов с высокой кардинальностью. Полные URL с ID, email, сырые значения SQL, тела запросов или строки ошибок могут создать миллионы уникальных значений.
  • Оценка производительности по среднему. Инциденты проявляются в перцентилях (p95/p99) и по частоте ошибок, а не по средней задержке.

Быстрая проверка здоровья — взять один реальный запрос и проследить его через границы. Если вы не видите один trace_id, проходящий через входящий запрос, запрос к БД, сторонний вызов и асинхронный воркер, у вас ещё нет сквозной видимости.

Практический чек‑лист «готово»

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

Вы близки, когда можете от отчёта пользователя перейти к точному запросу и проследить его через все хопы.

  • Возьмите одну строку лога из API и найдите точную трассу по trace_id. Подтвердите, что глубокие логи из того же запроса (БД, HTTP‑клиент, воркер) несут тот же контекст трассы.
  • Откройте трассу и проверьте вложенность: сверху — серверный HTTP‑span, затем дочерние спаны для вызовов БД и сторонних API. Плоский список часто значит, что контекст потерян.
  • Инициируйте фоновую задачу из API (например, отправку email‑чека) и убедитесь, что спан воркера связан с запросом.
  • Проверьте метрики по базовым показателям: количество запросов, частота ошибок и перцентили задержек. Убедитесь, что можно фильтровать по маршруту или операции.
  • Просканируйте атрибуты и логи на предмет безопасности: нет паролей, токенов, полных номеров карт или сырых персональных данных.

Простой тест — симулировать медленный checkout, когда платёжный провайдер задерживается. Вы должны увидеть одну трассу с чётко помеченным спаном внешнего вызова и всплеск p95 для маршрута checkout.

Если вы генерируете Go‑бэкенды (например, с AppMaster), полезно сделать этот чек‑лист частью релизной рутины, чтобы новые эндпоинты и воркеры оставались трассируемыми по мере роста приложения. AppMaster (appmaster.io) генерирует реальные Go‑сервисы, поэтому вы можете стандартизовать одну настройку OpenTelemetry и переносить её через сервисы и фоновые задачи.

Пример: отладка медленного checkout через сервисы

Сообщение от клиента: «Checkout иногда зависает». Воспроизвести это по требованию сложно — именно здесь пригодится Go OpenTelemetry.

Начните с метрик, чтобы понять форму проблемы. Посмотрите скорость запросов, частоту ошибок и p95/p99 для эндпоинта checkout. Если замедление происходит короткими всплесками и только для части запросов, это обычно указывает на зависимость, очередь или поведение повторов, а не на CPU.

Затем откройте медленную трассу за тот же временной интервал. Часто достаточно одной трассы. Здоровый checkout может занимать 300–600 мс. Плохой — 8–12 секунд, при этом большая часть времени приходится на один спан.

Обычный паттерн: обработчик API быстрый, работа с БД в основном нормальна, затем спан платёжного провайдера показывает повторы с бэкоффом, а downstream‑вызов ждёт из‑за блокировки или очереди. Ответ при этом может возвращать 200, поэтому алерты, основанные только на ошибках, не сработают.

Скооррелированные логи подскажут точный путь в простых словах: «retrying Stripe charge: timeout», затем «db tx aborted: serialization failure», затем «retry checkout flow». Это явный сигнал, что несколько мелких проблем складываются в плохой опыт для пользователя.

Когда вы нашли бутылочное горлышко, консистентность поддерживает читабельность со временем. Стандартизируйте имена спанов, атрибуты (безопасный хеш user ID, order ID, имя зависимости) и правила семплинга между сервисами, чтобы все читали трассы одинаково.

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster