🧠FFVAcademy
🛰️

OpenTelemetry end-to-end: instrumentação app → backend

18 min de leitura·+90 XP
Pré-requisitos (0/1)0%

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.)
ComponenteO que fazOnde roda
SDKGera spans/metrics/logs na sua appDentro do processo da aplicação
CollectorRecebe via OTLP, transforma, exporta pra backendsSidecar, daemonset (por node) ou gateway (cluster separado)
BackendArmazena, indexa, permite queryManaged (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:

javascript
// 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();
bash
# 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:

bash
# 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
python
# 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}
💡
Regra de ouro: use auto-instrumentation pra infra comum (HTTP server/client, DB, Redis, Kafka, gRPC). Adicione manual spans pra lógica de negócio que importa no debugging ("processPayment", "generateInvoice", "runEval"). Nomeie spans semanticamente — eles aparecem no trace como operação nomeada.

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ínioAtributo padrãoExemplo
HTTP serverhttp.request.method, http.response.status_code, url.pathGET, 200, /api/users
HTTP clientserver.address, server.port, http.urlapi.stripe.com, 443
Databasedb.system, db.name, db.statementpostgresql, myapp, SELECT ...
Messagingmessaging.system, messaging.destination.name, messaging.message.idkafka, orders, abc123
RPCrpc.system, rpc.service, rpc.methodgrpc, UserService, GetUser
Exceptionexception.type, exception.message, exception.stacktraceValueError, ...
⚠️
Não invente nomes próprios pra coisas já padronizadas. "url.path" é a convenção OTel; "request_path", "http_path" e "endpoint" são não-padrão e quebram dashboards/queries pré-construídos. Use vocabulário oficial; adicione atributos custom (com prefixo da sua empresa) pra business-specific.

OTel Collector: o proxy de telemetria

Collector é um binário Go que você roda como agent (sidecar/daemon) ou gateway. Pipeline: receivers → processors → exporters.

yaml
# 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]
💡
Por que agent + gateway? Agent (sidecar ou daemonset) tá próximo da app, faz buffering local se a rede falhar. Gateway (cluster central) consolida, aplica políticas corporativas (sampling, PII, routing), é o ponto único de contato com backends. Arquitetura típica de produção séria.

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:

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 context

OTel SDK injeta esses headers no outbound HTTP e extrai no inbound, automaticamente (via auto-instrumentation). Pra libs não suportadas, injete manual:

python
# 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.

TipoQuando decidePrósContras
Head sampling (probabilistic)No início (primeiro span) — N%Barato, decisão local, consistentePode perder erros raros; não conhece duration no início
Tail sampling (rule-based)Depois do trace completar, baseado em propriedadesGuarda todos os erros, traces lentos, amostra o restoRequer Collector bufferizar, mais memória e latência
Ratio-based (TraceIdRatioBased)Head, baseado em hash(trace_id) < ratioConsistente entre serviços — todo serviço do mesmo trace decide igualMesmas limitações do head
yaml
# 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 }
💡
Regra prática: em prod, use tail sampling pra manter 100% de erros e outliers + 1-5% baseline. Reduz custo ~20x com zero perda de visibilidade no que importa. Honeycomb popularizou isso; Tempo, Datadog e Grafana suportam.

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:

javascript
// 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?

Auto-instrumentation + Collector sidecar + backend managed

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 com receiver jaeger, app segue emitindo Jaeger, depois migre SDK gradual

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 migrationRisco de perder visibilidade durante.

📋 Alta volumetria (10k+ RPS) — sampling strategy

Tail sampling com 100% errors/slow + 1-5% baseline

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.

Take-aways:
  • 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

Continue lendo