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 не пройдёт.
Содержание
Заголовок раздела «Содержание»- Концепция: архитектура OTel
- Production-deep dive: SDK, Collector, sampling, exemplars, slog integration
- Gotchas (10+)
- Real cases: Cloudflare, Lyft, FAANG observability
- Вопросы (30+)
- Practice
- Источники
1. Концепция
Заголовок раздела «1. Концепция»1.1 Что такое OpenTelemetry
Заголовок раздела «1.1 Что такое OpenTelemetry»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).
1.2 Архитектура
Заголовок раздела «1.2 Архитектура» ┌────────────────────────────────────────────────────────────┐ │ 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 │ └────────────────────────────────────────────────────────────┘1.3 Signals
Заголовок раздела «1.3 Signals»- Traces — series spans, формирующие dependency граф.
- Metrics — counters, histograms, gauges, sum, observable*.
- Logs — structured logs с trace_id (correlated).
1.4 Semantic Conventions
Заголовок раздела «1.4 Semantic Conventions»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 = 200url.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-ответы», не зная конкретных сервисов.
1.5 Context Propagation
Заголовок раздела «1.5 Context Propagation»Между сервисами 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=euBaggage — это произвольные ключ-значения, путешествующие с 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{},))2. Production-deep dive
Заголовок раздела «2. Production-deep dive»2.1 SDK инициализация
Заголовок раздела «2.1 SDK инициализация»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}2.2 Manual tracing patterns
Заголовок раздела «2.2 Manual tracing patterns»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.
2.3 Auto-instrumentation
Заголовок раздела «2.3 Auto-instrumentation»Готовые 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 — буквально все популярные библиотеки.
2.4 Metrics
Заголовок раздела «2.4 Metrics»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.
2.5 Sampling
Заголовок раздела «2.5 Sampling»Head-based (в SDK)
Заголовок раздела «Head-based (в SDK)»Решение о сэмпле принимается в начале 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-based (в Collector)
Заголовок раздела «Tail-based (в Collector)»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.
2.6 Exemplars
Заголовок раздела «2.6 Exemplars»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+).
2.7 OTLP protocol
Заголовок раздела «2.7 OTLP protocol»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, }),)2.8 OTel Collector в production
Заголовок раздела «2.8 OTel Collector в production»Двухуровневая 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 (1.0+)
Заголовок раздела «Connectors (1.0+)»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]2.9 Slog + OTel integration
Заголовок раздела «2.9 Slog + OTel integration»С 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)}2.10 Cost considerations
Заголовок раздела «2.10 Cost considerations»| Объём | Стоимость |
|---|---|
| 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.
2.11 Backends
Заголовок раздела «2.11 Backends»| Backend | Тип | Особенность |
|---|---|---|
| Tempo | open-source | Grafana, S3/GCS backend, traces only |
| Mimir | open-source | Grafana, scaled Prometheus storage |
| Loki | open-source | Grafana, logs |
| Jaeger | open-source | классика, поддерживает OTLP |
| SigNoz | open-source | full stack, ClickHouse |
| Uptrace | open-source | ClickHouse |
| Datadog | commercial | full stack, дорого |
| Honeycomb | commercial | best-in-class trace exploration |
| Lightstep | commercial | сейчас часть ServiceNow |
| New Relic | commercial | |
| Splunk Observability | commercial |
2.12 Production Collector configuration (полный пример)
Заголовок раздела «2.12 Production Collector configuration (полный пример)»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 }2.13 OTel Logs API (Stable)
Заголовок раздела «2.13 OTel Logs API (Stable)»С 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.Recordrec.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.
2.14 Continuous profiling в OTel
Заголовок раздела «2.14 Continuous profiling в OTel»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, },})3. Gotchas
Заголовок раздела «3. Gotchas»Gotcha 1: ⚠️ Cardinality bomb в metric attributes
Заголовок раздела «Gotcha 1: ⚠️ Cardinality bomb в metric attributes»Если положить user_id в attribute — каждое уникальное значение создаёт новую time series. 1M пользователей → 1M серий → ваш Prometheus умер. Правило: ≤100 уникальных значений на attribute, используйте route (/users/:id), а не URL (/users/42).
Gotcha 2: ⚠️ Sampling в SDK не помогает после export
Заголовок раздела «Gotcha 2: ⚠️ Sampling в SDK не помогает после export»Если head-sampling 1%, но BatchSpanProcessor всё равно стоит — он только не добавит в очередь. Это OK. Но если используете SimpleSpanProcessor — он экспортит синхронно, медленно.
Gotcha 3: ⚠️ Утечка контекста
Заголовок раздела «Gotcha 3: ⚠️ Утечка контекста»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 висит в памяти. Утечка.
Gotcha 5: ⚠️ Baggage может протечь к downstream
Заголовок раздела «Gotcha 5: ⚠️ Baggage может протечь к downstream»Если кладёте internal_user_email в Baggage — он отправится в каждый downstream-сервис, включая third-party (если вы вызываете chargesheets API). Утечка PII.
Gotcha 6: ⚠️ Tail sampling требует памяти
Заголовок раздела «Gotcha 6: ⚠️ Tail sampling требует памяти»Collector держит все spans trace в памяти decision_wait (30s). При 10k RPS × 10 spans/req × 30s = 3M spans в буфере. OOM. Регулируйте num_traces и масштабируйте collector-pods.
Gotcha 7: ⚠️ Sampling consistency между сервисами
Заголовок раздела «Gotcha 7: ⚠️ Sampling consistency между сервисами»Если A сэмплит 10% по trace_id, и B сэмплит 10% по trace_id, и оба используют TraceIDRatioBased с одинаковым hash — они выберут одни и те же trace_ids. Если разные алгоритмы — получите половинные траectories, ад для дебага.
Gotcha 8: ⚠️ http.route не auto-detect в gorilla mux / chi
Заголовок раздела «Gotcha 8: ⚠️ http.route не auto-detect в gorilla mux / chi»otelhttp.NewHandler ставит span name по r.URL.Path — содержит ID! Используйте otelhttp.WithSpanNameFormatter чтобы взять route pattern из mux’а (chi: chi.RouteContext(r.Context()).RoutePattern()).
Gotcha 9: ⚠️ Metric histogram buckets неправильные
Заголовок раздела «Gotcha 9: ⚠️ Metric histogram buckets неправильные»Бакеты по умолчанию ([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.
Gotcha 11: ⚠️ OTLP gRPC vs HTTP — разные ports
Заголовок раздела «Gotcha 11: ⚠️ OTLP gRPC vs HTTP — разные ports»4317 (gRPC) ≠ 4318 (HTTP). Перепутали — клиент не получит ничего, тихо.
Gotcha 12: ⚠️ Logger creation cost
Заголовок раздела «Gotcha 12: ⚠️ Logger creation cost»Если каждый запрос создаёт slog.New(otelslog.NewHandler(...)) — это аллокация и атрибуты не унифицированы. Создавайте один раз в init, используйте WithGroup для контекстных групп.
4. Real cases
Заголовок раздела «4. Real cases»4.1 Cloudflare
Заголовок раздела «4.1 Cloudflare»Cloudflare обрабатывает миллионы RPS. Observability — внутренние системы (Quicksilver, internal log pipeline) + OTel для customer-facing аналитики. Tail sampling на edge, агрегации на uberscale.
4.2 Lyft
Заголовок раздела «4.2 Lyft»Lyft был пионером observability. От statsd/Datadog к собственному OTel-стеку. Public talks от Lyft на CNCF events.
4.3 Honeycomb
Заголовок раздела «4.3 Honeycomb»Honeycomb — backend с уникальным подходом «high cardinality OK». Их подход: «не сэмплируйте, всё храните и stream-аналитика». Идея — observation, не aggregation.
4.4 Большой ритейл (Black Friday)
Заголовок раздела «4.4 Большой ритейл (Black Friday)»Реальный случай. Pre-Black-Friday включили tail sampling на gateway collector. Decision-wait 60s, num_traces 1M, кип все 5xx и >500ms. Объём traces упал с 12TB/день до 800GB/день; всё «больное» осталось. Cost saving 90%.
5. Вопросы
Заголовок раздела «5. Вопросы»- Что такое OpenTelemetry и какие CNCF-проекты он заменил?
- Какие три signals в OTel?
- Чем API отличается от SDK?
- Что такое Resource и какие атрибуты в нём обязательны?
- Что такое semantic conventions?
- Что такое W3C TraceContext и какие headers в нём?
- Что такое Baggage и в чём опасности?
- Что такое SpanKind (SERVER, CLIENT, INTERNAL, PRODUCER, CONSUMER)?
- Чем
BatchSpanProcessorотличается отSimpleSpanProcessor? - Чем head-based отличается от tail-based sampling?
- Что такое
ParentBasedsampler? - Что такое exemplars и зачем они нужны?
- Что такое OTLP gRPC vs HTTP?
- Какие два уровня OTel Collector (agent + gateway)?
- Что такое connector в Collector (с 1.0+)?
- Что такое spanmetrics connector?
- Как настроить tail_sampling в Collector?
- Чем
k8sattributesprocessor помогает? - Что такое
memory_limiterprocessor и почему он критичен? - Как интегрировать slog с OTel?
- Почему trace_id в логах важен?
- Что такое cardinality bomb и как её избежать?
- Какие auto-instrumentation библиотеки есть для Go?
- Как считаются stable cost OTel в production?
- Как сделать
http.routeправильным (без ID)? - Что такое OTel logs API и в чём отличие от slog/zap?
- Какие популярные backends (open-source + commercial)?
- Как тестировать OTel-инструментацию локально?
- Что такое continuous profiling и связь с OTel?
- Как сделать graceful shutdown OTel SDK (flush before exit)?
6. Practice
Заголовок раздела «6. Practice»- Setup. Инициализируйте OTel SDK с tracer/meter/logger, выгрузите в локальный Collector → Tempo + Mimir + Loki (Grafana stack).
- Auto-instrument HTTP. Поднимите net/http API с
otelhttp.NewHandler, посмотрите traces в Tempo. - Manual span. Добавьте custom span внутри handler с attributes.
- Database. Подключите Postgres через
otelsql, проверьтеdb.statementв трейсе. - Metric exemplars. Сделайте histogram с exemplars, кликните из Grafana metric → trace.
- Head sampling. Настройте
TraceIDRatioBased(0.05), проверьте, что сэмплируется 5%. - Tail sampling. Сконфигурируйте Collector с tail_sampling: keep-errors + keep-slow + 1%, проверьте на нагрузке.
- slog + trace_id. Сделайте slog handler, добавляющий trace_id, проверьте корреляцию в Loki.
- spanmetrics. Включите connector spanmetrics, посмотрите RED metrics в Mimir без explicit instrument.
- Cardinality test. Положите user_id в attribute, посмотрите, как растёт количество series в Prometheus, верните обратно.
7. Источники
Заголовок раздела «7. Источники»- OpenTelemetry docs — главная.
- OpenTelemetry Specification.
- Semantic Conventions.
- Go SDK godoc.
- OTel contrib (auto-instrumentation).
- OpenTelemetry Collector docs.
- OTel Collector contrib (processors/receivers/exporters).
- W3C TraceContext.
- W3C Baggage.
- “Observability Engineering” (Charity Majors et al.) — O’Reilly.
- Grafana Tempo — backend.
- SigNoz — open-source full-stack.
- Honeycomb learn — статьи про observability.
- Cloudflare blog: observability.