Distributed Tracing: OpenTelemetry в Go
Зачем знать: в микросервисной архитектуре один пользовательский запрос проходит через 5-50 сервисов. Логи показывают «что произошло», метрики — «сколько раз», но только трассы отвечают «где и почему медленно». В 2026 OpenTelemetry — единый стандарт; middle 1 Go-разработчик обязан уметь инструментировать сервис, прокидывать context и читать flame graph в Tempo/Jaeger.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- Как в Go (с примерами)
- Gotchas
- Best practices в production
- Вопросы на собесе
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»Что такое distributed tracing
Заголовок раздела «Что такое distributed tracing»Distributed tracing — это техника наблюдения за запросом, который пересекает границы процесса/сервиса. Каждое действие записывается как span (отрезок времени с метаданными), spans связаны причинно-следственной связью и образуют trace (дерево).
Пример: пользователь нажимает «Купить»:
trace_id=abc├── span: api-gateway: POST /buy [120ms]│ ├── span: auth-svc: VerifyToken [10ms]│ ├── span: cart-svc: GetCart [25ms]│ │ └── span: redis: GET cart:user123 [3ms]│ ├── span: payment-svc: Charge [60ms]│ │ └── span: external: stripe.com [55ms] ← вот и узкое место│ └── span: order-svc: CreateOrder [20ms]│ └── span: postgres: INSERT orders [12ms]- Видеть где медленно (latency breakdown по сервисам).
- Понимать зависимости (карта сервисов).
- Дебажить ошибки, которые ловятся не там, где случились.
- Коррелировать с логами и метриками через trace_id.
OpenTelemetry (OTel)
Заголовок раздела «OpenTelemetry (OTel)»В 2026 OTel — индустриальный стандарт. История:
- OpenTracing (2016) — API стандарт.
- OpenCensus (Google, 2018) — API + SDK.
- OpenTelemetry (2019) — слияние двух выше.
OTel = API + SDK + semantic conventions + protocol (OTLP).
Ключевые концепции
Заголовок раздела «Ключевые концепции»Дерево spans с одним TraceID (16 байт, обычно hex 32 символа).
Один отрезок времени:
Name(короткое:db.query,http.request).SpanID(8 байт hex).ParentSpanID(для иерархии).StartTime/EndTime(длительность).Attributes(key-value).Events(точки во времени внутри span).Status(Ok,Error).
Context propagation
Заголовок раздела «Context propagation»Trace должен пересекать процесс. Когда сервис A зовёт B по HTTP, в заголовке передаётся traceparent:
traceparent: 00-{trace_id}-{parent_span_id}-{trace_flags}Сервис B читает этот header, создаёт child span. Стандарт — W3C TraceContext.
Baggage
Заголовок раздела «Baggage»Произвольные key-value, которые пробрасываются вместе с trace context (например, user_id). Не для большой нагрузки!
Sampler
Заголовок раздела «Sampler»Решает, записывать ли trace полностью. Без sampling — слишком много данных.
AlwaysOn— 100% (dev).AlwaysOff— 0%.TraceIDRatioBased(0.1)— 10%.ParentBased— наследовать от parent (если parent sampled, дети тоже).- Tail sampling — решение принимается в Collector после получения всего trace (полезно: «оставить только ошибочные и медленные»).
Exporter
Заголовок раздела «Exporter»Куда отправлять spans. В 2026 фактически все используют OTLP (gRPC или HTTP/protobuf) → OTel Collector → бэкенд.
Backends
Заголовок раздела «Backends»- Jaeger — open-source классика (CNCF).
- Tempo (Grafana) — дешёвое хранилище, ищет по trace_id, использует логи/метрики для запросов.
- Zipkin — старый формат, но всё ещё используется.
- Datadog APM, Honeycomb, New Relic, Lightstep — коммерческие.
OTel Collector
Заголовок раздела «OTel Collector»Пайплайн: receivers → processors → exporters. Без него ваш сервис шлёт прямо в backend, но Collector добавляет:
- Batching (отправка пачками).
- Retry, queue (надёжность).
- Tail sampling.
- Преобразования (filter, attributes).
- Multiple exporters (один трасса → Jaeger + Datadog).
2. Как в Go (с примерами)
Заголовок раздела «2. Как в Go (с примерами)»2.1 Установка
Заголовок раздела «2.1 Установка»go get go.opentelemetry.io/otelgo get go.opentelemetry.io/otel/sdkgo get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpcgo get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttpВ 2026 актуальная версия SDK — v1.30+, API стабильное.
2.2 Инициализация
Заголовок раздела «2.2 Инициализация»package main
import ( "context" "log" "os" "time"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "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.26.0")
func initTracer(ctx context.Context, serviceName, endpoint string) (func(context.Context) error, error) { exp, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(endpoint), // "otel-collector:4317" otlptracegrpc.WithInsecure(), ) if err != nil { return nil, err }
res, err := resource.New(ctx, resource.WithAttributes( semconv.ServiceName(serviceName), semconv.ServiceVersion("1.0.0"), semconv.DeploymentEnvironment(os.Getenv("ENV")), ), ) if err != nil { return nil, err }
tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(5*time.Second), sdktrace.WithMaxExportBatchSize(512), ), sdktrace.WithResource(res), sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))), )
otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, ))
return tp.Shutdown, nil}
func main() { ctx := context.Background() shutdown, err := initTracer(ctx, "payments", "otel-collector:4317") if err != nil { log.Fatal(err) } defer shutdown(ctx)
// ... ваш сервис}2.3 Создание spans вручную
Заголовок раздела «2.3 Создание spans вручную»import "go.opentelemetry.io/otel"
var tracer = otel.Tracer("payments-service")
func ProcessPayment(ctx context.Context, userID string, amount int64) error { ctx, span := tracer.Start(ctx, "ProcessPayment") defer span.End()
span.SetAttributes( attribute.String("user.id", userID), attribute.Int64("payment.amount", amount), )
if err := validate(ctx, userID, amount); err != nil { span.RecordError(err) span.SetStatus(codes.Error, "validation failed") return err }
span.AddEvent("validation_passed")
if err := charge(ctx, userID, amount); err != nil { span.RecordError(err) span.SetStatus(codes.Error, "charge failed") return err }
return nil}
func validate(ctx context.Context, userID string, amount int64) error { _, span := tracer.Start(ctx, "validate") defer span.End() // ... return nil}tracer.Start возвращает новый ctx с прокинутым span. Передавайте дальше — child-spans автоматически найдут parent через ctx.
2.4 Auto-instrumentation HTTP server
Заголовок раздела «2.4 Auto-instrumentation HTTP server»import ( "net/http" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp")
func main() { mux := http.NewServeMux() mux.HandleFunc("/api/users", usersHandler)
// Обёртка создаёт span для каждого запроса автоматически handler := otelhttp.NewHandler(mux, "users-api", otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string { return r.Method + " " + r.URL.Path }), )
http.ListenAndServe(":8080", handler)}2.5 Auto-instrumentation HTTP client
Заголовок раздела «2.5 Auto-instrumentation HTTP client»client := http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport),}
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.com/users", nil)resp, _ := client.Do(req)traceparent header добавляется автоматически.
2.6 gRPC
Заголовок раздела «2.6 gRPC»import ( "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc")
// Serverserver := grpc.NewServer( grpc.StatsHandler(otelgrpc.NewServerHandler()),)
// Clientconn, _ := grpc.NewClient("payments:9090", grpc.WithStatsHandler(otelgrpc.NewClientHandler()),)(Раньше использовались interceptors — otelgrpc.UnaryServerInterceptor() — но в 2026 рекомендуется StatsHandler, он покрывает streams корректно.)
2.7 База данных
Заголовок раздела «2.7 База данных»import ( "github.com/XSAM/otelsql" "database/sql" _ "github.com/lib/pq")
db, err := otelsql.Open("postgres", connStr, otelsql.WithAttributes(semconv.DBSystemPostgreSQL),)
// Регистрация метрик пулаotelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes(...))Каждый db.QueryContext создаёт span db.query с атрибутами db.statement, db.operation.
2.8 Redis
Заголовок раздела «2.8 Redis»import ( "github.com/redis/go-redis/extra/redisotel/v9" "github.com/redis/go-redis/v9")
rdb := redis.NewClient(&redis.Options{Addr: "redis:6379"})if err := redisotel.InstrumentTracing(rdb); err != nil { log.Fatal(err)}Команды GET, SET создают spans.
2.9 Kafka
Заголовок раздела «2.9 Kafka»import ( "go.opentelemetry.io/contrib/instrumentation/github.com/segmentio/kafka-go/otelkafkago")// или для confluent-kafka-go / sarama — отдельные пакетыProducer/consumer оборачиваются, headers заполняются traceparent-ом.
2.10 Propagation вручную
Заголовок раздела «2.10 Propagation вручную»Если нет auto-instrumentation (например, кастомный transport):
// Sender sidecarrier := propagation.MapCarrier{}otel.GetTextMapPropagator().Inject(ctx, carrier)// положить carrier в headers сообщения
// Receiver sidecarrier := propagation.MapCarrier(headers) // headers — map[string]stringctx = otel.GetTextMapPropagator().Extract(ctx, carrier)// теперь tracer.Start(ctx, ...) подцепит parent2.11 Span Events
Заголовок раздела «2.11 Span Events»span.AddEvent("cache_miss", trace.WithAttributes(attribute.String("key", k)),)span.AddEvent("cache_filled", trace.WithTimestamp(time.Now()),)Events отображаются как точки во времени внутри span.
2.12 Span Links
Заголовок раздела «2.12 Span Links»Связь span с другим trace (например, batch обрабатывает много исходных запросов):
spanCtx := trace.SpanContextFromContext(parentCtx)ctx, span := tracer.Start(ctx, "batch.process", trace.WithLinks(trace.Link{SpanContext: spanCtx}),)2.13 Baggage
Заголовок раздела «2.13 Baggage»import "go.opentelemetry.io/otel/baggage"
mem, _ := baggage.NewMember("user_id", "u-123")b, _ := baggage.New(mem)ctx = baggage.ContextWithBaggage(ctx, b)В следующем сервисе:
b := baggage.FromContext(ctx)userID := b.Member("user_id").Value()baggage уходит в baggage header. Не кладите много — это дорого по сети.
2.14 Кастомный Sampler
Заголовок раздела «2.14 Кастомный Sampler»type CustomSampler struct{}
func (s CustomSampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingResult { for _, attr := range p.Attributes { if attr.Key == "http.url" && strings.Contains(attr.Value.AsString(), "/health") { return sdktrace.SamplingResult{Decision: sdktrace.Drop} } } return sdktrace.SamplingResult{Decision: sdktrace.RecordAndSample}}func (s CustomSampler) Description() string { return "custom" }Часто /health, /metrics дропаются, чтобы не засорять.
2.15 Корреляция с slog
Заголовок раздела «2.15 Корреляция с slog»func traceCtxAttrs(ctx context.Context) []slog.Attr { span := trace.SpanFromContext(ctx) sc := span.SpanContext() if !sc.IsValid() { return nil } return []slog.Attr{ slog.String("trace_id", sc.TraceID().String()), slog.String("span_id", sc.SpanID().String()), }}Логи получают trace_id — в Grafana клик переносит на trace в Tempo.
2.16 OTel Metrics (briefly)
Заголовок раздела «2.16 OTel Metrics (briefly)»OTel поддерживает не только traces, но и metrics, logs (через одну SDK). В 2026 в Go: metrics — стабильно, logs — beta. Можно использовать OTel metrics вместо Prometheus client, экспортируя в Prometheus через collector. Но в большинстве проектов: metrics — Prometheus, traces — OTel, logs — slog → Loki.
3. Gotchas
Заголовок раздела «3. Gotchas»3.1 Не передаёте ctx — теряете parent
Заголовок раздела «3.1 Не передаёте ctx — теряете parent»func handler(w http.ResponseWriter, r *http.Request) { go process() // BAD: новый goroutine без ctx}
func handler(w http.ResponseWriter, r *http.Request) { go process(r.Context()) // GOOD}Без ctx tracer.Start создаст root span — связь с запросом потеряна.
3.2 Span.End() в defer обязательно
Заголовок раздела «3.2 Span.End() в defer обязательно»ctx, span := tracer.Start(ctx, "op")defer span.End() // ОБЯЗАТЕЛЬНО, иначе span не закроется и не уйдётЕсли забыли — span зависнет в SDK буфере. При перезапуске потеряется.
3.3 Sampling в prod
Заголовок раздела «3.3 Sampling в prod»sdktrace.WithSampler(sdktrace.AlwaysSample()) // BAD в prod — много данных100% sampling в prod = терабайты в неделю. Используйте TraceIDRatioBased(0.05-0.1) или tail-based в collector.
3.4 Cardinality attributes
Заголовок раздела «3.4 Cardinality attributes»Не клейте user_id, request_id в attributes как сотни тысяч уникальных значений — backend (Jaeger/Tempo) индексирует, и cardinality растёт. Лучше user_id один раз в root span, не в каждом child.
3.5 Большие attributes
Заголовок раздела «3.5 Большие attributes»span.SetAttributes(attribute.String("response.body", body)) // BAD: тело 1MBBбекенды режут до ~10KB. Большие данные → s3 link.
3.6 PII в attributes
Заголовок раздела «3.6 PII в attributes»Аналогично логам: не клейте пароли, токены, email в attributes. У трасс retention обычно 14-30 дней — но всё равно лучше mask или вообще не писать.
3.7 Span name cardinality
Заголовок раздела «3.7 Span name cardinality»tracer.Start(ctx, fmt.Sprintf("GET /users/%s", id)) // BAD: имя span содержит user_idSpan names как метрики — должны быть низкой cardinality. Используйте route pattern: GET /users/:id.
3.8 RecordError + SetStatus
Заголовок раздела «3.8 RecordError + SetStatus»if err != nil { span.RecordError(err) // запишет error.* attributes // span.SetStatus(codes.Error, err.Error()) // нужно отдельно! return err}RecordError НЕ меняет статус span на Error. Делайте оба.
3.9 Shutdown — обязательно
Заголовок раздела «3.9 Shutdown — обязательно»defer tp.Shutdown(ctx) // flush buffered spansБез shutdown буферизированные spans потеряются при выходе.
3.10 Context timeout
Заголовок раздела «3.10 Context timeout»tracer.Start(ctx, ...) не уважает ctx.Deadline() — span не отменится. Но операции внутри должны уважать ctx.
3.11 Слишком много spans
Заголовок раздела «3.11 Слишком много spans»Span на каждую функцию = огромные деревья и шум. Один span на «работу» (HTTP-handler, DB-query, RPC-call). Внутри handler — события, attributes.
3.12 Receiver vs Sender propagation
Заголовок раздела «3.12 Receiver vs Sender propagation»Не все клиенты поддерживают OTel headers по умолчанию. Если RabbitMQ — пишите сами в headers сообщения; читая — extract вручную.
3.13 Версии semconv
Заголовок раздела «3.13 Версии semconv»go.opentelemetry.io/otel/semconv/v1.26.0 — версия семантических соглашений. Атрибуты меняются (например, было http.status_code, стало http.response.status_code). При обновлении SDK проверьте — backend дашборды могут сломаться.
3.14 ParentBased сэмплер
Заголовок раздела «3.14 ParentBased сэмплер»sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))Если parent sampled, ваш сервис тоже sample (даже если 0.1). Это критично для consistency — нельзя дропать trace в середине.
3.15 SDK packages vs API packages
Заголовок раздела «3.15 SDK packages vs API packages»import "go.opentelemetry.io/otel" // APIimport "go.opentelemetry.io/otel/sdk/trace" // SDKБиблиотеки должны зависеть только от API (otel). SDK — только в main сервиса. Это позволяет менять SDK без рекомпиляции зависимостей.
4. Best practices в production
Заголовок раздела «4. Best practices в production»4.1 Sampling 5-10% + tail-based для ошибок
Заголовок раздела «4.1 Sampling 5-10% + tail-based для ошибок»В collector настройте tail sampling:
processors: tail_sampling: policies: - name: errors type: status_code status_code: { status_codes: [ERROR] } - name: slow type: latency latency: { threshold_ms: 1000 } - name: probabilistic type: probabilistic probabilistic: { sampling_percentage: 5 }Получаете 100% ошибочных, 100% медленных, 5% остальных.
4.2 Service name + version + env
Заголовок раздела «4.2 Service name + version + env»В каждом сервисе:
service.name— нижний регистр, дефис:payments-service.service.version— git SHA или semver.deployment.environment—prod/staging/dev.
4.3 Не трассируйте /health, /metrics
Заголовок раздела «4.3 Не трассируйте /health, /metrics»Это шум. Дропайте в SDK sampler или collector filter.
4.4 Auto + manual instrumentation
Заголовок раздела «4.4 Auto + manual instrumentation»- Auto: HTTP, gRPC, БД, Redis, Kafka — через готовые пакеты
go.opentelemetry.io/contrib/.... - Manual: бизнес-логика, важные шаги (валидация, расчёт).
4.5 Collector — обязательно
Заголовок раздела «4.5 Collector — обязательно»Не отправляйте напрямую из приложения в backend. Collector:
- Batching → меньше сетевых вызовов.
- Retry/queue → надёжность.
- Tail sampling → меньше данных.
- Multiple exporters → migration легко.
4.6 Resource limits
Заголовок раздела «4.6 Resource limits»processors: memory_limiter: check_interval: 1s limit_percentage: 75Без этого collector OOM при пике трафика.
4.7 Корреляция с логами
Заголовок раздела «4.7 Корреляция с логами»{"level":"ERROR","msg":"payment failed","trace_id":"abc","span_id":"def","err":"..."}В Grafana: панель Tempo рядом с Loki, клик на trace_id в логе открывает trace.
4.8 Корреляция с метриками — exemplars
Заголовок раздела «4.8 Корреляция с метриками — exemplars»OTel + Prometheus exemplars: метрика http_request_duration_seconds хранит ссылки на trace_id. Из dashboard клик на «медленную» точку → trace.
4.9 Attributes — стандартные
Заголовок раздела «4.9 Attributes — стандартные»Используйте semconv пакет, не свои имена:
span.SetAttributes( semconv.HTTPMethod(r.Method), semconv.HTTPRoute("/users/:id"), semconv.HTTPResponseStatusCode(200), semconv.UserID("u-123"),)Дашборды и алерты в backend опираются на стандартные имена.
4.10 Не трассируйте чувствительные пути
Заголовок раздела «4.10 Не трассируйте чувствительные пути»OAuth-callback, password reset — лучше игнорировать или strip query string.
4.11 Бюджет на overhead
Заголовок раздела «4.11 Бюджет на overhead»Целевой overhead — < 5%. Если больше:
- Снизьте sampling.
- Меньше spans (агрегируйте мелкие операции).
- Не вызывайте span.SetAttributes в hot loop.
4.12 Retention
Заголовок раздела «4.12 Retention»Trace storage дорог. Tempo + S3 backend = $/TB. 7-14 дней retention обычно достаточно. Долгое хранение — только для аудита/compliance.
4.13 SLI на trace coverage
Заголовок раздела «4.13 SLI на trace coverage»Метрика «процент запросов с trace_id» = SLI наблюдаемости. Если < 95% — auto-instrumentation сломана.
4.14 OTel Collector — Deployment в K8s
Заголовок раздела «4.14 OTel Collector — Deployment в K8s»- Sidecar (один collector на pod) — для очень больших инстансов.
- Daemonset (один на node) — стандарт.
- Deployment (центральный) — для tail sampling, нужен load balancer.
Обычно: agent (daemonset) → gateway (deployment) → backend.
4.15 Tracing для batch-jobs
Заголовок раздела «4.15 Tracing для batch-jobs»Создавайте root span на запуск, передавайте ctx в горутины. Часто помогают span links: один batch span с links на исходные events.
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»-
Что такое distributed tracing? Запись пути запроса через сервисы как дерева spans с общим trace_id. Используется для debugging latency и зависимостей.
-
Чем OpenTelemetry отличается от OpenTracing? OTel = слияние OpenTracing (API) + OpenCensus (SDK) + новые vendor-neutral conventions. OpenTracing deprecated.
-
Что такое span? Отрезок времени с именем, временем начала/конца, attributes, events, status. Имеет родителя — образует tree (trace).
-
Как пробрасывается trace между сервисами? Через W3C TraceContext header
traceparent(HTTP) или message headers (Kafka, RabbitMQ). -
Что такое sampling? Решение, записывать ли trace. AlwaysOn (dev), TraceIDRatioBased(0.1) для prod, ParentBased для consistency.
-
Чем tail-based sampling лучше head-based? Head-based решает в начале (теряет редкие ошибки если sample rate низкий). Tail-based — в collector, имея весь trace (оставляет ошибки и медленные).
-
Что такое OTLP? OpenTelemetry Protocol — gRPC/HTTP бинарный (protobuf) протокол отправки telemetry. Поддерживается всеми backends в 2026.
-
Зачем OTel Collector? Batching, retry, tail sampling, преобразования, multiple exporters. Sделать в коде каждого сервиса нельзя — collector централизует.
-
Чем
RecordErrorотличается отSetStatus?RecordErrorдобавляет eventexceptionс attributes (msg, stacktrace).SetStatus(codes.Error)меняет статус span. Делайте оба. -
Что такое baggage? Key-value, пробрасываемые с trace context. Для бизнес-данных (
user_id), но не для большого объёма. -
Чем
tracer.Startотличается от просто создания span?Startвозвращает новый ctx с прокинутым span. Без передачи ctx дальше — child spans не свяжутся. -
Что такое semantic conventions? Стандартные имена attributes:
http.method,db.system,service.name. Позволяют backend универсально визуализировать. -
Как корелировать traces с логами? Класть
trace_idиspan_idиз контекста в каждый лог-record (через custom handler в slog). -
Что такое exemplars? Ссылки из Prometheus метрики на trace_id. Клик в Grafana → flame graph конкретного запроса.
-
Сколько накладных расходов добавляет OTel? Обычно < 5% CPU, ~100-200 ns на span. При 100% sampling и тысячах spans/sec может быть больше — снижайте sampling.
-
Что такое span link? Связь между spans разных traces (например, batch обрабатывает 100 событий из 100 разных traces).
-
Зачем
ParentBasedsampler? Чтобы child spans всегда наследовали решение от parent. Иначе trace будет «дырявым» (одни сервисы sample, другие нет). -
Чем
defer span.End()важен? БезEndspan не отправится. Defer гарантирует вызов даже при панике. -
Что такое Jaeger / Tempo / Zipkin? Backends для traces. Jaeger — CNCF классика. Tempo — Grafana, ищет по trace_id, дешёвое хранилище. Zipkin — старый формат.
-
Зачем
Shutdownу TracerProvider? Flush буферизированных spans перед выходом. Без него — потеряются. -
Что такое propagator? Компонент, который сериализует/десериализует trace context в headers. По умолчанию
TraceContext(W3C) +Baggage. -
Чем
StatsHandlerлучшеInterceptorдля gRPC? Покрывает streaming, более низкоуровневый, рекомендован OTel community с 2023. -
Что писать в span name? Низкая cardinality, короткое:
db.query,http.request,kafka.produce. НЕGET /users/123(cardinality bomb), ноGET /users/:idнорм. -
Как тестировать tracing?
sdktrace.NewTracerProviderс in-memory exporter (tracetest.SpanRecorder). Проверяете spans в Recorder. -
Как OTel помогает находить N+1 query? В trace видно: внутри HTTP-handler сотни spans
db.queryс одинаковым SQL — характерная картина N+1.
6. Practice
Заголовок раздела «6. Practice»Упражнение 1: Базовый setup
Заголовок раздела «Упражнение 1: Базовый setup»Создайте сервис «hello-world», подключите OTel SDK, экспорт в локальный Jaeger (docker run jaegertracing/all-in-one).
Упражнение 2: HTTP цепочка
Заголовок раздела «Упражнение 2: HTTP цепочка»Сделайте 2 сервиса: A (gateway) → B (worker). Запрос в A создаёт span, A зовёт B по HTTP, B создаёт child span. Проверьте в Jaeger, что trace связан.
Упражнение 3: gRPC + БД
Заголовок раздела «Упражнение 3: gRPC + БД»Сервис с gRPC + Postgres (otelgrpc + otelsql). Один gRPC-метод делает 3 SQL-запроса — в Jaeger увидьте 4 spans (gRPC + 3 SQL).
Упражнение 4: Ручной span
Заголовок раздела «Упражнение 4: Ручной span»В функции CalculatePrice оберните цикл в span, добавьте attributes (items.count), events (discount_applied).
Упражнение 5: Sampler
Заголовок раздела «Упражнение 5: Sampler»Подключите TraceIDRatioBased(0.1). Запустите 1000 запросов, посчитайте в Jaeger — ~100 traces. Затем ParentBased(TraceIDRatioBased(0.1)) — поведение должно быть таким же, но trace целостный.
Упражнение 6: Tail-based в Collector
Заголовок раздела «Упражнение 6: Tail-based в Collector»Настройте collector на tail sampling: все traces с ошибкой и > 500ms, плюс 5% остальных.
Упражнение 7: slog + trace_id
Заголовок раздела «Упражнение 7: slog + trace_id»Реализуйте slog.Handler, который читает trace_id/span_id из ctx и добавляет в каждый лог.
Упражнение 8: Baggage
Заголовок раздела «Упражнение 8: Baggage»В сервисе A пробросьте user_id через baggage. В B прочитайте и используйте в логах.
7. Источники
Заголовок раздела «7. Источники»- Официальная документация OpenTelemetry — https://opentelemetry.io/docs/languages/go/.
- W3C TraceContext spec — https://www.w3.org/TR/trace-context/.
- Semantic Conventions — https://opentelemetry.io/docs/specs/semconv/.
- OpenTelemetry Collector — https://opentelemetry.io/docs/collector/.
- Grafana Tempo docs — https://grafana.com/docs/tempo/.
- Jaeger Architecture — https://www.jaegertracing.io/docs/latest/architecture/.
- “Distributed Systems Observability” by Cindy Sridharan (O’Reilly).
- CNCF OpenTelemetry Project — https://www.cncf.io/projects/opentelemetry/.
- Honeycomb blog about tracing — https://www.honeycomb.io/blog (Charity Majors).
- otel-go contrib repo — https://github.com/open-telemetry/opentelemetry-go-contrib (готовые instrumentations).