OpenTelemetry end-to-end: instrumentação app → backend
- ⬜📉 Métricas RED e USE: os frameworks que cobrem 90% dos casos(Observabilidade & SRE)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
OpenTelemetry (OTel) é a maior vitória recente da observability — padrão aberto da CNCF pra gerar e exportar telemetria sem depender de vendor. Antes dele: cada backend (Datadog, New Relic, Jaeger) tinha SDK próprio, trocar era reescrever instrumentação. Depois dele: instrumente uma vez, exporte pra qualquer backend que aceite OTLP.
Este módulo é o guia prático de ponta a ponta: SDK (auto + manual), Collector (receivers, processors, exporters), context propagation, resource detection, e pipelines reais em Node/Python/Go. É o conhecimento que todo engenheiro sério precisa em 2026.
Arquitetura OTel: os 3 componentes
┌──────────────┐ ┌───────────────────┐ ┌─────────────────┐
│ SDK │ OTLP │ Collector │ OTLP │ Backend(s) │
│ (na sua app) │ ───────► │ (proxy/transform) │ ───────► │ Jaeger / Tempo │
│ │ grpc/ │ │ grpc/ │ Prometheus / │
│ auto+manual │ http │ receivers │ http │ Datadog / ... │
│ instrumenta- │ │ processors │ │ │
│ ção │ │ exporters │ │ │
└──────────────┘ └───────────────────┘ └─────────────────┘
3 signals: │
├─ Traces (spans) │
├─ Metrics (counter, gauge, histogram) │
└─ Logs (opcional via OTel Logs SDK, ainda beta em alguns langs)
│
Query / Dashboard
(Grafana, etc.)| Componente | O que faz | Onde roda |
|---|---|---|
| SDK | Gera spans/metrics/logs na sua app | Dentro do processo da aplicação |
| Collector | Recebe via OTLP, transforma, exporta pra backends | Sidecar, daemonset (por node) ou gateway (cluster separado) |
| Backend | Armazena, indexa, permite query | Managed (Datadog) ou self-host (Tempo/Jaeger/Loki) |
OTel SDK: auto vs manual instrumentation
O SDK tem 2 camadas: auto-instrumentation (plug-and-play pra libs populares) e manual (spans customizados pra lógica de negócio).
Node.js — auto-instrumentation:
// instrumentation.js — rodado antes do app inicializar
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'checkout-api',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION,
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV,
}),
traceExporter: new OTLPTraceExporter({ url: 'http://otel-collector:4317' }),
metricExporter: new OTLPMetricExporter({ url: 'http://otel-collector:4317' }),
instrumentations: [getNodeAutoInstrumentations({
// Remove instrumentação barulhenta se precisar
'@opentelemetry/instrumentation-fs': { enabled: false },
})],
});
sdk.start();# Rode com a instrumentação carregada ANTES da app
node --require ./instrumentation.js server.js
# Ou (mais recente) via package.json:
# "scripts": { "start": "node --import ./instrumentation.js server.js" }Python — auto + manual:
# Instala SDK + auto-instrumentations pra libs populares pip install opentelemetry-distro opentelemetry-exporter-otlp opentelemetry-bootstrap -a install # Rode a app embrulhada OTEL_SERVICE_NAME=checkout-api \ OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \ OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production \ opentelemetry-instrument python app.py
# app.py — spans manuais pra lógica de negócio
from opentelemetry import trace, metrics
tracer = trace.get_tracer(__name__)
meter = metrics.get_meter(__name__)
orders_counter = meter.create_counter(
"orders_created_total",
description="Número de pedidos criados",
)
async def create_order(data: dict):
with tracer.start_as_current_span("create_order") as span:
span.set_attribute("user.id", data["user_id"])
span.set_attribute("cart.item_count", len(data["items"]))
span.set_attribute("cart.total_cents", data["total_cents"])
# Sub-span pra lógica de validação
with tracer.start_as_current_span("validate_inventory"):
await validate_inventory(data["items"])
# Sub-span pra payment
with tracer.start_as_current_span("charge_payment") as pay_span:
try:
result = await charge(data["user_id"], data["total_cents"])
pay_span.set_attribute("payment.gateway", result.gateway)
except PaymentError as e:
pay_span.set_status(trace.StatusCode.ERROR, str(e))
pay_span.record_exception(e)
raise
orders_counter.add(1, {"tier": data.get("user_tier", "free")})
return {"id": order_id}Semantic Conventions: vocabulário padrão
OTel padroniza nomes de atributos (semantic conventions) pra que traces entre serviços/vendors sejam comparáveis. Exemplos:
| Domínio | Atributo padrão | Exemplo |
|---|---|---|
| HTTP server | http.request.method, http.response.status_code, url.path | GET, 200, /api/users |
| HTTP client | server.address, server.port, http.url | api.stripe.com, 443 |
| Database | db.system, db.name, db.statement | postgresql, myapp, SELECT ... |
| Messaging | messaging.system, messaging.destination.name, messaging.message.id | kafka, orders, abc123 |
| RPC | rpc.system, rpc.service, rpc.method | grpc, UserService, GetUser |
| Exception | exception.type, exception.message, exception.stacktrace | ValueError, ... |
OTel Collector: o proxy de telemetria
Collector é um binário Go que você roda como agent (sidecar/daemon) ou gateway. Pipeline: receivers → processors → exporters.
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# também aceita formatos alternativos pra migração
jaeger:
protocols:
grpc: { endpoint: 0.0.0.0:14250 }
prometheus:
config:
scrape_configs:
- job_name: 'apps'
static_configs:
- targets: ['app:9090']
processors:
batch: # agrupa antes de exportar — eficiência massiva
timeout: 5s
send_batch_size: 1024
memory_limiter: # evita OOM do Collector sob burst
check_interval: 1s
limit_mib: 500
attributes: # scrub PII / transform attributes
actions:
- key: user.email
action: delete
- key: authorization
action: delete
- key: http.url
action: update
value: "REDACTED"
pattern: "token=[^&]+"
tail_sampling: # amostragem inteligente — explicado abaixo
decision_wait: 10s
policies:
- name: errors-policy
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-policy
type: latency
latency: { threshold_ms: 500 }
- name: random-policy
type: probabilistic
probabilistic: { sampling_percentage: 5 }
exporters:
otlp/tempo:
endpoint: tempo:4317
tls: { insecure: true }
prometheusremotewrite:
endpoint: http://mimir:8080/api/v1/push
loki:
endpoint: http://loki:3100/loki/api/v1/push
service:
pipelines:
traces:
receivers: [otlp, jaeger]
processors: [memory_limiter, attributes, tail_sampling, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, attributes, batch]
exporters: [loki]Context Propagation: W3C Trace Context
Quando Serviço A chama Serviço B, o span de B precisa saber o trace_id/span_id de A pra conectar. OTel usa W3C Trace Context (RFC, 2020) via headers HTTP:
# Request de A pra B
GET /api/items HTTP/1.1
Host: items-service
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ │
│ └─ trace-id (hex, 16 bytes) └─ parent-span flags
└─ version
tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7 # vendor-specific extensions
baggage: user.tier=pro,tenant=acme # application-level contextOTel SDK injeta esses headers no outbound HTTP e extrai no inbound, automaticamente (via auto-instrumentation). Pra libs não suportadas, injete manual:
# manual_propagation.py — para libs sem instrumentação automática
from opentelemetry.propagate import inject, extract
from opentelemetry import context
# ─ outbound: injetar ─
carrier = {}
inject(carrier) # preenche carrier com traceparent, tracestate, baggage
# carrier = {"traceparent": "00-...-01", "tracestate": "..."}
response = await http.post(url, json=data, headers=carrier)
# ─ inbound: extrair ─
ctx = extract(incoming_headers)
with tracer.start_as_current_span("handle", context=ctx):
# span novo virou child do span upstream
...Baggage é diferente: carrega dados de negócio através do trace (ex: tenant_id=acme, user.tier=pro) disponíveis em qualquer span downstream. Use com moderação — cada header pesa na bandwidth.
Sampling: head vs tail, probabilístico vs rule-based
Em volumes altos, armazenar 100% dos traces é caro e desnecessário. Sampling escolhe quais guardar.
| Tipo | Quando decide | Prós | Contras |
|---|---|---|---|
| Head sampling (probabilistic) | No início (primeiro span) — N% | Barato, decisão local, consistente | Pode perder erros raros; não conhece duration no início |
| Tail sampling (rule-based) | Depois do trace completar, baseado em propriedades | Guarda todos os erros, traces lentos, amostra o resto | Requer Collector bufferizar, mais memória e latência |
| Ratio-based (TraceIdRatioBased) | Head, baseado em hash(trace_id) < ratio | Consistente entre serviços — todo serviço do mesmo trace decide igual | Mesmas limitações do head |
# tail_sampling — política inteligente no Collector
processors:
tail_sampling:
decision_wait: 10s # aguarda trace completar
num_traces: 100000 # buffer de traces em memória
policies:
# Sempre guardar traces com erro
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
# Sempre guardar traces lentos
- name: slow
type: latency
latency: { threshold_ms: 1000 }
# Sempre guardar algum endpoint crítico
- name: checkout
type: string_attribute
string_attribute:
key: url.path
values: ["/checkout"]
# Amostra 5% do resto
- name: baseline
type: probabilistic
probabilistic: { sampling_percentage: 5 }Resource Detection: contexto automático
Resource Attributes identificam quem gerou a telemetria — service name, version, environment, kubernetes pod, cloud region. OTel detecta automaticamente se você ativar:
// Node.js — resource detectors
import { Resource } from '@opentelemetry/resources';
import { envDetector, hostDetector, processDetector } from '@opentelemetry/resources';
import { awsEcsDetector, awsEc2Detector } from '@opentelemetry/resource-detector-aws';
import { k8sAttributesDetector } from '@opentelemetry/resource-detector-kubernetes';
const sdk = new NodeSDK({
resourceDetectors: [
envDetector, hostDetector, processDetector,
awsEc2Detector, awsEcsDetector,
k8sAttributesDetector,
],
// ...
});Resultado: todo span automaticamente tem k8s.pod.name, k8s.namespace, cloud.region, etc. Filtrar "traces só do pod X" fica trivial.
Decisões reais
📋 App novo em Node/Python — como começar OTel do zero?
80% do valor vem grátis. SDK auto + Collector como sidecar = ~2 dias pra ter traces end-to-end. Adicione manual spans em business logic conforme identifica gaps. Depois, adicione tail sampling pra reduzir custo.
Alt: SDK vendor-specific (Datadog Tracer, New Relic) — Menos portável; OTel venceu o mercado em 2024.
📋 Migração de Jaeger client legacy pra OTel
Collector aceita Jaeger input e exporta OTLP. Você ganha flexibilidade de backend imediatamente sem tocar app. Depois, migre cada serviço pro OTel SDK no seu tempo.
Alt: Big bang migration — Risco de perder visibilidade durante.
📋 Alta volumetria (10k+ RPS) — sampling strategy
Cobre os traces que importam (problemas) + amostra normal. Reduz storage e custo de backend. Tail requer Collector bufferizado — use memory_limiter e gateway separado do sidecar.
Alt: Head sampling 10% — Mais simples, perde erros raros.
Perguntas típicas (Q&A)
OTel overhead é relevante?
Auto-instrumentation: ~1-3% CPU em apps típicas. Manual spans: desprezível (ns/span). Export síncrono é que pode travar — SEMPRE use exportador batch+async (padrão do SDK).
OTel Logs já tá maduro?
Em Java, Python, .NET: sim. Em Node.js: quase (GA recente). Alternativa: continue com structured logs (JSON) + correlation via trace_id, OTel Collector tem receiver filelog pra coletar. Logs como signal OTel viraram GA em 2023.
Posso ter Collector em múltiplos backends?
Sim — pipelines paralelos. Ex: traces pra Tempo + Datadog (dual-write durante migração), metrics pra Prometheus + Honeycomb. O Collector copia pra cada exporter configurado.
Como lidar com apps legacy que não quero instrumentar?
OTel Collector tem receivers pra sources externos: Prometheus (scrape), Fluentd/Fluent Bit (logs), podman/docker stats, hostmetrics. Você importa pra OTLP sem tocar o app.
Span attribute com dados sensíveis (PII) — o que fazer?
Use processor attributes ou transform no Collector pra delete/redact. Fazer no Collector centraliza políticas — em vez de dependendo de cada app fazer corretamente.
- OpenTelemetry = padrão aberto CNCF pra gerar/exportar telemetria. Mata vendor lock-in.
- SDK em 2 camadas: auto-instrumentation (pra libs) + manual (pra business logic).
- OTel Collector é o proxy: receivers → processors → exporters. Sidecar + Gateway pra produção séria.
- Semantic Conventions: use nomes padrão (http.request.method, db.system). Não invente.
- W3C Trace Context propaga trace_id via headers (traceparent, tracestate, baggage).
- Tail sampling > head sampling em volumes altos — guarda o que importa (erros, slow), amostra o resto.
- Resource detection (K8s, AWS, GCP) atribui contexto automático a todos os sinais.
Próximo módulo: o pilar mais subestimado — logs estruturados bem feitos.
Quiz rápido
4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito