opentelemetry-go для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

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

Начать курс

OpenTelemetry Go: Полное руководство по распределенной трассировке

1. Что это и зачем нужна?

OpenTelemetry — это набор API, SDK и инструментов, предназначенных для создания и управления данными телеметрии, такими как трассы (traces), метрики (metrics) и логи (logs). Библиотека opentelemetry-go является официальной реализацией стандарта OpenTelemetry для языка Go. Она предоставляет разработчикам единый, вендоронезависимый способ инструментировать свои приложения для сбора телеметрии, которую затем можно отправлять в различные бэкенды, такие как Jaeger, Zipkin, Prometheus, Grafana Tempo, Datadog и многие другие.

Зачем она нужна? В современных микросервисных архитектурах запрос пользователя часто проходит через десятки различных сервисов. Когда возникает проблема (например, запрос выполняется слишком долго или заканчивается ошибкой), без распределенной трассировки (distributed tracing) крайне сложно понять, какой именно сервис стал узким местом или источником сбоя. OpenTelemetry позволяет отследить путь запроса через всю систему, увидеть время выполнения каждого шага и связать их в единую трассу (trace), состоящую из спанов (spans). Это дает разработчикам и инженерам по надежности (SRE) мощный инструмент для отладки, мониторинга и оптимизации производительности.

Ключевое преимущество OpenTelemetry перед другими решениями (например, старыми библиотеками OpenTracing или OpenCensus) — это его универсальность и стандартизация. Вы пишете код один раз, используя единый API, и можете менять бэкенд для хранения и визуализации данных без изменения кода приложения. Это делает opentelemetry-go не просто библиотекой, а фундаментом для построения наблюдаемой (observable) системы на Go.

2. Установка

Для установки библиотеки opentelemetry-go и её основных компонентов используйте стандартный менеджер пакетов Go. Вам потребуется установить основной пакет API, SDK для трассировки и экспортер (например, для консоли или Jaeger).

# Устанавливаем основной API
go get go.opentelemetry.io/otel

# Устанавливаем SDK для трассировки
go get go.opentelemetry.io/otel/sdk

# Устанавливаем экспортер для вывода в консоль (для отладки)
go get go.opentelemetry.io/otel/exporters/stdout/stdouttrace

# Устанавливаем HTTP-пропагатор для контекста (для интеграции с HTTP)
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp

# Устанавливаем экспортер для Jaeger (для продакшена)
go get go.opentelemetry.io/otel/exporters/jaeger

После выполнения этих команд, все необходимые зависимости будут добавлены в ваш файл go.mod.

3. Быстрый старт — минимальный рабочий пример

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

package main

import (
	"context"
	"fmt"
	"log"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
	"go.opentelemetry.io/otel/trace"
)

func main() {
	// 1. Создаем экспортер для вывода в консоль
	exporter, err := stdouttrace.New(
		stdouttrace.WithPrettyPrint(),
	)
	if err != nil {
		log.Fatal(err)
	}

	// 2. Создаем TracerProvider с экспортером и ресурсом
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String("my-service"),
		)),
	)
	defer func() {
		if err := tp.Shutdown(context.Background()); err != nil {
			log.Fatal(err)
		}
	}()

	// 3. Устанавливаем глобальный TracerProvider
	otel.SetTracerProvider(tp)

	// 4. Получаем Tracer
	tracer := otel.Tracer("my-tracer")

	// 5. Создаем спан
	ctx, span := tracer.Start(context.Background(), "my-first-span")
	defer span.End()

	// 6. Добавляем атрибуты к спану
	span.SetAttributes(attribute.String("key", "value"))
	span.SetAttributes(attribute.Int("count", 42))

	// 7. Создаем дочерний спан
	_, childSpan := tracer.Start(ctx, "child-span")
	childSpan.SetAttributes(attribute.Bool("is_child", true))
	childSpan.End()

	fmt.Println("Трасса создана и выведена в консоль!")
}

Запустите этот код командой go run main.go. Вы увидите в консоли JSON-представление вашей трассы, состоящей из двух спанов.

4. Основные методы/функции/классы

В этом разделе мы рассмотрим ключевые компоненты библиотеки opentelemetry-go. Для каждого из них приведена сигнатура, описание и пример кода.

4.1. otel.Tracer(name string, options ...trace.TracerOption) trace.Tracer

Что делает: Возвращает объект Tracer из глобального TracerProvider. Tracer используется для создания новых спанов. Параметр name обычно идентифицирует библиотеку или пакет, создающий спаны.

tracer := otel.Tracer("my-app")
ctx, span := tracer.Start(context.Background(), "operation-name")
defer span.End()

4.2. tracer.Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span)

Что делает: Создает новый спан с именем spanName внутри переданного контекста. Возвращает новый контекст, содержащий созданный спан, и сам объект спана. Это основной метод для начала отслеживания операции.

func processRequest(ctx context.Context) {
    tracer := otel.Tracer("processor")
    ctx, span := tracer.Start(ctx, "process-request")
    defer span.End()

    // Выполняем работу...
    doWork(ctx)
}

4.3. span.End(options ...trace.SpanEndOption)

Что делает: Завершает спан. Должен вызываться с помощью defer сразу после создания спана. После вызова этого метода в спан нельзя добавлять атрибуты или события.

func handleRequest(ctx context.Context) {
    _, span := otel.Tracer("handler").Start(ctx, "handle")
    defer span.End()

    // Логика обработки
}

4.4. span.SetAttributes(kv ...attribute.KeyValue)

