Перейти к содержимому

OpenTelemetry в Production: глубокое погружение

Зачем знать на Middle 3: OpenTelemetry (OTel) — это стандарт observability в 2026 году. CNCF-проект, который заменил Jaeger client, Zipkin, OpenTracing, OpenCensus, Prometheus client (для метрик в новых проектах). Любой Go-сервис на Middle 3 уровне должен экспортировать traces/metrics/logs через OTel: корректно проставлять resource attributes, использовать context propagation (W3C TraceContext), настраивать sampling (head + tail в Collector), интегрировать slog с trace IDs, и понимать pipeline Collector’а. Без этого SRE-команда не сможет диагностировать инциденты, а compliance не пройдёт.

  1. Концепция: архитектура OTel
  2. Production-deep dive: SDK, Collector, sampling, exemplars, slog integration
  3. Gotchas (10+)
  4. Real cases: Cloudflare, Lyft, FAANG observability
  5. Вопросы (30+)
  6. Practice
  7. Источники

OpenTelemetry — CNCF-проект (incubating → graduated), который объединил OpenCensus (Google) и OpenTracing (CNCF). Цель — единый vendor-neutral API/SDK для traces, metrics, logs. Любой бэкенд (Jaeger, Tempo, Datadog, Honeycomb, New Relic) — это просто экспортёр.

Состояние signals на 2026:

  • Traces — Stable много лет.
  • Metrics — Stable с 2022.
  • Logs — Stable, в Go-SDK GA с 2024.
  • Profiles — beta / experimental (continuous profiling).
┌────────────────────────────────────────────────────────────┐
│ Application (Go) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ OpenTelemetry API (vendor-neutral) │ │
│ │ tracer.Start, meter.Int64Counter, logger.Info │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ OpenTelemetry SDK │ │
│ │ - TracerProvider, MeterProvider, LoggerProvider │ │
│ │ - Sampler │ │
│ │ - Processor (Batch / Simple) │ │
│ │ - Exporter (OTLP, stdout, prometheus) │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
│ OTLP gRPC / HTTP
┌────────────────────────────────────────────────────────────┐
│ OpenTelemetry Collector │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Receivers: OTLP, Jaeger, Zipkin, Prometheus scrape, │ │
│ │ Kafka, FluentForward, HostMetrics │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Processors: batch, memory_limiter, attributes, │ │
│ │ tail_sampling, transform, filter, │ │
│ │ k8sattributes, resource │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Exporters: OTLP, Tempo, Loki, Prometheus RW, │ │
│ │ Kafka, Datadog, Honeycomb, S3 │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ Backends: Tempo, Jaeger, Mimir, Loki, Datadog, NewRelic │
└────────────────────────────────────────────────────────────┘
  • Traces — series spans, формирующие dependency граф.
  • Metrics — counters, histograms, gauges, sum, observable*.
  • Logs — structured logs с trace_id (correlated).

OTel определяет стандартные атрибуты, чтобы данные были совместимы между приложениями. Resource attributes (на уровне процесса):

service.name = "checkout-api"
service.version = "1.4.2"
service.namespace = "ecommerce"
service.instance.id = uuid()
deployment.environment = "production"
k8s.namespace.name = "shop"
k8s.pod.name = "checkout-7d8f-xyz"
k8s.container.name = "app"
host.name = "node-3"
cloud.provider = "aws"
cloud.region = "eu-west-1"

Span attributes:

http.method = "GET"
http.route = "/api/v1/orders/:id"
http.status_code = 200
url.scheme = "https"
url.full = "https://api.example.com/orders/42"
db.system = "postgresql"
db.namespace = "users"
db.statement = "SELECT * FROM users WHERE id = $1"
db.operation = "SELECT"
messaging.system = "kafka"
messaging.destination.name = "orders"

Стандарт sem-conv позволяет писать backend-логику типа «найти все 5xx-ответы», не зная конкретных сервисов.

Между сервисами trace распространяется через W3C TraceContext HTTP headers:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^ ^ ^ ^
| └── trace_id (16 bytes) | └── flags
└── version └── span_id (8 bytes)
tracestate: vendor1=value,vendor2=value

И W3C Baggage:

baggage: userId=42,tenant=acme,region=eu

Baggage — это произвольные ключ-значения, путешествующие с trace. Используется для propagation business context (tenant_id), но не для секретов (виден downstream).

В Go propagation настраивается так:

import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))

package observability
import (
"context"
"fmt"
"os"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
func Init(ctx context.Context, serviceName, version, env string) (func(context.Context) error, error) {
res, err := resource.New(ctx,
resource.WithFromEnv(),
resource.WithProcess(),
resource.WithOS(),
resource.WithContainer(),
resource.WithHost(),
resource.WithAttributes(
semconv.ServiceName(serviceName),
semconv.ServiceVersion(version),
semconv.DeploymentEnvironment(env),
semconv.ServiceInstanceID(os.Getenv("HOSTNAME")),
),
)
if err != nil {
return nil, fmt.Errorf("resource: %w", err)
}
// Trace exporter (OTLP gRPC).
traceExp, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("trace exporter: %w", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(0.1), // head-sampling 10%
)),
sdktrace.WithBatcher(traceExp,
sdktrace.WithBatchTimeout(5*time.Second),
sdktrace.WithMaxExportBatchSize(512),
sdktrace.WithMaxQueueSize(2048),
),
)
otel.SetTracerProvider(tp)
// Metric exporter.
metricExp, err := otlpmetricgrpc.New(ctx,
otlpmetricgrpc.WithEndpoint("otel-collector:4317"),
otlpmetricgrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("metric exporter: %w", err)
}
mp := metric.NewMeterProvider(
metric.WithResource(res),
metric.WithReader(metric.NewPeriodicReader(metricExp,
metric.WithInterval(15*time.Second),
)),
)
otel.SetMeterProvider(mp)
// Propagator.
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return func(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
_ = tp.Shutdown(ctx)
_ = mp.Shutdown(ctx)
return nil
}, nil
}
func (s *OrderService) Create(ctx context.Context, req *CreateRequest) (*Order, error) {
ctx, span := otel.Tracer("orders").Start(ctx, "OrderService.Create",
trace.WithAttributes(
attribute.String("order.customer_id", req.CustomerID),
attribute.Int("order.items_count", len(req.Items)),
),
trace.WithSpanKind(trace.SpanKindInternal),
)
defer span.End()
order, err := s.repo.Insert(ctx, req)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
span.SetAttributes(attribute.String("order.id", order.ID))
return order, nil
}

Конвенции:

  • defer span.End() сразу после Start.
  • При ошибке: RecordError + SetStatus(codes.Error).
  • Span name — название операции (OrderService.Create, GET /api/orders/:id).
  • SpanKind: SERVER (incoming RPC), CLIENT (outgoing), PRODUCER, CONSUMER (messaging), INTERNAL.

Готовые middleware из go.opentelemetry.io/contrib:

import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/contrib/instrumentation/database/sql/otelsql"
"go.opentelemetry.io/contrib/instrumentation/github.com/redis/go-redis/extra/redisotel/v9"
)
// HTTP server.
handler := otelhttp.NewHandler(mux, "api",
otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string {
return r.Method + " " + r.URL.Path
}),
)
// HTTP client.
client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
// gRPC server.
grpc.NewServer(grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()))
// SQL.
db, _ := otelsql.Open("postgres", dsn, otelsql.WithAttributes(semconv.DBSystemPostgreSQL))
// Redis (v9).
rdb.AddHook(redisotel.NewTracingHook())

В 2026 году появилось много готовых обёрток в contrib — буквально все популярные библиотеки.

