логотип PurpleSchool
логотип PurpleSchool

Трейсинг запросов с OpenTelemetry в Go

Автор

Олег Марков

Введение

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

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

Сейчас я покажу, как на практике организовать трейсинг запросов в приложениях на Go с помощью OpenTelemetry, объясню, как это работает, расскажу про базовые и продвинутые возможности, а также разберу примеры кода для типовых задач: HTTP, gRPC, кастомные события.

Концепции трейсинга и OpenTelemetry

Что такое трейсинг

Трейсинг — это процесс отслеживания пути запроса через компоненты вашей системы. Такой подход позволяет определить, где происходят задержки, какие сервисы задействованы и как они взаимодействуют между собой. Каждый отдельный запуск запроса называют "трейсом", а его отдельные этапы — "спанами" (spans).

Пример: клиент отправил HTTP-запрос, который пробежал через несколько микросервисов и базу данных. OpenTelemetry поможет увидеть всю цепочку действий, выявить "узкие места" и понять, в каком компоненте возникла ошибка или задержка.

Из чего состоит трейсинг в OpenTelemetry

  • Trace (Трейс) — полный путь запроса через сервисы.
  • Span (Спан) — отдельный этап или операция в этой цепочке, например, SQL-запрос, внешний HTTP-запрос.
  • Context — объект в Go, хранящий информацию о текущем трейсинге и служащий для передачи этого контекста между функциями и сервисами.

С помощью инструментов визуализации (например, Jaeger, Zipkin, Tempo, Honeycomb и др.) вы получаете UI с деревом вызовов и временными характеристиками.

Зачем использовать OpenTelemetry

  • Не завязан на конкретный вендор
  • Поддерживает стандартные библиотеки Go
  • Умеет работать с HTTP, gRPC и сторонними библиотеками
  • Автоматически собирает трейсинг внутри фреймворков
  • Легко расширяется дополнительными инструментами (метрики, логи)

Подключение OpenTelemetry к Go проекту

Установка необходимых пакетов

Для начала вам понадобятся основные библиотеки. Смотрите, вот команды, которые устанавливают всё необходимое:

go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk/trace
go get go.opentelemetry.io/otel/exporters/jaeger # Можно выбрать любой экспортёр

Для интеграции с HTTP и gRPC есть дополнительные обёртки:

go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
go get go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc

Базовая настройка трейсера

Для работы OpenTelemetry необходима инициализация трейсера и экспортёра. Экспортёр отвечает за отправку собранных трейсингов (например, в Jaeger или Zipkin).

Покажу базовый пример настройки трейсера на отправку данных в Jaeger:

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/sdk/trace"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "context"
    "log"
)

func InitTracer() func(context.Context) error {
    // Создаём экспортёр Jaeger с указанием адреса агента
    exp, err := jaeger.New(jaeger.WithAgentEndpoint(jaeger.WithAgentHost("localhost"), jaeger.WithAgentPort("6831")))
    if err != nil {
        log.Fatal("Failed to create Jaeger exporter:", err)
    }

    // Создаём поставщика трейсинга с установленным экспортёром
    tp := trace.NewTracerProvider(
        trace.WithBatcher(exp),
        trace.WithSampler(trace.AlwaysSample()), // Всегда сохраняет трейсы, для регулировки ставьте подходящий сэмплер
    )

    otel.SetTracerProvider(tp)

    // Возвращаем функцию для graceful shutdown
    return tp.Shutdown
}

Комментарий: Эта функция инициализирует трейсинг с отправкой в Jaeger и возвращает функцию завершения — её удобно вызывать при закрытии приложения. Без инициализации поставщика трейсинга данные попросту не отправятся никуда.

Пример интеграции с HTTP сервером

OpenTelemetry предоставляет адаптер для стандартного net/http сервера, который автоматически создаёт спаны на каждом запросе.

Вот рабочий шаблон:

import (
    "net/http"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func main() {
    shutdown := InitTracer()         // Запускаем трейсинг
    defer shutdown(context.Background())

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Здесь автоматически будет доступен контекст трейсинга
        w.Write([]byte("Hello, OpenTelemetry!"))
    })

    // Оборачиваем обработчик в otelhttp
    wrappedHandler := otelhttp.NewHandler(handler, "HelloHandler")

    http.ListenAndServe(":8080", wrappedHandler)
}

Комментарий: Теперь каждый входящий HTTP-запрос станет отдельным спаном в цепочке трейсинга. Спаны со всеми метаданными отправятся в Jaeger.

Автоматическая трассировка внешних HTTP-запросов

Часто нужно видеть не только входящие, но и исходящие запросы из вашего сервиса к сторонним API или сервисам. Для этого используйте otelhttp.Transport как обёртку для http-клиентов:

import (
    "net/http"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

client := http.Client{
    // otelhttp.Transport автоматически создаёт спан на каждый исходящий запрос
    Transport: otelhttp.NewTransport(http.DefaultTransport),
}

// Теперь все запросы этого клиента будут попадать в трейсинг
req, _ := http.NewRequest("GET", "http://example.com", nil)
client.Do(req)

Явное создание кастомных спанов в приложении

Вы можете создавать свои кастомные спаны для отслеживания конкретных этапов или медленных операций.

Вот так вручную создается спан:

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
)

func DoSomething(ctx context.Context) {
    tracer := otel.Tracer("myapp")
    // Начинаем спан — ctx становится контекстом нового спана
    ctx, span := tracer.Start(ctx, "DoSomething")
    defer span.End() // Не забывайте завершать спан

    // Проводим работу — всё это будет замеряться
    result := someHeavyComputation()

    // Добавляем атрибут к спану (дополнительные метаданные)
    span.SetAttributes(attribute.String("result", result))
}

Комментарий: Контекст ctx должен передаваться по всей цепочке вызовов. Если теряется или создается новый, цепочка трейсинга рвется.

Пример трейсинга в gRPC сервисах

Для gRPC серверов и клиентов в Go есть специализированные интерцепторы.

Пример для сервера:

import (
    grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
    "google.golang.org/grpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)

func NewGRPCServer() *grpc.Server {
    // Interceptor автоматически создаёт спан на каждый вызов метода gRPC
    return grpc.NewServer(
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
            otelgrpc.UnaryServerInterceptor(),
            // Вы можете добавить и другие интерцепторы!
        )),
    )
}

Пример для клиента:

import (
    "google.golang.org/grpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)

// dial gRPC server with OpenTelemetry interceptor
conn, err := grpc.Dial(
    "localhost:50051",
    grpc.WithInsecure(),
    grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()), // вот тут
)

Теперь каждый вызов gRPC метода будет трейситься как отдельный спан, а их цепочки можно будет видеть в Jaeger/Zipkin.

Трейсинг SQL-запросов

Для популярной библиотеки database/sql существует обертка для автоматического трейсинга.

go get go.opentelemetry.io/contrib/instrumentation/database/sql/otelsql

Подключение:

import (
    "database/sql"
    "go.opentelemetry.io/contrib/instrumentation/database/sql/otelsql"
    _ "github.com/lib/pq"
)

func main() {
    shutdown := InitTracer()
    defer shutdown(context.Background())

    // Регистрируем драйвер с трейсингом
    otelsql.Register("postgres", otelsql.WithAttributes())

    db, err := sql.Open("postgres", "user=... dbname=... sslmode=disable")
    // теперь ваши SQL-запросы попадают в трейсинг как отдельные спаны
}

Экспорт трейсингов для визуализации

Мы использовали Jaeger в примерах, но можно так же отправлять данные в Zipkin, Tempo, New Relic, Datadog и остальные популярные платформы мониторинга. Для каждого из них есть свой экспортёр (otel/exporters/zipkin, otel/exporters/otlp/otlptrace, и т.д.).

