Distributed Tracing em Kubernetes: Jaeger e OpenTelemetry Collector na Prática Já leu

O que é Distributed Tracing e por que você precisa disso em Kubernetes Distributed Tracing é uma técnica de observabilidade que permite rastrear uma requisição através de múltiplos serviços em um sistema distribuído. Diferentemente de logs tradicionais, que registram eventos isolados em cada serviço, o distributed tracing conecta essas informações em uma única "trace" que mostra todo o fluxo de execução, os tempos de latência em cada etapa e onde os gargalos acontecem. Em um ambiente Kubernetes com dezenas ou centenas de microserviços, essa visibilidade se torna crítica. Quando um usuário faz uma requisição em um sistema monolítico, o log é trivial: você vê a entrada, o processamento e a saída em um único lugar. Mas em Kubernetes, essa mesma requisição pode passar por um API Gateway, três serviços backend, um banco de dados, e um cache distribuído — cada um em um container diferente, possivelmente em nodes diferentes. Sem distributed tracing, você fica perdido tentando correlacionar logs de diversos

O que é Distributed Tracing e por que você precisa disso em Kubernetes

Distributed Tracing é uma técnica de observabilidade que permite rastrear uma requisição através de múltiplos serviços em um sistema distribuído. Diferentemente de logs tradicionais, que registram eventos isolados em cada serviço, o distributed tracing conecta essas informações em uma única "trace" que mostra todo o fluxo de execução, os tempos de latência em cada etapa e onde os gargalos acontecem. Em um ambiente Kubernetes com dezenas ou centenas de microserviços, essa visibilidade se torna crítica.

Quando um usuário faz uma requisição em um sistema monolítico, o log é trivial: você vê a entrada, o processamento e a saída em um único lugar. Mas em Kubernetes, essa mesma requisição pode passar por um API Gateway, três serviços backend, um banco de dados, e um cache distribuído — cada um em um container diferente, possivelmente em nodes diferentes. Sem distributed tracing, você fica perdido tentando correlacionar logs de diversos serviços manualmente. Com distributed tracing, você tem um mapa visual completo, chamado de trace, que mostra exatamente onde a requisição foi, quanto tempo levou em cada passo, e onde ocorreram erros.

Jaeger: A Solução de Tracing que você pode usar hoje

O que é Jaeger e como funciona

Jaeger é um sistema open source de distributed tracing desenvolvido originalmente no Uber. Ele implementa o padrão OpenTracing (hoje evoluído para OpenTelemetry) e oferece coleta, armazenamento e visualização de traces. A arquitetura do Jaeger é composta por alguns componentes principais: o agent, que roda ao lado de cada aplicação e recebe spans; o collector, que agrega esses spans; e o backend de armazenamento, que pode ser Elasticsearch, Cassandra ou memória.

Um "span" é a unidade básica de um trace. Ele representa uma unidade de trabalho — pode ser uma chamada HTTP, uma query ao banco de dados, ou qualquer operação que você queira rastrear. Quando você inicia uma requisição, cria um span raiz (root span), e cada operação subsequente cria child spans. O trace é simplesmente uma coleção de todos os spans relacionados, identificados por um trace ID único que trafega junto com a requisição através de todos os serviços.

Instalando Jaeger em Kubernetes com Helm

A forma mais prática de instalar Jaeger em Kubernetes é usando Helm. Primeiro, adicione o repositório oficial:

helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo update

Crie um arquivo values.yaml com uma configuração simples para desenvolvimento:

# jaeger-values.yaml
provisionDataStore:
  cassandra: false

storage:
  type: badger
  badger:
    ephemeral: false
    directoryvolume:
      size: 10Gi

agent:
  enabled: true
  service:
    type: ClusterIP
    port: 6831
    protocols:
      - udp

collector:
  enabled: true
  service:
    type: ClusterIP
    port: 14268

query:
  enabled: true
  service:
    type: ClusterIP
    port: 16686

Instale no seu cluster:

helm install jaeger jaegertracing/jaeger -f jaeger-values.yaml -n observability --create-namespace

Para acessar a UI do Jaeger, faça um port-forward:

kubectl port-forward -n observability svc/jaeger-query 16686:16686

Acesse em http://localhost:16686 e você verá a interface de visualização de traces.

OpenTelemetry Collector: O middleware inteligente para tracing

Por que você precisa do OpenTelemetry Collector

OpenTelemetry é um conjunto de padrões abertos para instrumentação de código. O OpenTelemetry Collector é um daemon que roda em seu cluster Kubernetes, funciona como um middleware central que recebe sinais de telemetria (traces, metrics, logs) de suas aplicações, processa, enriquece, e envia para backends como Jaeger, Prometheus, ou Loki. Enquanto Jaeger é uma solução completa de tracing, OpenTelemetry Collector oferece flexibilidade para trabalhar com múltiplos backends e fazer transformações nos dados.

A principal vantagem é a separação de responsabilidades: suas aplicações não precisam saber onde os dados vão parar. Elas enviam para o Collector, que fica responsável por rotear, processar, samplear (reduzir volume), e enviar para os destinos finais. Isso facilita mudanças arquiteturais — você pode trocar o backend de tracing sem alterar o código das aplicações.

Instalando OpenTelemetry Collector em Kubernetes

Instale o Collector usando Helm:

helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update

Crie um arquivo otel-collector-values.yaml:

# otel-collector-values.yaml
mode: daemonset

config:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318
    jaeger:
      protocols:
        grpc:
          endpoint: 0.0.0.0:14250
        thrift_http:
          endpoint: 0.0.0.0:14268

  processors:
    batch:
      send_batch_size: 100
      timeout: 10s
    memory_limiter:
      check_interval: 1s
      limit_mib: 512
      spike_limit_mib: 128
    attributes:
      actions:
        - key: deployment.name
          value: myapp
          action: insert

  exporters:
    jaeger:
      endpoint: jaeger-collector:14250
      tls:
        insecure: true

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

service:
  enabled: true
  type: ClusterIP

serviceAccount:
  create: true
  name: otel-collector

Instale:

helm install otel-collector open-telemetry/opentelemetry-collector \
  -f otel-collector-values.yaml \
  -n observability

Instrumentando suas aplicações para enviar traces

Exemplo prático: Python com FastAPI e OpenTelemetry

Vamos criar uma aplicação Python simples que envia traces para o OpenTelemetry Collector. Primeiro, instale as dependências:

pip install fastapi uvicorn opentelemetry-api opentelemetry-sdk \
  opentelemetry-exporter-otlp opentelemetry-instrumentation-fastapi \
  opentelemetry-instrumentation-requests

Crie um arquivo app.py:

from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
import requests

# Configurar o exporter OTLP (envia para o Collector)
otlp_exporter = OTLPSpanExporter(
    endpoint="otel-collector.observability.svc.cluster.local:4317"
)

# Configurar o TracerProvider
trace_provider = TracerProvider()
trace_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(trace_provider)

# Instrumentar FastAPI e requests automaticamente
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
RequestsInstrumentor().instrument()

# Obter um tracer manualmente
tracer = trace.get_tracer(__name__)

@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
    """
    Este endpoint cria spans automaticamente graças ao
    FastAPIInstrumentor, mas também adicionamos um span manual
    """
    with tracer.start_as_current_span("fetch_user_from_db") as span:
        span.set_attribute("user.id", user_id)

        # Simular chamada a banco de dados
        with tracer.start_as_current_span("database_query"):
            # Em uma app real, isso seria uma query ao banco
            user = {"id": user_id, "name": f"User {user_id}"}

        # Simular chamada a outro serviço
        with tracer.start_as_current_span("call_auth_service"):
            try:
                response = requests.get(
                    "http://auth-service:8080/validate",
                    params={"user_id": user_id},
                    timeout=5
                )
                span.set_attribute("auth.status", response.status_code)
            except Exception as e:
                span.set_attribute("auth.error", str(e))

        return user

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Exemplo prático: Node.js com Express e OpenTelemetry

Crie um aplicativo Express instrumentado:

npm install express @opentelemetry/api @opentelemetry/sdk-node \
  @opentelemetry/auto @opentelemetry/sdk-trace-node \
  @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/instrumentation-http \
  @opentelemetry/instrumentation-express

Crie um arquivo tracing.js (deve ser importado antes do app):

const { NodeSDK } = require("@opentelemetry/sdk-node");
const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node");
const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-grpc");
const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-node");

const traceExporter = new OTLPTraceExporter({
  url: "grpc://otel-collector.observability.svc.cluster.local:4317",
});

const sdk = new NodeSDK({
  traceExporter,
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

console.log("Tracing initialized");

process.on("SIGTERM", () => {
  sdk.shutdown()
    .then(() => console.log("Tracing terminated"))
    .catch((log) => console.log("Error terminating tracing", log))
    .finally(() => process.exit(0));
});

Crie um arquivo app.js:

// IMPORTANTE: tracing.js deve ser importado PRIMEIRO
require("./tracing");

const express = require("express");
const { trace } = require("@opentelemetry/api");

const app = express();
const tracer = trace.getTracer("express-app");

app.get("/api/orders/:order_id", async (req, res) => {
  const { order_id } = req.params;

  // Criar um span manual
  const span = tracer.startSpan("fetch_order");

  try {
    span.setAttributes({
      "order.id": order_id,
      "service.name": "order-service",
    });

    // Simular operações
    const orderData = await new Promise((resolve) => {
      setTimeout(() => {
        resolve({ id: order_id, total: 199.99 });
      }, 100);
    });

    span.end();
    res.json(orderData);
  } catch (error) {
    span.recordException(error);
    span.end();
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => {
  console.log("Server running on port 3000");
});

Deployment no Kubernetes

Crie um arquivo deployment.yaml para sua aplicação Python:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api-service
  template:
    metadata:
      labels:
        app: api-service
    spec:
      containers:
      - name: api-service
        image: seu-registry/api-service:latest
        ports:
        - containerPort: 8000
        env:
        - name: OTEL_EXPORTER_OTLP_ENDPOINT
          value: "http://otel-collector.observability.svc.cluster.local:4318"
        - name: OTEL_SERVICE_NAME
          value: "api-service"
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"

Aplique no cluster:

kubectl apply -f deployment.yaml

Visualizando e analisando traces

Faça uma requisição para gerar um trace:

# Forward para sua aplicação
kubectl port-forward svc/api-service 8000:8000

# Em outro terminal, faça uma requisição
curl http://localhost:8000/api/users/123

Acesse a UI do Jaeger em http://localhost:16686. Você verá:

  • Uma lista de serviços descobertos automaticamente
  • Traces com latência total e breakdown por serviço
  • Spans individuais com atributos customizados
  • Informações de erro e stack traces
  • Visualização em cascata mostrando paralelismo

Na UI, procure pelo serviço "api-service" e selecione uma operação. Clique em um trace para ver:

Trace ID: a1b2c3d4e5f6g7h8
Service: api-service
Operation: GET /api/users/{user_id}
Duration: 245ms
Spans:
  ├─ GET /api/users/{user_id} [200ms]
  │  ├─ fetch_user_from_db [120ms]
  │  │  ├─ database_query [80ms]
  │  │  └─ call_auth_service [40ms]
  │  └─ serialize_response [5ms]

Conclusão

Os três pontos fundamentais que você deve levar consigo: primeiro, distributed tracing é observabilidade de verdade — não é mais opcional em arquiteturas de microserviços em Kubernetes. Logs isolados em cada serviço deixam você cego. Com Jaeger ou similares, você vê o fluxo completo da requisição. Segundo, OpenTelemetry é o padrão de facto — enquanto Jaeger é a solução de backend, OpenTelemetry é como você instrumenta seu código. É agnóstico de vendor, o que significa que você não fica preso. Terceiro, a instrumentação não é complexa — com bibliotecas modernas e auto-instrumentação, você ganha tracing de graça em muitos casos, e adicionar spans customizados é trivial. Comece pequeno com uma aplicação, veja os traces sendo coletados, e expanda gradualmente.

Referências


Artigos relacionados