meter := otel.Meter("payments")
requestsCounter, _ := meter.Int64Counter(
"payments.requests",
metric.WithDescription("Total payment requests"),
metric.WithUnit("{requests}"),
)
latencyHist, _ := meter.Float64Histogram(
"payments.duration",
metric.WithUnit("s"),
metric.WithExplicitBucketBoundaries(0.001, 0.01, 0.1, 0.5, 1, 5),
)
queueDepth, _ := meter.Int64ObservableGauge(
"payments.queue.depth",
)
_, _ = meter.RegisterCallback(func(_ context.Context, o metric.Observer) error {
o.ObserveInt64(queueDepth, int64(queue.Len()))
return nil
}, queueDepth)
// Использование.
requestsCounter.Add(ctx, 1, metric.WithAttributes(
attribute.String("method", "charge"),
attribute.String("status", "success"),
))
latencyHist.Record(ctx, dur.Seconds(), metric.WithAttributes(...))

Кардинальность атрибутов — главная проблема метрик. Не кладите user_id в атрибуты — взорвётся series count.

Решение о сэмпле принимается в начале trace, до спана.

  • AlwaysSample — всё.
  • NeverSample — ничего.
  • TraceIDRatioBased(0.1) — 10% по hash trace_id.
  • ParentBased(...) — уважать решение родителя; если parent сэмплирован — мы тоже.

Standard production setup:

sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(0.1),
sdktrace.WithRemoteParentSampled(sdktrace.AlwaysSample()),
sdktrace.WithRemoteParentNotSampled(sdktrace.NeverSample()),
)

Это значит: если parent сэмплирован — мы тоже; если нет — нет; если корень — 10%.

Tail sampling собирает весь trace (через буферизацию по trace_id), потом решает. Дорого, но позволяет «сохранять все ошибки и 1% успешных».

processors:
tail_sampling:
decision_wait: 30s # ждём 30s после первого спана
num_traces: 100000
expected_new_traces_per_sec: 1000
policies:
- name: keep-errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: keep-slow
type: latency
latency: { threshold_ms: 1000 }
- name: keep-canary
type: string_attribute
string_attribute: { key: env, values: [canary] }
- name: random-1pct
type: probabilistic
probabilistic: { sampling_percentage: 1 }

Логика: оставляем все ошибки, все trace’ы >1s, все canary, плюс 1% всего остального. Это даёт «дешёвый» observability без потери важных traces.

Exemplar — это trace_id, прикреплённый к metric sample. Когда в Grafana вы видите spike на P99 latency — можно кликнуть и сразу перейти к конкретному trace.

OTel SDK добавляет exemplars автоматически, если в meter-provider включена опция WithReader(metric.NewPeriodicReader(..., metric.WithExemplarFilter(metric.AlwaysOn))) (или TraceBased).

Backend (Prometheus / Mimir / Tempo / Grafana) должен поддерживать exemplars (Prometheus 2.26+).

OpenTelemetry Protocol — стандарт передачи телеметрии:

  • OTLP/gRPC (port 4317) — самый эффективный.
  • OTLP/HTTP+Protobuf (port 4318) — proxy-friendly.
  • OTLP/HTTP+JSON (port 4318) — для debug, медленный.
otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("collector:4317"),
otlptracegrpc.WithCompressor("gzip"),
otlptracegrpc.WithTimeout(5*time.Second),
otlptracegrpc.WithRetry(otlptracegrpc.RetryConfig{
Enabled: true,
InitialInterval: 1 * time.Second,
MaxInterval: 30 * time.Second,
MaxElapsedTime: 2 * time.Minute,
}),
)

Двухуровневая deploy-схема:

App pod (gateway sidecar — agent)
│ OTLP localhost:4317
Agent (DaemonSet)
- k8sattributes (auto-tag k8s metadata)
- batch
- memory_limiter
│ OTLP (across cluster)
Gateway (Deployment, 3+ replicas, HPA)
- tail_sampling
- heavier transforms
- fan-out to backends
Backends (Tempo / Mimir / Loki / Datadog / Honeycomb)

Connectors — pipeline-to-pipeline компоненты. Например, spanmetrics connector: считает RED метрики (request rate, error rate, duration) из spans и выдаёт как metrics.

connectors:
spanmetrics:
namespace: tracing
dimensions:
- name: http.method
- name: http.status_code
service:
pipelines:
traces:
receivers: [otlp]
exporters: [spanmetrics, otlp/tempo]
metrics:
receivers: [spanmetrics, otlp]
exporters: [prometheusremotewrite]