Что делает: Добавляет один или несколько атрибутов (пар ключ-значение) к спану. Атрибуты используются для аннотирования спана дополнительной информацией, такой как ID пользователя, URL запроса, размер ответа и т.д.

span.SetAttributes(
    attribute.String("http.method", "GET"),
    attribute.String("http.url", "/api/users"),
    attribute.Int("http.status_code", 200),
)

4.5. span.AddEvent(name string, options ...trace.EventOption)

Что делает: Добавляет к спану событие с указанным именем. События — это временные метки с именем и опциональными атрибутами. Они полезны для логирования важных моментов в жизненном цикле операции (например, "кэш-промах", "ошибка валидации").

span.AddEvent("cache-miss", trace.WithAttributes(
    attribute.String("cache.key", "user:123"),
))

4.6. span.SetStatus(code codes.Code, description string)

Что делает: Устанавливает статус спана. Обычно используется для обозначения ошибки. Код codes.Error указывает на то, что операция завершилась неудачно. Код codes.Ok (по умолчанию) означает успех.

if err != nil {
    span.SetStatus(codes.Error, "ошибка при обработке запроса")
    span.RecordError(err)
    return
}
span.SetStatus(codes.Ok, "успешно")

4.7. span.RecordError(err error, options ...trace.EventOption)

Что делает: Записывает ошибку как событие спана. Это удобный способ зафиксировать исключение или ошибку, произошедшую во время выполнения операции. Ошибка будет видна в UI трассировки.

if err != nil {
    span.RecordError(err, trace.WithAttributes(
        attribute.String("error.type", "database_error"),
    ))
}

4.8. otel.SetTracerProvider(tp trace.TracerProvider)

Что делает: Устанавливает глобальный TracerProvider. Все последующие вызовы otel.Tracer() будут использовать этот провайдер. Обычно вызывается один раз в начале работы приложения.

tp := sdktrace.NewTracerProvider(
    sdktrace.WithBatcher(exporter),
    sdktrace.WithResource(resource),
)
otel.SetTracerProvider(tp)

4.9. propagation.Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context

Что делает: Извлекает контекст трассировки из входящего запроса (например, из HTTP-заголовков). Это необходимо для связывания спанов в распределенной системе.

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

func handler(w http.ResponseWriter, r *http.Request) {
    // Извлекаем контекст из HTTP-заголовков
    ctx := propagation.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
    // Теперь ctx содержит контекст родительского спана
    _, span := otel.Tracer("server").Start(ctx, "handle-request")
    defer span.End()
}

4.10. propagation.Inject(ctx context.Context, carrier propagation.TextMapCarrier)

Что делает: Внедряет контекст трассировки в исходящий запрос (например, в HTTP-заголовки). Позволяет передать контекст текущего спана следующему сервису в цепочке.

func makeRequest(ctx context.Context, url string) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    // Внедряем контекст в заголовки запроса
    propagation.Inject(ctx, propagation.HeaderCarrier(req.Header))
    // Отправляем запрос
    resp, _ := http.DefaultClient.Do(req)
}

5. Реальный практический пример (100-200 строк кода)

В этом примере мы создадим простой HTTP-сервер, который делает запрос к другому сервису. Мы добавим распределенную трассировку с помощью opentelemetry-go и экспортера Jaeger. Этот пример покажет, как контекст трассировки передается между сервисами.

package main

import (
	"context"
	"io"
	"log"
	"net/http"
	"time"

	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)

// initTracer создает и возвращает TracerProvider, настроенный на Jaeger
func initTracer() (*sdktrace.TracerProvider, error) {
	// Создаем Jaeger экспортер
	exp, err := jaeger.New(jaeger.WithCollectorEndpoint(
		jaeger.WithEndpoint("http://localhost:14268/api/traces"),
	))
	if err != nil {
		return nil, err
	}

	// Создаем ресурс с именем сервиса
	res, err := resource.New(context.Background(),
		resource.WithAttributes(
			semconv.ServiceNameKey.String("go-microservice"),
		),
	)
	if err != nil {
		return nil, err
	}

	// Создаем TracerProvider
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exp),
		sdktrace.WithResource(res),
	)
	return tp, nil
}

// downstreamHandler имитирует внешний сервис
func downstreamHandler(w http.ResponseWriter, r *http.Request) {
	// Извлекаем контекст трассировки из заголовков
	ctx := propagation.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
	_, span := otel.Tracer("downstream").Start(ctx, "process-downstream")
	defer span.End()

	// Симулируем работу
	time.Sleep(50 * time.Millisecond)
	span.SetAttributes(attribute.String("downstream.result", "success"))
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK from downstream"))
}

// mainHandler - основной обработчик, который вызывает downstreamHandler
func mainHandler(w http.ResponseWriter, r *http.Request) {
	// Контекст уже содержит спан от middleware
	ctx := r.Context()
	tracer := otel.Tracer("main-service")

	// Создаем дочерний спан для обработки запроса
	ctx, span := tracer.Start(ctx, "handle-main-request")
	defer span.End()

	// Добавляем атрибуты запроса
	span.SetAttributes(
		attribute.String("http.method", r.Method),
		attribute.String("http.url", r.URL.String()),
	)

	// Создаем HTTP-клиент с поддержкой трассировки
	client := http.Client{
		Transport: otelhttp.NewTransport(http.DefaultTransport),
	}

	// Создаем запрос к downstream сервису
	req, err := http.NewRequestWithContext(ctx, "GET", "http://localhost:8081/downstream", nil)
	if err != nil {
		span.RecordError(err)
		http                    

Рекомендаций