<h2>O que é Distributed Tracing e por que você precisa disso em Kubernetes</h2>
<p>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.</p>
<p>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.</p>
<h2>Jaeger: A Solução de Tracing que você pode usar hoje</h2>
<h3>O que é Jaeger e como funciona</h3>
<p>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.</p>
<p>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.</p>
<h3>Instalando Jaeger em Kubernetes com Helm</h3>
<p>A forma mais prática de instalar Jaeger em Kubernetes é usando Helm. Primeiro, adicione o repositório oficial:</p>
<pre><code class="language-bash">helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo update</code></pre>
<p>Crie um arquivo <code>values.yaml</code> com uma configuração simples para desenvolvimento:</p>
<pre><code class="language-yaml"># 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</code></pre>
<p>Instale no seu cluster:</p>
<pre><code class="language-bash">helm install jaeger jaegertracing/jaeger -f jaeger-values.yaml -n observability --create-namespace</code></pre>
<p>Para acessar a UI do Jaeger, faça um port-forward:</p>
<pre><code class="language-bash">kubectl port-forward -n observability svc/jaeger-query 16686:16686</code></pre>
<p>Acesse em <code>http://localhost:16686</code> e você verá a interface de visualização de traces.</p>
<h2>OpenTelemetry Collector: O middleware inteligente para tracing</h2>
<h3>Por que você precisa do OpenTelemetry Collector</h3>
<p>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.</p>
<p>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.</p>
<h3>Instalando OpenTelemetry Collector em Kubernetes</h3>
<p>Instale o Collector usando Helm:</p>
<pre><code class="language-bash">helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update</code></pre>
<p>Crie um arquivo <code>otel-collector-values.yaml</code>:</p>
<pre><code class="language-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</code></pre>
<p>Instale:</p>
<pre><code class="language-bash">helm install otel-collector open-telemetry/opentelemetry-collector \
-f otel-collector-values.yaml \
-n observability</code></pre>
<h2>Instrumentando suas aplicações para enviar traces</h2>
<h3>Exemplo prático: Python com FastAPI e OpenTelemetry</h3>
<p>Vamos criar uma aplicação Python simples que envia traces para o OpenTelemetry Collector. Primeiro, instale as dependências:</p>
<pre><code class="language-bash">pip install fastapi uvicorn opentelemetry-api opentelemetry-sdk \
opentelemetry-exporter-otlp opentelemetry-instrumentation-fastapi \
opentelemetry-instrumentation-requests</code></pre>
<p>Crie um arquivo <code>app.py</code>:</p>
<pre><code class="language-python">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)</code></pre>
<h3>Exemplo prático: Node.js com Express e OpenTelemetry</h3>
<p>Crie um aplicativo Express instrumentado:</p>
<pre><code class="language-bash">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</code></pre>
<p>Crie um arquivo <code>tracing.js</code> (deve ser importado antes do app):</p>
<pre><code class="language-javascript">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));
});</code></pre>
<p>Crie um arquivo <code>app.js</code>:</p>
<pre><code class="language-javascript">// 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");
});</code></pre>
<h3>Deployment no Kubernetes</h3>
<p>Crie um arquivo <code>deployment.yaml</code> para sua aplicação Python:</p>
<pre><code class="language-yaml">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"</code></pre>
<p>Aplique no cluster:</p>
<pre><code class="language-bash">kubectl apply -f deployment.yaml</code></pre>
<h2>Visualizando e analisando traces</h2>
<p>Faça uma requisição para gerar um trace:</p>
<pre><code class="language-bash"># 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</code></pre>
<p>Acesse a UI do Jaeger em <code>http://localhost:16686</code>. Você verá:</p>
<ul>
<li>Uma lista de serviços descobertos automaticamente</li>
<li>Traces com latência total e breakdown por serviço</li>
<li>Spans individuais com atributos customizados</li>
<li>Informações de erro e stack traces</li>
<li>Visualização em cascata mostrando paralelismo</li>
</ul>
<p>Na UI, procure pelo serviço "api-service" e selecione uma operação. Clique em um trace para ver:</p>
<pre><code>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]</code></pre>
<h2>Conclusão</h2>
<p>Os três pontos fundamentais que você deve levar consigo: primeiro, <strong>distributed tracing é observabilidade de verdade</strong> — 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, <strong>OpenTelemetry é o padrão de facto</strong> — 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, <strong>a instrumentação não é complexa</strong> — 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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.jaegertracing.io/docs/" target="_blank" rel="noopener noreferrer">Documentação oficial do Jaeger</a></li>
<li><a href="https://opentelemetry.io/docs/instrumentation/" target="_blank" rel="noopener noreferrer">OpenTelemetry - Guia de Getting Started</a></li>
<li><a href="https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-fastapi" target="_blank" rel="noopener noreferrer">OpenTelemetry Python - FastAPI Integration</a></li>
<li><a href="https://github.com/open-telemetry/opentelemetry-helm-charts" target="_blank" rel="noopener noreferrer">Helm Charts - OpenTelemetry Collector</a></li>
<li><a href="https://www.oreilly.com/library/view/distributed-systems-observability/9781492033431/" target="_blank" rel="noopener noreferrer">Distributed Systems Observability - O'Reilly (Yuri Shkuro)</a></li>
</ul>
<p><!-- FIM --></p>