🧠FFVAcademy
🔍

Distributed Tracing: spans, baggage e sampling strategies

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

Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.

Métricas dizem que algo está errado. Logs mostram o que aconteceu num serviço. Traces revelam por que uma requisição foi lenta ou falhou — rastreando o caminho completo por todos os serviços envolvidos. Em sistemas distribuídos com dezenas de microserviços, tracing é a única técnica que dá visibilidade ponta-a-ponta.

Neste módulo você vai entender a estrutura de spans e traces, como o W3C Trace Context propaga contexto por HTTP/gRPC/filas, o que é baggage e quando usar, e a diferença crucial entre head sampling (simples) e tail sampling (inteligente). Ao final, você saberá configurar tracing em serviços reais e escolher entre Jaeger e Grafana Tempo.

Anatomia de um trace: spans e árvore

Um trace é a representação completa de uma requisição distribuída — do momento em que entra no sistema até a resposta. É composto de spans: unidades de trabalho com duração, atributos e relação parent-child.


  Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736
  ────────────────────────────────────────────────────────────────
  ─────── HTTP POST /checkout (api-gateway)            450ms ────
   span_id: a1b2  parent: -  status: OK
    │
    ├─── validate-order (order-service)                 20ms ──
    │    span_id: c3d4  parent: a1b2  status: OK
    │
    ├─── reserve-inventory (inventory-service)          80ms ──────
    │    span_id: e5f6  parent: a1b2  status: OK
    │     │
    │     └─── db.query SELECT inventory (postgres)     75ms ──────
    │          span_id: g7h8  parent: e5f6  db.system=postgresql
    │
    └─── process-payment (payment-service)             340ms ───────────────
         span_id: i9j0  parent: a1b2  status: ERROR
          │
          ├─── stripe-api-call (http.client)            300ms ─────────────
          │    span_id: k1l2  parent: i9j0  http.status=408
          │
          └─── payment-service.compensate               35ms ───
               span_id: m3n4  parent: i9j0  status: OK

  ────────────────────────────────────────────────────────────────
  0ms                                                        450ms
  Waterfall: cada span mostra onde o tempo foi gasto

Cada span contém:

python
# Estrutura de um span OpenTelemetry
{
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",  # 128 bits, único por trace
    "span_id": "i9j0c2d4e6f8a0b2",                  # 64 bits, único por span
    "parent_span_id": "a1b2c3d4e5f6a7b8",            # None = root span
    "name": "process-payment",
    "kind": "SERVER",              # CLIENT | SERVER | PRODUCER | CONSUMER | INTERNAL
    "start_time": 1705329791.350,
    "end_time": 1705329791.690,    # duration = 340ms
    "status": {"code": "ERROR", "message": "Stripe timeout"},

    # Atributos — dados de contexto do span
    "attributes": {
        "service.name": "payment-service",
        "http.method": "POST",
        "http.url": "https://api.stripe.com/v1/charges",
        "http.status_code": 408,
        "payment.amount": 9990,
        "payment.currency": "BRL",
    },

    # Eventos — logs point-in-time dentro do span
    "events": [
        {
            "name": "exception",
            "timestamp": 1705329791.645,
            "attributes": {
                "exception.type": "stripe.error.APIConnectionError",
                "exception.message": "Connection timeout after 300s",
                "exception.stacktrace": "...",
            }
        }
    ],

    # Links — referências a outros traces (ex: mensagem de fila)
    "links": [],
    "resource": {"service.name": "payment-service", "service.version": "2.1.0"}
}

W3C Trace Context: propagação por HTTP

Para que spans de serviços diferentes compartilhem o mesmo trace_id, o contexto precisa ser propagado nos headers HTTP. O padrão W3C Trace Context define dois headers:

bash
# traceparent — obrigatório
# Formato: versão-trace_id-parent_span_id-flags
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
#             ^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^
#             v0  trace_id (32 hex chars / 128 bits) parent_span_id  sampled=1

# tracestate — opcional, vendor-specific key-value
tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE

# Flags (último byte):
# 01 = sampled (este trace está sendo coletado)
# 00 = not sampled (propaga contexto mas sem coletar dados)

O OpenTelemetry propaga isso automaticamente com auto-instrumentação. Para propagação manual:

python
# Python: propagação manual (útil em filas, workers assíncronos)
from opentelemetry import trace, propagate
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator

propagator = TraceContextTextMapPropagator()

# === PRODUTOR (quem cria a mensagem) ===
def publish_to_queue(payload: dict, queue):
    carrier = {}
    propagator.inject(carrier)  # Extrai contexto ativo → dict

    message = {
        "payload": payload,
        "otel_context": carrier,  # {"traceparent": "00-4bf9...", "tracestate": ""}
    }
    queue.publish(message)

# === CONSUMIDOR (worker que processa a mensagem) ===
def process_queue_message(message: dict):
    carrier = message.get("otel_context", {})
    ctx = propagator.extract(carrier)  # Restaura contexto do produtor

    tracer = trace.get_tracer(__name__)
    with tracer.start_as_current_span(
        "process-order-event",
        context=ctx,  # Continua o trace do produtor
        kind=trace.SpanKind.CONSUMER,
    ) as span:
        span.set_attribute("messaging.system", "rabbitmq")
        span.set_attribute("messaging.destination", "orders")
        process(message["payload"])

Baggage: metadados de negócio no contexto

Baggage é um mecanismo para propagar key-value pairs pelo contexto de trace — além do trace_id. Qualquer serviço downstream pode ler sem receber como parâmetro explícito.

python
from opentelemetry import baggage, context

# === Injetar no início da requisição (ex: middleware de auth) ===
ctx = baggage.set_baggage("user.tier", "premium")
ctx = baggage.set_baggage("tenant.id", "acme-corp", context=ctx)
ctx = baggage.set_baggage("feature.flags", "new-checkout", context=ctx)

# Attach ao contexto da thread/coroutine
token = context.attach(ctx)

# === Ler em qualquer serviço downstream ===
user_tier = baggage.get_baggage("user.tier")      # "premium"
tenant_id = baggage.get_baggage("tenant.id")      # "acme-corp"

# Usar para decisões sem parâmetro explícito
if user_tier == "premium":
    rate_limit = 1000  # req/min
else:
    rate_limit = 100

# Baggage é propagado no header:
# baggage: user.tier=premium,tenant.id=acme-corp,feature.flags=new-checkout
⚠️
Limites do Baggage:Baggage é propagado em clear text no header HTTP e vai para TODOS os serviços downstream — incluindo serviços de terceiros. Nunca coloque PII, tokens ou dados sensíveis em baggage. Mantenha o payload pequeno (recomendado: < 8KB total).

Instrumentação manual: quando auto-instrumentation não basta

Auto-instrumentação cobre HTTP, DB, gRPC. Para lógica de negócio interna (processamento de arquivo, algoritmo customizado, call externo não-suportado), instrumentação manual é necessária.

python
from opentelemetry import trace
from opentelemetry.trace import StatusCode

tracer = trace.get_tracer(__name__, schema_url="https://semconv.org/")

def process_checkout(cart_id: str, user_id: str) -> Order:
    with tracer.start_as_current_span("checkout.process") as span:
        # Atributos descrevem o que o span representa
        span.set_attribute("checkout.cart_id", cart_id)
        span.set_attribute("checkout.user_id", user_id)
        span.set_attribute("checkout.item_count", len(cart.items))

        try:
            # Sub-operação — span filho automático
            with tracer.start_as_current_span("checkout.validate_inventory") as inv_span:
                inventory = check_inventory(cart.items)
                inv_span.set_attribute("inventory.items_available", len(inventory))

            # Evento point-in-time dentro do span pai
            span.add_event("inventory.validated", {"available": len(inventory)})

            order = create_order(cart, user_id)
            span.set_attribute("checkout.order_id", order.id)
            span.set_status(StatusCode.OK)
            return order

        except InventoryError as e:
            # Registrar exceção — aparece na timeline do span no Jaeger/Tempo
            span.record_exception(e)
            span.set_status(StatusCode.ERROR, f"Inventory unavailable: {e}")
            raise

        except Exception as e:
            span.record_exception(e)
            span.set_status(StatusCode.ERROR, str(e))
            raise
typescript
// Node.js: instrumentação manual
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';

const tracer = trace.getTracer('checkout-service', '1.0.0');

async function processCheckout(cartId: string, userId: string): Promise<Order> {
  return tracer.startActiveSpan(
    'checkout.process',
    {
      kind: SpanKind.SERVER,
      attributes: { 'checkout.cart_id': cartId, 'checkout.user_id': userId },
    },
    async (span) => {
      try {
        const order = await createOrder(cartId, userId);
        span.setAttributes({ 'checkout.order_id': order.id });
        span.setStatus({ code: SpanStatusCode.OK });
        return order;
      } catch (err) {
        span.recordException(err as Error);
        span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
        throw err;
      } finally {
        span.end(); // SEMPRE chamar end() — mesmo em erro
      }
    }
  );
}

Sampling: head vs tail

Em produção, coletar 100% dos traces é inviável (custo, storage, ingestão). Sampling define quais traces guardar. Existem duas estratégias fundamentais:


  HEAD SAMPLING (decisão no início)
  ─────────────────────────────────
  Request chega
       │
       ▼
  ┌──────────────────────────────┐
  │  Root Span criado            │
  │  Sorteia aleatório: 10%      │──── 10% YES ──→ coleta TODOS os spans
  │  sampled flag = 0/1          │──── 90% NO  ──→ descarta TODOS os spans
  └──────────────────────────────┘
  ✅ Zero overhead para 90% dos requests
  ❌ Pode descartar traces com erro ou lentos

  TAIL SAMPLING (decisão após completar)
  ───────────────────────────────────────
  Request chega
       │
       ▼ todos os spans são coletados e bufferizados
  ┌─────────────────────────────────────────────────────┐
  │  OTel Collector aguarda o trace completar (~30s)    │
  │  Avalia políticas:                                  │
  │    ─ status == ERROR?          → guardar sempre     │
  │    ─ duration > 2s?            → guardar sempre     │
  │    ─ tem atributo "debug"?     → guardar sempre     │
  │    ─ caso contrário: 5% random → guardar            │
  └─────────────────────────────────────────────────────┘
  ✅ Garante captura de traces problemáticos
  ❌ Requer buffer de memória no Collector (custo)
yaml
# Tail sampling no OTel Collector
processors:
  tail_sampling:
    decision_wait: 30s          # Aguarda trace completar
    num_traces: 100000          # Buffer de traces em memória
    expected_new_traces_per_sec: 1000

    policies:
      # SEMPRE guardar traces com erro
      - name: errors-policy
        type: status_code
        status_code: {status_codes: [ERROR]}

      # SEMPRE guardar traces lentos (> 2s)
      - name: slow-traces-policy
        type: latency
        latency: {threshold_ms: 2000}

      # SEMPRE guardar traces com header de debug
      - name: debug-policy
        type: string_attribute
        string_attribute:
          key: "sampling.debug"
          values: ["true"]

      # 5% dos restantes aleatoriamente
      - name: baseline-policy
        type: probabilistic
        probabilistic: {sampling_percentage: 5}

      # Composta: todos os spans de um trace devem satisfazer
      - name: composite-policy
        type: composite
        composite:
          max_total_spans_per_second: 10000
          policy_order: [errors-policy, slow-traces-policy, debug-policy, baseline-policy]

Jaeger vs Grafana Tempo

CritérioJaegerGrafana Tempo
Storage backendElasticsearch, Cassandra, BadgerObject storage (S3, GCS, MinIO) — muito mais barato
Query languageJaeger Query (simples)TraceQL — como PromQL mas para traces
Integração GrafanaPlugin separadoNativa — mesma UI que Loki e Mimir
EscalabilidadeLimitada pelo ES/CassandraHorizontal via object storage — barato em escala
Quando usarJá tem ES na stack; time pequeno; familiarStack LGTM completa; custo é preocupação; TraceQL
ManagedJaeger no Kubernetes | sem SaaS oficialGrafana Cloud Traces (serverless)

📋 Nova stack de observabilidade, time de 5-20 engenheiros, custo importa

Grafana Tempo + Loki + Mimir (LGTM stack)

Object storage é 10-50× mais barato que Elasticsearch para dados de trace. TraceQL permite queries avançadas. Integração nativa entre traces (Tempo), logs (Loki) e métricas (Mimir) no Grafana — link direto de log para trace via trace_id.

Alt: Jaeger + ElasticsearchJá tem ES operado internamente, time familiarizado com Jaeger

Alt: Datadog APM / Honeycomb (SaaS)Time pequeno sem capacidade de operar infra de observabilidade; aceita custo SaaS

Alt: ZipkinLegacy — evitar para novas stacks; sem suporte a OTLP nativo

TraceQL: consultando traces como dados

TraceQL (Grafana Tempo) permite queries poderosas sobre atributos de spans — muito além de buscar por trace_id.

bash
# TraceQL — exemplos práticos

# Todos os traces com erro no payment-service
{service.name="payment-service" && status=error}

# Traces com duração > 2s no checkout
{span.name="checkout.process" && duration > 2s}

# Traces onde payment falhou com status HTTP 500
{service.name="payment-service" && http.status_code=500}

# Traces de usuário premium com latência > 1s (via baggage/atributo)
{span.user.tier="premium" && duration > 1s}

# Buscar por atributo de negócio
{span.checkout.order_id="ORD-12345"}

# Agrupar por serviço + contar erros (Tempo 2.4+)
{status=error} | by(service.name) | count() > 10

# Rate de erros por serviço
rate({status=error}[5m]) by (service.name)

Semantic Conventions: nomes padronizados

OpenTelemetry Semantic Conventions define nomes de atributos padronizados. Seguir essas convenções permite que ferramentas como Jaeger e Tempo reconheçam e exibam dados automaticamente.

DomínioAtributoExemplo
HTTPhttp.method, http.url, http.status_code, http.routePOST, /checkout, 200, /checkout
Databasedb.system, db.name, db.statement, db.operationpostgresql, orders_db, SELECT..., query
Messagingmessaging.system, messaging.destination, messaging.operationrabbitmq, orders, publish
RPCrpc.system, rpc.service, rpc.methodgrpc, OrderService, CreateOrder
Serviçoservice.name, service.version, service.namespacepayment-service, 2.1.0, production
Runtimeprocess.runtime.name, process.pidpython, 12345

Q&A — perguntas típicas de entrevista SRE

P: Como tracear uma requisição que passa por uma fila de mensagens (ex: SQS, Kafka)?

R: Injetar o carrier (traceparent + tracestate) nos metadados da mensagem ao publicar. O consumidor extrai o carrier e restaura o contexto com propagator.extract(). O span do consumidor usa kind=CONSUMER e recebe o contexto do produtor — continuando o mesmo trace_id. Assim o Jaeger/Tempo mostra o trace completo incluindo o "gap" assíncrono da fila.

P: Um trace tem um span lento mas não consigo ver por quê. O que verificar?

R: Waterfall: identificar qual span filho é o mais lento. Verificar atributos do span (ex: db.statement — query sem índice?). Ver eventos registrados (span.add_event) para timestamps internos. Verificar se há "gap" entre spans (tempo no servidor sem span correspondente — pode indicar processamento não instrumentado ou contenção de thread). Se for DB, verificar db.rows_affected e db.statement.

P: Por que não instrumentar tudo manualmente desde o início?

R: Auto-instrumentação cobre 80% do trabalho com zero código. Frameworks como FastAPI, Django, Flask, Express, gRPC, sqlalchemy, redis, boto3 já têm instrumentadores automáticos. Instrumentação manual deve ser adicionada apenas para operações de negócio críticas não cobertas — ex: processamento de arquivo, algoritmos customizados, integrações sem instrumentador oficial.

Take-aways:
  • Trace = árvore de spans com mesmo trace_id. Span = unidade de trabalho com duração, atributos e eventos.
  • W3C Trace Context (header traceparent) propaga contexto automaticamente por HTTP — OTel faz isso sem código manual.
  • Baggage propaga metadados de negócio (tenant_id, user_tier) downstream — nunca PII ou dados sensíveis.
  • Head sampling: simples, zero overhead, pode perder traces problemáticos. Tail sampling: inteligente, guarda erros/lentos, requer buffer.
  • Sempre registrar exceção no span: record_exception() + set_status(ERROR) + span.end() no finally.
  • Grafana Tempo + object storage (S3/MinIO) é significativamente mais barato que Jaeger + Elasticsearch em escala.
  • Seguir Semantic Conventions é o que permite ferramentas reconhecerem automaticamente operações de DB, HTTP, messaging.
🧩

Quiz rápido

4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito

Continue lendo