Tracing distribuído com OpenTelemetry: rastreamento de requests entre serviços

1. Fundamentos do Tracing Distribuído

O tracing distribuído é uma técnica essencial para entender o fluxo de requisições em arquiteturas de microsserviços. Diferente de logs (eventos discretos) e métricas (agregações numéricas), o tracing captura a jornada completa de uma requisição através de múltiplos serviços.

Conceitos fundamentais:

  • Trace: Representa uma requisição completa, do início ao fim, abrangendo todos os serviços envolvidos.
  • Span: Unidade atômica de trabalho dentro de um trace. Cada span contém nome, duração, atributos e referência ao span pai.
  • Contexto de propagação: Mecanismo que transporta o identificador do trace entre serviços via headers HTTP, gRPC metadata ou mensagens.

A diferença crucial entre tracing, logging e métricas está no escopo: logs são eventos pontuais, métricas são agregações estatísticas, enquanto traces fornecem a visão因果 completa de uma requisição individual.

Desafios em microsserviços:
- Sincronização de relógios entre serviços
- Propagação confiável de contexto através de fronteiras de rede
- Volume massivo de dados gerados em sistemas de alta throughput

2. Arquitetura do OpenTelemetry para Tracing

OpenTelemetry (OTel) é o padrão CNCF para observabilidade, oferecendo uma arquitetura modular e vendor-neutral.

Componentes principais:

[Aplicação] → [SDK OTel] → [Exporter] → [Collector OTel] → [Backend]
  • API: Define interfaces para criar spans, adicionar atributos e propagar contexto.
  • SDK: Implementação concreta da API, com gerenciamento de spans e sampling.
  • Collector: Pipeline de dados que recebe, processa e exporta traces para múltiplos backends.
  • Exporters: Conectores para Jaeger, Zipkin, Grafana Tempo, Prometheus, entre outros.

Propagação de contexto segue o padrão W3C Trace Context, utilizando headers HTTP como traceparent e tracestate:

Header: traceparent
Formato: 00-{trace_id}-{span_id}-{trace_flags}
Exemplo: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

Sampling estratégico:
- Head-based sampling: Decisão tomada no início do trace, antes da execução. Simples, mas pode perder traces raros.
- Tail-based sampling: Decisão após o trace completo, permitindo preservar traces de erro ou alta latência.

3. Instrumentação Automática vs Manual

Instrumentação automática utiliza agentes ou bibliotecas que interceptam chamadas a frameworks populares:

# Exemplo com Python Flask (instrumentação automática)
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap --action=install
OTEL_SERVICE_NAME="meu-servico" \
OTEL_EXPORTER_OTLP_ENDPOINT="http://collector:4318" \
python app.py

Instrumentação manual oferece controle granular sobre spans e atributos:

from opentelemetry import trace
from opentelemetry.trace import SpanKind

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("processar_pagamento", kind=SpanKind.SERVER) as span:
    span.set_attribute("valor.total", 150.00)
    span.set_attribute("metodo.pagamento", "cartao_credito")

    # Criar span filho para suboperação
    with tracer.start_as_current_span("validar_cartao") as child_span:
        child_span.set_attribute("bandeira", "visa")
        # lógica de validação

Propagação entre serviços:

# Serviço A (produtor HTTP)
from opentelemetry import propagate
from opentelemetry.propagators import inject

headers = {}
propagate.inject(headers)
# headers agora contém 'traceparent' e 'tracestate'
requests.post("http://servico-b/api", headers=headers)

# Serviço B (consumidor HTTP)
from opentelemetry.propagators import extract

context = extract(request.headers)
tracer.start_span("processar_requisicao", context=context)

4. Rastreamento de Requests Multi-serviço

Considere um fluxo completo: API Gateway → Serviço de Autenticação → Serviço de Pedidos → Banco de Dados.

Estrutura de spans:

Trace ID: abc123
├── Span: "api_gateway.handler" (root)
│   ├── Span: "autenticacao.validar_token"
│   │   └── Span: "redis.get" (cache de token)
│   └── Span: "pedidos.criar_pedido"
│       ├── Span: "pedidos.validar_estoque"
│       │   └── Span: "mysql.query" (consulta estoque)
│       └── Span: "pedidos.processar_pagamento"
│           └── Span: "http.post" (gateway de pagamento externo)

Relação causal é estabelecida via parent-child spans, onde o span pai contém o span_id referenciado pelo filho.