С Go 1.21+ slog стало стандартом structured logging. OTel предоставляет slog handler:

import (
"log/slog"
"go.opentelemetry.io/contrib/bridges/otelslog"
)
func main() {
logger := otelslog.NewLogger("checkout-api")
slog.SetDefault(logger)
// ...
ctx, span := otel.Tracer("...").Start(ctx, "Handle")
defer span.End()
slog.InfoContext(ctx, "processing request",
slog.String("user_id", "42"),
slog.Int("amount", 100),
)
// log record получит trace_id / span_id автоматически.
}

Кастомный handler, который вкладывает trace_id в slog atributes:

type otelHandler struct{ slog.Handler }
func (h *otelHandler) Handle(ctx context.Context, r slog.Record) error {
if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
sc := span.SpanContext()
r.AddAttrs(
slog.String("trace_id", sc.TraceID().String()),
slog.String("span_id", sc.SpanID().String()),
)
}
return h.Handler.Handle(ctx, r)
}
ОбъёмСтоимость
100 RPS, 5 spans/req~43k spans/min, ~1.5GB/day raw
1000 RPS, 10 spans/req~860k spans/min, ~30GB/day raw
10k RPS, 20 spans/req~17M spans/min, ~600GB/day raw

Сэмплирование 1-10% обычно достаточно. Tail sampling в Collector — must для serious-volume. Datadog/Honeycomb берут $$$ за spans.

BackendТипОсобенность
Tempoopen-sourceGrafana, S3/GCS backend, traces only
Mimiropen-sourceGrafana, scaled Prometheus storage
Lokiopen-sourceGrafana, logs
Jaegeropen-sourceклассика, поддерживает OTLP
SigNozopen-sourcefull stack, ClickHouse
Uptraceopen-sourceClickHouse
Datadogcommercialfull stack, дорого
Honeycombcommercialbest-in-class trace exploration
Lightstepcommercialсейчас часть ServiceNow
New Reliccommercial
Splunk Observabilitycommercial
otel-collector-gateway.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
max_recv_msg_size_mib: 16
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
scrape_configs:
- job_name: k8s-pods
kubernetes_sd_configs:
- role: pod
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 20
batch:
send_batch_size: 8192
timeout: 5s
send_batch_max_size: 16384
k8sattributes:
auth_type: serviceAccount
passthrough: false
extract:
metadata:
- k8s.namespace.name
- k8s.pod.name
- k8s.pod.uid
- k8s.deployment.name
- k8s.node.name
- k8s.container.name
resource:
attributes:
- key: deployment.environment
from_attribute: env
action: upsert
tail_sampling:
decision_wait: 30s
num_traces: 100000
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 1000 }
- name: random-1pct
type: probabilistic
probabilistic: { sampling_percentage: 1 }
transform/redact:
error_mode: ignore
trace_statements:
- context: span
statements:
- replace_pattern(attributes["db.statement"], "password='[^']+'", "password='[REDACTED]'")
- delete_key(attributes, "http.request.header.authorization")
connectors:
spanmetrics:
namespace: tracing
histogram:
explicit: { buckets: [2ms, 10ms, 50ms, 100ms, 500ms, 1s, 5s] }
dimensions:
- { name: http.method, default: GET }
- { name: http.status_code }
- { name: service.name }
exporters:
otlp/tempo:
endpoint: tempo:4317
tls: { insecure: true }
prometheusremotewrite:
endpoint: http://mimir:9009/api/v1/push
external_labels: { cluster: prod-eu }
loki:
endpoint: http://loki:3100/loki/api/v1/push
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, transform/redact, tail_sampling, batch]
exporters: [otlp/tempo, spanmetrics]
metrics:
receivers: [otlp, prometheus, spanmetrics]
processors: [memory_limiter, k8sattributes, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, batch]
exporters: [loki]
telemetry:
metrics: { level: detailed, address: 0.0.0.0:8888 }
logs: { level: info }

С 2024 Logs SDK в Go стабилен. Использование напрямую (без slog bridge):