Конфигурирование обычно отличается только параметрами подключения — посмотрите пример для Zipkin:

import (
    "go.opentelemetry.io/otel/exporters/zipkin"
)

exp, err := zipkin.New("http://localhost:9411/api/v2/spans")

Сэмплирование (Sampling)

Для высоконагруженных сервисов трейсить каждый запрос — дорого. Оптимально использовать сэмплирование, чтобы сохранялось, например, только 10% трафика.

Пример настройки сэмплера:

import "go.opentelemetry.io/otel/sdk/trace"

// Сохранять 10% трафика
sampler := trace.ParentBased(trace.TraceIDRatioBased(0.1))

tp := trace.NewTracerProvider(
    trace.WithSampler(sampler),
    // другие параметры...
)

Распространённые проблемы при интеграции

Потеря контекста

Если не передавать контекст ctx между обработчиками, цепочка трейсинга обрывается: спаны теряют своих "родителей", получается несколько непересекающихся трейс-деревьев.

Рекомендация: всегда передавайте ctx в функции и используйте его при старте новых спанов.

Мешанина зависимостей

Разделяйте ответственность между инициализацией SDK, экспортёром, регистрацией обработчиков и бизнес-логикой. Это облегчает тестирование и миграции.

Утечки памяти

Не забывайте вызывать span.End() для каждого открытого спана, иначе ресурс не освободится.

Зависание shutdown при остановке приложения

Вызовите функцию завершения (например, tp.Shutdown(context.Background())), чтобы все буферизированные трейсинги ушли на сервер мониторинга.

Практические советы по работе с трейсингом OpenTelemetry в Go

Базовые best practices

  • Всегда передавайте context.Context между функциями.
  • Явно завершайте спаны через defer span.End().
  • Согласно необходимости добавляйте атрибуты (span.SetAttributes) и события (span.AddEvent) для детализации.
  • Настраивайте сэмплинг разумно — чем больше трафика, тем выше процент пропуска.
  • Мониторьте метрики SDK (otel/sdk/metric), чтобы понимать, сколько спанов создано, отправлено, отфильтровано.
  • Используйте один глобальный TracerProvider для приложения и настраивайте его централизованно.
  • Для дебага полезно использовать stdout-экспортёр для быстрой проверки структуры спанов.

Расширенные сценарии

  • Интегрируйте трейсинг с системами логирования (например, через TraceID/SpanID в логах для стыковки логов и трейсинга).
  • Инструментируйте middleware (аутентификация, авторизация, rate limiting), чтобы трейсинг был полным.
  • Обрабатывайте ошибки через API трейсинга, чтобы находить проблемные участки не только по задержкам, но и по причинам возникновения ошибок.

Заключение

Трейсинг в Go с помощью OpenTelemetry — это современный способ получить детальное представление о работе вашего приложения, его проблемных и медленных местах. Благодаря интеграции с HTTP, gRPC, базами данных, возможностям кастомизации, сэмплингу и мощной визуализации, этот инструмент отлично подходит как для небольших сервисов, так и для крупных распределённых систем.

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

Частозадаваемые технические вопросы по теме статьи и ответы на них

Как избежать создания большого числа спанов при внутреннем цикличном вызове функций?

Используйте кастомный сэмплер или оборачивайте циклические вызовы условием — создавайте спаны только для "наиболее интересных" случаев, например, для первой итерации или при возникновении ошибки. Либо размещайте спан вокруг большого блока, а не на каждый микровызов.

Как интегрировать трейсинг с существующей системой логирования?

Передавайте идентификаторы TraceID и SpanID через context. В логерах, поддерживающих хуки (например, logrus, zap), пишите middleware, который извлекает эти идентификаторы из текущего ctx. Указывайте их в логах, чтобы быстро находить логи по конкретной трассе.

Можно ли настроить несколько экспортёров одновременно (например, Jaeger и stdout)?