Propagação de baggage permite transportar atributos contextuais:

# Injetar baggage
from opentelemetry.baggage import set_baggage
set_baggage("user.id", "12345")

# Extrair em serviço downstream
from opentelemetry.baggage import get_baggage
user_id = get_baggage("user.id")

5. Configuração do Collector OpenTelemetry

O Collector atua como middleware de observabilidade, recebendo dados de múltiplas fontes e exportando para diversos backends.

Pipeline básico:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
  attributes:
    actions:
      - key: environment
        value: production
        action: upsert

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8889

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch, attributes]
      exporters: [jaeger, prometheus]

Filtros e amostragem no Collector:

processors:
  tail_sampling:
    decision_wait: 30s
    policies:
      - name: error-sampling
        type: status_code
        config:
          status_codes: [ERROR]
      - name: latency-sampling
        type: latency
        config:
          threshold_ms: 500

6. Análise de Performance com Tracing

Visualização em waterfall chart (Jaeger UI):

Serviço A (15ms)  ████████████████
  ├── Serviço B (8ms)   ████████
  │     └── DB (3ms)    ███
  └── Serviço C (5ms)   █████
      └── Cache (1ms)   █

Identificação de gargalos:

  • Caminho crítico: Soma das durações no caminho mais longo do trace.
  • Latência por span: Spans com duração anormalmente alta indicam problemas.
  • Taxa de erro: Spans com status code ERROR indicam falhas.

Métricas derivadas de traces:

# Exemplo de métricas via OpenTelemetry Metrics
from opentelemetry import metrics

meter = metrics.get_meter(__name__)
latency_histogram = meter.create_histogram(
    "request.latency",
    unit="ms",
    description="Latência das requisições"
)

histogram.record(span.end_time - span.start_time, {
    "service": "pedidos",
    "operation": "criar_pedido"
})

7. Troubleshooting e Debugging com Traces

Correlação trace-log-métrica usando atributos comuns:

# Log estruturado com trace_id
import logging
from opentelemetry import trace

logger = logging.getLogger(__name__)
span = trace.get_current_span()

logger.info("Pedido processado", extra={
    "trace_id": span.get_span_context().trace_id,
    "span_id": span.get_span_context().span_id,
    "order_id": "ORD-12345"
})

Exemplo prático: Debugging de timeout em cascata

Cenário: Requisição ao Serviço de Pedidos leva 10 segundos, causando timeout no API Gateway.

Trace ID: def456
API Gateway (span: "handler.pedidos") - duração: 5000ms (timeout!)
└── Serviço Pedidos (span: "criar_pedido") - duração: 3000ms
    ├── Serviço Estoque (span: "validar_estoque") - duração: 2500ms
    │   └── DB MySQL (span: "query_estoque") - duração: 2000ms (gargalo!)
    └── Serviço Pagamento (span: "processar_pagamento") - duração: 500ms

Análise: O span query_estoque com 2 segundos indica problema no banco de dados. Ao correlacionar com logs, descobrimos lock de tabela.

Identificação de dependências ocultas:

# Service Graph (Jaeger)
Serviço A → Serviço B → Serviço C → DB
                 ↓
           Serviço D (dependência não documentada!)

8. Boas Práticas e Padrões para Tracing Distribuído

Nomenclatura consistente:

# Padrão recomendado
Span name: "{serviço}.{recurso}.{operação}"
Exemplo: "pedidos.api.criar_pedido"

# Atributos padronizados
span.set_attribute("http.method", "POST")
span.set_attribute("http.status_code", 200)
span.set_attribute("db.system", "postgresql")
span.set_attribute("db.statement", "SELECT * FROM orders WHERE id = ?")

Gerenciamento de cardinalidade:

  • Evite atributos com alta cardinalidade (ex: IDs de usuário individuais)
  • Use baggage para dados contextuais, mas limite a 10-20 atributos por span
  • Configure sampling adaptativo para reduzir volume em picos de tráfego

Privacidade e segurança:

# Sanitização de dados sensíveis no Collector
processors:
  attributes:
    actions:
      - key: db.statement
        action: hash
      - key: http.request.body
        action: delete
      - key: user.email
        action: redact

Padrões recomendados:
- Sempre propagar contexto, mesmo em cenários assíncronos (filas, eventos)
- Definir SLOs baseados em percentis de latência derivados de traces
- Utilizar tags de ambiente, versão e região para filtrar traces em troubleshooting

Referências