<h2>Introdução ao Distributed Tracing</h2>
<p>A arquitetura de microsserviços revolucionou a forma como desenvolvemos aplicações, permitindo escalabilidade e independência entre serviços. No entanto, ela introduziu um desafio crítico: como rastrear uma requisição quando ela atravessa múltiplos serviços? Quando um usuário faz uma requisição que passa por 5, 10 ou 20 serviços diferentes, entender o que aconteceu em cada etapa e onde surgem gargalos se torna extremamente complexo.</p>
<p>O Distributed Tracing resolve este problema capturando toda a jornada de uma requisição através dos serviços. Diferentemente dos logs tradicionais, que registram eventos isolados, o tracing fornece uma visão holística de como a requisição flui pela arquitetura. O Jaeger e o Zipkin são os dois principais frameworks open-source para implementar distributed tracing em produração, cada um com suas características distintas e casos de uso específicos.</p>
<h3>Por que Distributed Tracing é essencial em microsserviços</h3>
<p>Em um monólito, você pode simplesmente analisar logs de uma única aplicação. Em microsserviços, a mesma operação de negócio envolve múltiplos serviços, cada um rodando em containers diferentes, possivelmente em máquinas diferentes. Sem tracing distribuído, investigar um problema de performance ou uma falha se torna uma caça ao tesouro. O distributed tracing permite que você visualize o tempo gasto em cada serviço, identifique gargalos, detecte falhas intermitentes e correlacione eventos em diferentes serviços automaticamente.</p>
<h2>Conceitos Fundamentais de Distributed Tracing</h2>
<h3>Trace, Span e Baggage</h3>
<p>Um <strong>trace</strong> é o registro completo de uma requisição passando por toda a arquitetura. Pense nele como uma unidade de trabalho lógica que pode envolver múltiplos serviços. Cada trace é identificado por um <code>trace_id</code> único e imutável.</p>
<p>Um <strong>span</strong> é uma operação individual dentro de um trace. Se um trace é a jornada completa, um span é cada parada nessa jornada. Cada span possui: um <code>span_id</code> único, um <code>trace_id</code> (herdado do trace pai), um <code>parent_span_id</code> (se aplicável), timestamps de início e fim, e tags/logs adicionais. Os spans formam uma relação hierárquica em forma de árvore, permitindo visualizar a estrutura exata da execução.</p>
<p><strong>Baggage</strong> é metadados que viajam junto com o trace através de todos os serviços. Exemplos: user_id, request_id customizado, tenant_id. O baggage permite correlacionar logs e métricas de diferentes serviços ao mesmo contexto de negócio.</p>
<pre><code>Trace (user_id: 123, request_id: abc-xyz)
├── Span: HTTP GET /api/usuarios/123 (serviço A)
│ ├── Span: Query database (serviço A)
│ └── Span: HTTP GET /api/detalhes/123 (serviço B)
│ ├── Span: Cache lookup (serviço B)
│ └── Span: Query database (serviço B)
└── Span: Response assembly (serviço A)</code></pre>
<h3>Context Propagation</h3>
<p>Para que diferentes serviços "saibam" que fazem parte do mesmo trace, o contexto (trace_id, span_id, baggage) deve ser propagado entre eles. Isso geralmente é feito através de headers HTTP. Os padrões mais comuns são:</p>
<ul>
<li><strong>Jaeger Headers</strong>: <code>uber-trace-id</code>, <code>jaeger-baggage</code></li>
<li><strong>W3C Trace Context</strong> (padrão moderno): <code>traceparent</code>, <code>tracestate</code>, <code>baggage</code></li>
<li><strong>Zipkin Headers</strong>: <code>X-B3-TraceId</code>, <code>X-B3-SpanId</code>, <code>X-B3-ParentSpanId</code></li>
</ul>
<p>Quando o serviço A faz uma chamada HTTP para o serviço B, inclui esses headers. O serviço B extrai o contexto, cria seus próprios spans como filhos, e continua propagando para o serviço C. Assim, toda a cadeia permanece conectada.</p>
<h2>Jaeger: Arquitetura, Instalação e Uso Prático</h2>
<h3>Arquitetura do Jaeger</h3>
<p>O Jaeger é mantido pela Cloud Native Computing Foundation e oferece uma arquitetura modular com três componentes principais:</p>
<ol>
<li><strong>Jaeger Client</strong>: biblioteca que instrumento seu código para gerar spans</li>
<li><strong>Jaeger Agent</strong>: daemon que coleta spans dos clientes (porta UDP 6831 por padrão)</li>
<li><strong>Jaeger Backend</strong>: processa, armazena e disponibiliza a UI para consulta</li>
</ol>
<p>A comunicação flui assim: sua aplicação → Jaeger Client → Jaeger Agent → Jaeger Collector → storage (Elasticsearch, Badger, etc.) → Jaeger Query UI.</p>
<p>Jaeger suporta múltiplas linguagens (Go, Java, Python, Node.js, C++, etc.) através de bibliotecas cliente. Para Java, usamos o <code>jaeger-client</code>, para Python <code>jaeger-client</code>, e assim por diante.</p>
<h3>Instalação e Configuração</h3>
<p>A forma mais prática de começar é com Docker Compose. Crie um arquivo <code>docker-compose.yml</code>:</p>
<pre><code class="language-yaml">version: '3'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "6831:6831/udp" # Jaeger agent (Thrift compact)
- "16686:16686" # Jaeger UI
- "14268:14268" # Jaeger collector HTTP
environment:
- COLLECTOR_ZIPKIN_HOST_PORT=:9411</code></pre>
<p>Suba com <code>docker-compose up -d</code>. A UI estará em <code>http://localhost:16686</code>.</p>
<h3>Instrumentação em Python</h3>
<p>Vamos criar dois microsserviços simples em Flask com tracing do Jaeger. Primeiro, instale as dependências:</p>
<pre><code class="language-bash">pip install flask jaeger-client opentelemetry-api opentelemetry-sdk</code></pre>
<p><strong>Serviço A (porta 5000)</strong>:</p>
<pre><code class="language-python">from flask import Flask, request
import requests
from jaeger_client import Config
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry import trace
app = Flask(__name__)
Configurar Jaeger
def init_jaeger_tracer(service_name):
config = Config(
config={
'sampler': {'type': 'const', 'param': 1},
'local_agent': {'reporting_host': 'localhost', 'reporting_port': 6831},
'logging': True,
},
service_name=service_name,
)
return config.initialize_tracer()
tracer = init_jaeger_tracer('servico-a')
Instrumentar Flask e requests automaticamente
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()
@app.route('/processar/<usuario_id>')
def processar(usuario_id):
Criar span manual se necessário
with tracer.start_active_span('buscar-dados-usuario') as scope:
scope.span.set_tag('usuario_id', usuario_id)
Chamar serviço B
response = requests.get(f'http://localhost:5001/dados/{usuario_id}')
dados = response.json()
return {'resultado': 'sucesso', 'dados': dados}
if __name__ == '__main__':
app.run(port=5000, debug=False)</code></pre>
<p><strong>Serviço B (porta 5001)</strong>:</p>
<pre><code class="language-python">from flask import Flask
from jaeger_client import Config
from opentelemetry.instrumentation.flask import FlaskInstrumentor
app = Flask(__name__)
def init_jaeger_tracer(service_name):
config = Config(
config={
'sampler': {'type': 'const', 'param': 1},
'local_agent': {'reporting_host': 'localhost', 'reporting_port': 6831},
'logging': True,
},
service_name=service_name,
)
return config.initialize_tracer()
tracer = init_jaeger_tracer('servico-b')
FlaskInstrumentor().instrument_app(app)
@app.route('/dados/<usuario_id>')
def obter_dados(usuario_id):
with tracer.start_active_span('query-database') as scope:
scope.span.set_tag('usuario_id', usuario_id)
Simular operação de banco
dados = {'id': usuario_id, 'nome': 'João Silva', 'email': 'joao@example.com'}
return dados
if __name__ == '__main__':
app.run(port=5001, debug=False)</code></pre>
<p>Agora, inicie ambos os serviços em terminais diferentes:</p>
<pre><code class="language-bash">python servico_a.py
python servico_b.py</code></pre>
<p>Faça uma requisição:</p>
<pre><code class="language-bash">curl http://localhost:5000/processar/123</code></pre>
<p>Acesse <code>http://localhost:16686</code> e procure pelo trace. Você verá toda a cadeia: Serviço A → Serviço B, com tempos de execução de cada span.</p>
<h2>Zipkin: Diferenças, Instalação e Uso Prático</h2>
<h3>Características do Zipkin</h3>
<p>Zipkin é um projeto mais maduro (origem no Twitter) focado em simplicidade e performance. Enquanto Jaeger oferece maior flexibilidade e features avançadas, Zipkin destaca-se por ser leve e direto. Zipkin também suporta múltiplas linguagens e usa o padrão B3 de headers de propagação de contexto.</p>
<p>A arquitetura é similar: clientes geram spans, enviam para o Zipkin (via HTTP ou Kafka), dados são armazenados e consultados na UI. Zipkin oferece suporte nativo a Elasticsearch, MySQL e Cassandra como backends.</p>
<h3>Instalação com Docker Compose</h3>
<p>Crie um <code>docker-compose.yml</code> para Zipkin:</p>
<pre><code class="language-yaml">version: '3'
services:
zipkin:
image: openzipkin/zipkin:latest
ports:
- "9410:9410" # Zipkin UI (diferente de Jaeger!)
- "9411:9411" # Zipkin collector HTTP</code></pre>
<p>Levante com <code>docker-compose up -d</code>. A UI estará em <code>http://localhost:9410</code>.</p>
<h3>Instrumentação em Java (Spring Boot)</h3>
<p>Para Java com Spring Boot, o processo é ainda mais simples. Adicione ao <code>pom.xml</code>:</p>
<pre><code class="language-xml"><dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency></code></pre>
<p>E configure no <code>application.yml</code>:</p>
<pre><code class="language-yaml">spring:
zipkin:
base-url: http://localhost:9411/
sender:
type: web
sleuth:
sampler:
probability: 1.0 # Coletar 100% dos traces (ajuste em produção)</code></pre>
<p><strong>Serviço A (porta 8080)</strong>:</p>
<pre><code class="language-java">package com.exemplo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class ServicoA {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ServicoA.class, args);
}
}
@RestController
class ControladorA {
private final RestTemplate restTemplate;
public ControladorA(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@GetMapping("/processar/{usuarioId}")
public Map<String, Object> processar(@PathVariable String usuarioId) {
// O Spring Sleuth automaticamente adiciona headers B3 a cada requisição
String dados = restTemplate.getForObject(
"http://localhost:8081/dados/" + usuarioId,
String.class
);
return Map.of("resultado", "sucesso", "dados", dados);
}
}</code></pre>
<p><strong>Serviço B (porta 8081)</strong>:</p>
<pre><code class="language-java">package com.exemplo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class ServicoB {
public static void main(String[] args) {
SpringApplication.run(ServicoB.class, args);
}
}
@RestController
class ControladorB {
@GetMapping("/dados/{usuarioId}")
public Map<String, Object> obterDados(@PathVariable String usuarioId) {
// Simular consulta ao banco
return Map.of(
"id", usuarioId,
"nome", "Maria Santos",
"email", "maria@example.com"
);
}
}</code></pre>
<p>Configure as portas em <code>application.yml</code> de cada serviço:</p>
<pre><code class="language-yaml">server:
port: 8080 # ou 8081 para Serviço B</code></pre>
<p>Após iniciar ambos, faça:</p>
<pre><code class="language-bash">curl http://localhost:8080/processar/123</code></pre>
<p>Acesse <code>http://localhost:9410</code> e procure pelos traces. Você verá a chamada de A para B rastreada automaticamente, pois o Spring Sleuth injeta os headers B3 transparentemente.</p>
<h2>Comparação Prática: Jaeger vs Zipkin</h2>
<h3>Características-chave</h3>
<div class="table-wrap"><table><thead><tr><th>Aspecto</th><th>Jaeger</th><th>Zipkin</th></tr></thead><tbody><tr><td><strong>Origem</strong></td><td>Uber (CNCF)</td><td>Twitter</td></tr><tr><td><strong>Maturidade</strong></td><td>Mais recente, inovador</td><td>Mais estabelecido</td></tr><tr><td><strong>Sampling</strong></td><td>Mais flexível (adaptive sampling)</td><td>Básico (fixed rate)</td></tr><tr><td><strong>Storage</strong></td><td>Elasticsearch, Badger, Cassandra</td><td>Elasticsearch, MySQL, Cassandra</td></tr><tr><td><strong>Headers</strong></td><td>Jaeger, W3C Trace Context</td><td>B3 (padrão)</td></tr><tr><td><strong>Curva de aprendizado</strong></td><td>Moderada</td><td>Suave</td></tr><tr><td><strong>Performance</strong></td><td>Excelente com Badger</td><td>Excelente em geral</td></tr><tr><td><strong>UI</strong></td><td>Avançada, com gráficos complexos</td><td>Simples e intuitiva</td></tr><tr><td><strong>Deploy</strong></td><td>Mais componentizado</td><td>Mais monolítico</td></tr></tbody></table></div>
<h3>Quando usar cada um</h3>
<p>Use <strong>Jaeger</strong> se: você precisa de sampling adaptativo baseado em taxa de erro, quer máxima flexibilidade na propagação de contexto (W3C), trabalha em ambiente Kubernetes/CNCF ou precisa de features avançadas como trace comparisons e node graphs.</p>
<p>Use <strong>Zipkin</strong> se: você quer simplicidade, já trabalha com Spring Boot/Spring Cloud, precisa de uma solução leve, ou prefere uma UI mais minimalista que focam em essencial.</p>
<p>Na prática, ambos resolvem o problema de distributed tracing de forma excelente. A escolha muitas vezes depende do seu ecossistema existente e preferências de arquitetura.</p>
<h2>Padrões Avançados e Boas Práticas</h2>
<h3>Sampling em Produção</h3>
<p>Coletar 100% dos traces em produção é caro e desnecessário. Implementar sampling adequado é crítico. Jaeger oferece várias estratégias:</p>
<p><strong>Constant Sampling</strong>: coleta uma fração fixa (ex: 10% de todos os traces)</p>
<p><strong>Probabilistic Sampling</strong>: coleta aleatoriamente com uma probabilidade</p>
<p><strong>Rate Limiting Sampling</strong>: coleta até um máximo de spans por segundo</p>
<p><strong>Remote Sampling</strong>: o servidor Jaeger decide qual percentage usar dinamicamente</p>
<p>Para Zipkin com Spring Boot:</p>
<pre><code class="language-yaml">spring:
sleuth:
sampler:
probability: 0.1 # 10% dos traces</code></pre>
<p>Para maior controle com Jaeger e Python:</p>
<pre><code class="language-python">config = Config(
config={
'sampler': {
'type': 'probabilistic',
'param': 0.1, # 10%
},
},
service_name='meu-servico',
)</code></pre>
<h3>Baggage para Contexto de Negócio</h3>
<p>Propagar contexto de negócio como user_id ou tenant_id é essencial. Em Java com Sleuth:</p>
<pre><code class="language-java">import org.springframework.cloud.sleuth.Tracer;
@RestController
public class MeuController {
private final Tracer tracer;
public MeuController(Tracer tracer) {
this.tracer = tracer;
}
@GetMapping("/comprar")
public void comprar(@RequestParam String usuarioId) {
// Adicionar ao baggage para propagar em todas as chamadas subsequentes
tracer.getBaggage("usuario_id").set(usuarioId);
// Chamadas subsequentes incluirão usuario_id automaticamente
servicoB.processar();
}
}</code></pre>
<p>Em Python com Jaeger:</p>
<pre><code class="language-python">with tracer.start_active_span('minha-operacao') as scope:
scope.span.set_tag('usuario_id', usuario_id)
scope.span.set_tag('tenant_id', tenant_id)
Estes tags ficarão visíveis na UI e nos logs</code></pre>
<h3>Logs estruturados integrados com tracing</h3>
<p>A verdadeira potência emerge quando logs estruturados incluem trace_id. Assim, você correlaciona logs com traces:</p>
<p><strong>Python com Python Logging</strong>:</p>
<pre><code class="language-python">import logging
from pythonjsonlogger import jsonlogger
from jaeger_client import Config
Configurar JSON logging
logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(logHandler)
logger.setLevel(logging.INFO)
config = Config(
config={
'sampler': {'type': 'const', 'param': 1},
'local_agent': {'reporting_host': 'localhost', 'reporting_port': 6831},
},
service_name='meu-servico',
)
tracer = config.initialize_tracer()
@app.route('/operacao')
def operacao():
span = tracer.start_span('operacao-principal')
Adicionar trace_id ao contexto de logging
logger.info('Iniciando operação', extra={
'trace_id': span.context.trace_id,
'span_id': span.context.span_id,
})
... seu código ...
span.finish()</code></pre>
<p>Resultado nos logs: <code>{"trace_id": 123456, "span_id": 789, "message": "Iniciando operação"}</code>. Agora você pode filtrar logs por trace_id na mesma ferramenta que filtra traces.</p>
<h2>Conclusão</h2>
<p>Três aprendizados principais que você deve levar consigo:</p>
<ol>
<li><strong>Distributed Tracing é não-negociável em microsserviços</strong>: sem ele, você está navegando às cegas. Traces conectam eventos isolados em um contexto único, permitindo investigação rápida de problemas e identificação de gargalos reais.</li>
</ol>
<ol>
<li><strong>Jaeger e Zipkin diferem em filosofia, não em core function</strong>: Jaeger é mais flexível e componentizado (melhor para ambientes complexos), Zipkin é mais leve e direto (melhor para começar simples). Ambos resolvem o problema. Escolha com base no seu ecossistema, não por modismo.</li>
</ol>
<ol>
<li><strong>Sampling e baggage são a base de um tracing efetivo em produção</strong>: coletar tudo é inviável e caro. Implementar sampling correto (10-25% em produção) reduz custos drasticamente. Propagar contexto de negócio (user_id, tenant_id) através de baggage permite correlacionar eventos entre serviços em nível de lógica de negócio, não apenas técnico.</li>
</ol>
<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://zipkin.io/" target="_blank" rel="noopener noreferrer">Documentação oficial do Zipkin</a></li>
<li><a href="https://opentelemetry.io/" target="_blank" rel="noopener noreferrer">OpenTelemetry - Padrão moderno de observabilidade</a></li>
<li><a href="https://spring.io/projects/spring-cloud-sleuth" target="_blank" rel="noopener noreferrer">Spring Cloud Sleuth - Distributed Tracing para Spring</a></li>
<li><a href="https://www.w3.org/TR/trace-context/" target="_blank" rel="noopener noreferrer">W3C Trace Context Specification</a></li>
</ul>
<p><!-- FIM --></p>