import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/log"
"go.opentelemetry.io/otel/log/global"
)
logger := global.Logger("checkout-api")
var rec log.Record
rec.SetTimestamp(time.Now())
rec.SetSeverity(log.SeverityInfo)
rec.SetBody(log.StringValue("processing order"))
rec.AddAttributes(
log.String("order_id", id),
log.Int64("amount", amt),
)
logger.Emit(ctx, rec)

Под капотом: log → BatchLogProcessor → OTLP → Collector → Loki/Elasticsearch.

Profiles в OTel — последний из сигналов. В 2024-2026 — beta. Идея: профили (CPU, heap, goroutine) тоже travel через OTLP вместе с traces/metrics/logs. Backend — Grafana Pyroscope, Parca, Polar Signals.

Преимущества: единый pipeline для всех 4 сигналов; correlation profile ↔ trace.

import "github.com/grafana/pyroscope-go"
pyroscope.Start(pyroscope.Config{
ApplicationName: "checkout-api",
ServerAddress: "http://pyroscope:4040",
Logger: pyroscope.StandardLogger,
Tags: map[string]string{
"version": version,
"env": env,
},
})

Если положить user_id в attribute — каждое уникальное значение создаёт новую time series. 1M пользователей → 1M серий → ваш Prometheus умер. Правило: ≤100 уникальных значений на attribute, используйте route (/users/:id), а не URL (/users/42).

Если head-sampling 1%, но BatchSpanProcessor всё равно стоит — он только не добавит в очередь. Это OK. Но если используете SimpleSpanProcessor — он экспортит синхронно, медленно.

go func() {
// ctx parent уже отменён, но мы делаем "fire-and-forget"
_, span := tracer.Start(ctx, "background")
defer span.End()
// span attached к мёртвому контексту, может быть отвергнут.
}()

Решение: используйте trace.ContextWithSpanContext(context.Background(), trace.SpanContextFromContext(ctx)) чтобы передать только span context.

Gotcha 4: ⚠️ Не вызывайте span.End() — span никогда не экспортится

Заголовок раздела «Gotcha 4: ⚠️ Не вызывайте span.End() — span никогда не экспортится»

defer span.End() сразу после Start. Без End — span висит в памяти. Утечка.

Если кладёте internal_user_email в Baggage — он отправится в каждый downstream-сервис, включая third-party (если вы вызываете chargesheets API). Утечка PII.

Collector держит все spans trace в памяти decision_wait (30s). При 10k RPS × 10 spans/req × 30s = 3M spans в буфере. OOM. Регулируйте num_traces и масштабируйте collector-pods.

Если A сэмплит 10% по trace_id, и B сэмплит 10% по trace_id, и оба используют TraceIDRatioBased с одинаковым hash — они выберут одни и те же trace_ids. Если разные алгоритмы — получите половинные траectories, ад для дебага.

otelhttp.NewHandler ставит span name по r.URL.Path — содержит ID! Используйте otelhttp.WithSpanNameFormatter чтобы взять route pattern из mux’а (chi: chi.RouteContext(r.Context()).RoutePattern()).

Бакеты по умолчанию ([5ms, 10ms, ..., 10s]) хороши для HTTP, но для DB-queries (sub-ms) или batch jobs (minutes) — катастрофа. Подберите бакеты под latency profile.

Gotcha 10: ⚠️ Resource создаётся заново при каждом тесте

Заголовок раздела «Gotcha 10: ⚠️ Resource создаётся заново при каждом тесте»

Если в init() или TestMain создаёте Resource — он кэширует hostname/container. В CI тестах гоняйте resource.New(ctx, resource.WithAttributes(...)) без WithHost, иначе тесты flaky.

4317 (gRPC) ≠ 4318 (HTTP). Перепутали — клиент не получит ничего, тихо.

Если каждый запрос создаёт slog.New(otelslog.NewHandler(...)) — это аллокация и атрибуты не унифицированы. Создавайте один раз в init, используйте WithGroup для контекстных групп.


Cloudflare обрабатывает миллионы RPS. Observability — внутренние системы (Quicksilver, internal log pipeline) + OTel для customer-facing аналитики. Tail sampling на edge, агрегации на uberscale.

Lyft был пионером observability. От statsd/Datadog к собственному OTel-стеку. Public talks от Lyft на CNCF events.

Honeycomb — backend с уникальным подходом «high cardinality OK». Их подход: «не сэмплируйте, всё храните и stream-аналитика». Идея — observation, не aggregation.

Реальный случай. Pre-Black-Friday включили tail sampling на gateway collector. Decision-wait 60s, num_traces 1M, кип все 5xx и >500ms. Объём traces упал с 12TB/день до 800GB/день; всё «больное» осталось. Cost saving 90%.


  1. Что такое OpenTelemetry и какие CNCF-проекты он заменил?
  2. Какие три signals в OTel?
  3. Чем API отличается от SDK?
  4. Что такое Resource и какие атрибуты в нём обязательны?
  5. Что такое semantic conventions?
  6. Что такое W3C TraceContext и какие headers в нём?
  7. Что такое Baggage и в чём опасности?
  8. Что такое SpanKind (SERVER, CLIENT, INTERNAL, PRODUCER, CONSUMER)?
  9. Чем BatchSpanProcessor отличается от SimpleSpanProcessor?
  10. Чем head-based отличается от tail-based sampling?
  11. Что такое ParentBased sampler?
  12. Что такое exemplars и зачем они нужны?
  13. Что такое OTLP gRPC vs HTTP?
  14. Какие два уровня OTel Collector (agent + gateway)?
  15. Что такое connector в Collector (с 1.0+)?
  16. Что такое spanmetrics connector?
  17. Как настроить tail_sampling в Collector?
  18. Чем k8sattributes processor помогает?
  19. Что такое memory_limiter processor и почему он критичен?
  20. Как интегрировать slog с OTel?
  21. Почему trace_id в логах важен?
  22. Что такое cardinality bomb и как её избежать?
  23. Какие auto-instrumentation библиотеки есть для Go?
  24. Как считаются stable cost OTel в production?
  25. Как сделать http.route правильным (без ID)?
  26. Что такое OTel logs API и в чём отличие от slog/zap?
  27. Какие популярные backends (open-source + commercial)?
  28. Как тестировать OTel-инструментацию локально?
  29. Что такое continuous profiling и связь с OTel?
  30. Как сделать graceful shutdown OTel SDK (flush before exit)?

  1. Setup. Инициализируйте OTel SDK с tracer/meter/logger, выгрузите в локальный Collector → Tempo + Mimir + Loki (Grafana stack).
  2. Auto-instrument HTTP. Поднимите net/http API с otelhttp.NewHandler, посмотрите traces в Tempo.
  3. Manual span. Добавьте custom span внутри handler с attributes.
  4. Database. Подключите Postgres через otelsql, проверьте db.statement в трейсе.
  5. Metric exemplars. Сделайте histogram с exemplars, кликните из Grafana metric → trace.
  6. Head sampling. Настройте TraceIDRatioBased(0.05), проверьте, что сэмплируется 5%.
  7. Tail sampling. Сконфигурируйте Collector с tail_sampling: keep-errors + keep-slow + 1%, проверьте на нагрузке.
  8. slog + trace_id. Сделайте slog handler, добавляющий trace_id, проверьте корреляцию в Loki.
  9. spanmetrics. Включите connector spanmetrics, посмотрите RED metrics в Mimir без explicit instrument.
  10. Cardinality test. Положите user_id в attribute, посмотрите, как растёт количество series в Prometheus, верните обратно.

  1. OpenTelemetry docs — главная.
  2. OpenTelemetry Specification.
  3. Semantic Conventions.
  4. Go SDK godoc.
  5. OTel contrib (auto-instrumentation).
  6. OpenTelemetry Collector docs.
  7. OTel Collector contrib (processors/receivers/exporters).
  8. W3C TraceContext.
  9. W3C Baggage.
  10. “Observability Engineering” (Charity Majors et al.) — O’Reilly.
  11. Grafana Tempo — backend.
  12. SigNoz — open-source full-stack.
  13. Honeycomb learn — статьи про observability.
  14. Cloudflare blog: observability.