Зачем OpenTelemetry
До OTel у каждого vendor свой SDK:
- Prometheus client для metrics
- Jaeger / Zipkin client для traces
- Datadog APM agent для всего, но vendor lock
- structured logger для logs
4 SDK, 4 пайплайна, 4 формата. Поменять backend = переписать всю instrumentation в коде.
OpenTelemetry (CNCF, 2019, объединение OpenCensus + OpenTracing) даёт:
- Один SDK на все три signals (metrics+traces+logs)
- Один протокол OTLP (OpenTelemetry Line Protocol)
- Один Collector для трансформации/роутинга
- Vendor-neutral: код не знает куда летит, в Prom, Datadog или cloud monitoring. Меняй backend через config.
В 2025 OTel, де-факто стандарт для новых проектов. Old Prometheus client всё ещё работает (OTel Collector умеет принимать), но новый код пишут на OTel.
Три signal'а
Traces (tracing-basics), request flow через сервисы. Span'ы
с parent-child связями, context propagation через
[[http2-internals|traceparent header]].
Metrics, counter, gauge, histogram. Семантика та же что в [[metric-types|Prometheus]], но через OTel SDK.
Logs, structured events с автоматической correlation: trace_id и span_id вшиваются в log record.
Все три едут одним SDK через один OTLP-канал. Это уменьшает coupling и даёт coherence.
Архитектура
┌──────────────┐ OTLP gRPC :4317 ┌─────────────┐
│ App │ ──────────────────────► │ Collector │
│ ┌──────────┐ │ OTLP HTTP :4318 │ │
│ │ OTel SDK │ │ │ ┌─────────┐ │
│ │ ┌──────┐ │ │ │ │receivers│ │
│ │ │tracer│ │ │ │ ├─────────┤ │
│ │ │meter │ │ │ │ │processor│ │
│ │ │logger│ │ │ │ ├─────────┤ │
│ │ └──────┘ │ │ │ │exporters│ │
│ └──────────┘ │ │ └────┬────┘ │
└──────────────┘ └──────┼──────┘
│
┌─────────┬───────┼────────┬────────┐
▼ ▼ ▼ ▼ ▼
Prometheus Tempo Loki Jaeger Datadog
OTLP, протокол
OTLP, единый wire-format. Два транспорта:
| Транспорт | Порт (default) | Когда |
|---|---|---|
| gRPC | 4317 | server-to-server, internal, low-latency |
| HTTP/protobuf | 4318 | через прокси, browser, restrictive net |
Payload, Protocol Buffers. Структура:
ResourceSpans
├── Resource (service.name, host.name, k8s.pod.name)
└── ScopeSpans
├── InstrumentationScope (library name + version)
└── Span[]
├── trace_id, span_id, parent_span_id
├── name, start_time_nano, end_time_nano
├── attributes (key-value)
├── events[]
├── links[]
└── status (OK / ERROR)
Аналогично для metrics (ResourceMetrics → ScopeMetrics → Metric) и
logs (ResourceLogs → ScopeLogs → LogRecord).
Преимущества над Prom format:
- Binary, компактнее в 3-5×
- Streaming через [[grpc-basics|gRPC]] (без http poll)
- Один формат для трёх signal'ов
SDK: auto vs manual instrumentation
Auto-instrumentation, agent патчит библиотеки runtime, без изменений кода:
- Java:
-javaagent:opentelemetry-javaagent.jar, патчит JDBC, Servlet, Kafka client, gRPC, ~120 библиотек - Python:
opentelemetry-instrument python app.py, патчит requests, Flask, Django, psycopg2, redis-py - Node.js:
--require @opentelemetry/auto-instrumentations-node - Go: нет рефлексии → нужно добавить вручную (eBPF-based agent в работе)
- .NET:
OTEL_DOTNET_AUTO_HOMEenv
Получаешь traces для HTTP/DB/Kafka без единой строки кода. Дальше можно дописать manual span'ы для бизнес-логики.
Manual instrumentation, explicit API:
from opentelemetry import trace, metrics
tracer = trace.get_tracer(__name__)
meter = metrics.get_meter(__name__)
request_counter = meter.create_counter("requests")duration_histogram = meter.create_histogram("request_duration_ms")@app.get("/checkout")def checkout():
with tracer.start_as_current_span("checkout") as span: span.set_attribute("user.id", user_id) request_counter.add(1, {"endpoint": "/checkout"})# ... business logic
OTel Collector
Standalone сервис, deploy в каждом узле (DaemonSet) или gateway per-cluster.
Конфиг, три секции:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
scrape_configs:
- job_name: app
static_configs:
- targets: [app:8080]
processors:
batch: # batches before exporting
send_batch_size: 8192
timeout: 200ms
memory_limiter: # backpressure
limit_mib: 512
tail_sampling: # sample by trace condition
policies:
- name: errors
type: status_code
status_code: {status_codes: [ERROR]}- name: slow
type: latency
latency: {threshold_ms: 1000}- name: probabilistic-1pct
type: probabilistic
probabilistic: {sampling_percentage: 1}exporters:
prometheusremotewrite:
endpoint: http://victoriametrics:8480/api/v1/write
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
loki:
endpoint: http://loki:3100/loki/api/v1/push
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]
Pipeline, ациклический граф. Один collector может обслуживать все три signal'а независимо.
Resource, что описывает источник
Resource, атрибуты процесса/хоста, общие для всех signal'ов:
service.name=checkout
service.version=1.4.2
service.instance.id=checkout-7f8b9c
k8s.namespace.name=prod
k8s.pod.name=checkout-7f8b9c-q2lx9
host.name=node-12.us-east
cloud.provider=aws
cloud.region=us-east-1
Установить через env:
OTEL_SERVICE_NAME=checkout
OTEL_RESOURCE_ATTRIBUTES=service.version=1.4.2,deployment.environment=prod
Auto-injected в k8s через [[opentelemetry-operator-k8s|OTel Operator]] (sidecar/auto-instrumentation CRD).
OTel vs Prometheus client
| Аспект | Prom client | OTel SDK |
|---|---|---|
| Signals | metrics only | metrics+traces+logs |
| Transport | HTTP pull (/metrics) | OTLP push |
| Vendor neutrality | Prom-only | любой backend |
| Auto-instrumentation | minimal | полный |
| Adoption | широчайшая | растёт быстро |
| Wire format | text/OpenMetrics | protobuf |
Можно совместить: OTel SDK для traces+logs + Prom client для metrics. Или OTel SDK для всего, а Collector экспортит metrics в Prom format.
Sampling, head vs tail
100% traces неподъёмны: 10K req/s × 5 spans × 5KB = ~250 MB/s. Sampling нужен.
- Head-based: решение sample-or-drop в начале трассы (на edge), все downstream спаны соблюдают. Просто и предсказуемо. Минус рандомно роняет error traces.
- Tail-based: собрать всю трассу в Collector, потом решить keep/drop по trace properties (status, latency, attributes). Видим все ошибки, но Collector держит все spans в памяти на 5-30s.
Tail-based предпочтительно. Tail Sampling Processor в Collector.
Когда что-то пошло не так
OTLP/gRPC connection refused, Collector не запущен или другой port. Default 4317. Проверь firewall.- Trace отсутствует, хотя ошибка, head sampling 1% уронил.
Используй tail sampling с
status_code: ERRORpolicy. - Collector OOM,
memory_limiterprocessor отсутствует или лимит выше RAM. Добавь limit < 80% от container memory. - Cardinality explosion,
attributesв metrics с user-id или request-id. См. cardinality-explosion. - Auto-instrumentation сломала app, обычно Java agent +
конфликт байткод-патча. Поднимай OTel agent версию или disable
конкретный instrumentation:
OTEL_INSTRUMENTATION_<name>_ENABLED=false. - Span.attributes теряются, забыт
batchprocessor, или attributes добавлены послеend(). Set до .end(). - Service.name = "unknown_service" в Tempo, забыт env
OTEL_SERVICE_NAME. Resource не сконфигурен.
OTel vs Datadog/New Relic
Vendor APMs (Datadog, New Relic, Splunk) дают всё-в-одном с UI и ML-features. Но vendor lock, заменить = переписать.
С OTel пишешь instrumentation один раз, отправляешь в Datadog через native receiver. Через год, переключаешь exporter на Tempo/Loki/Mimir, код не трогаешь.
Cost-aware: OTel + self-hosted (Tempo+Loki+VictoriaMetrics) дешевле Datadog в 5-10× при ≥100 GB/day, но требует ops-инвестиций.