Да, создавайте несколько экспортёров и объединяйте их с помощью trace.NewBatchSpanProcessor для каждого экспортёра — все они будут получать одни и те же спаны. Пример:

tp := trace.NewTracerProvider(
    trace.WithSpanProcessor(trace.NewBatchSpanProcessor(exporter1)),
    trace.WithSpanProcessor(trace.NewBatchSpanProcessor(exporter2)),
)

Как проверить, что трейсинг работает без установки Jaeger/Zipkin?

Используйте экспортёр stdout, который пишет трейсинги прямо в консоль в читаемом (JSON либо human-readable) формате:

import "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
exp, _ := stdouttrace.New(stdouttrace.WithPrettyPrint())

Как исключить из трейсинга определённые endpoint'ы (например, healthcheck)?

Используйте фильтрацию в middleware, например, не оборачивайте обработчик /healthz в otelhttp.NewHandler или пишите дополнительную логику в интерцепторе, чтобы игнорировать определённые пути или методы.

Стрелочка влевоПаттерны проектирования в GolangНастройка шины событий NATS NSQ в GoСтрелочка вправо

Постройте личный план изучения Golang до уровня Middle — бесплатно!

Golang — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Golang

Работа с YAML в GolangПреобразование типов в GolangКонвертация структур в JSON в GolangStrconv в GolangИспользование пакета SQLx для работы с базами данных в GolangРазбираемся с SQL в GolangРазделение строк с помощью функции split в GolangSort в GoПоиск и замена строк в Go - GolangИспользование пакета reflect в GolangРабота с PostgreSQL в GoPointers в GolangПарсинг в GoРабота со списками (list) в GolangПреобразование int в string в GolangРабота с числами с плавающей точкой в GolangРабота с полями в GolangИспользование enum в GolangОбработка JSON в GoЧтение и запись CSV-файлов в GolangРабота с cookie в GolangРегистры в GoКэширование данных в GolangПреобразование byte в string в GolangByte в GoИспользование bufio для работы с потоками данных в GolangДобавление данных и элементов (add) в Go
Логирование в Golang. Zap, Logrus, Loki, GrafanaРабота с Docker-контейнерами в GoИспользование pprof в GolangМеханизмы синхронизации в GolangРабота с пакетом S3 в GolangМониторинг Golang приложений с помощью PrometheusОптимизация проектов на GoПаттерны проектирования в GolangТрейсинг запросов с OpenTelemetry в GoНастройка шины событий NATS NSQ в GoМиграции базы данных в GolangНастройка уровней логирования log levels в GoОркестрация контейнеров Go с Kubernetes + DockerGjGo Playground и компилятор GolangИспользование go mod init для создания модулей GolangРабота с переменными окружения (env) в GolangКоманда go build в GolangАвтоматизация Golang проектов — CI/CD с GitLab CI и JenkinsРуководство по embed в GoОтладка кода в GolangЧтение и использование конфигурации в приложениях на GolangКомпиляция в GolangКак развернуть Go-приложение на облаке AWSАутентификация в Golang
Сетевые протоколы в GoПеременные в GolangЗначения в GolangДженерик %T и его применение в GolangТипы данных в GolangИспользование tls в GolangИспользование tag в структурах GolangSwitch в GoСтроки в GolangРабота с потоками (stream) в GolangSelect в GoРуны в GoРабота с пакетом params в GolangКонвертация строк в числа в GolangNull, Nil, None, 0 в GoНаименования переменных, функций и структур в GoInt в GolangУстановка GolangЧтение и установка HTTP заголовков в GolangMethods в GolangGoLand — IDE для разработки на Golang от JetBrainsОбработка «not found» в GolangFloat в GolangФлаги командной строки в Go (Golang)Запуск внешних команд в GolangОбработка ошибок в GoИспользование defer в GolangЗначения default в GolangГенерация кода в GoФорматирование кода в GolangЧистая архитектура в GolangКаналы (channels) в GolangПолучение body из HTTP запроса в Golang
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Основы Golang

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее