Distributed Tracing em Python Puro: O Sistema Que Rastreia Requisicoes Entre Multiplos Servicos e Conecta Logs Automaticamente (Sem Jaeger, Sem OpenTelemetry)
Voce ja passou pela seguinte situacao: um usuario reporta um erro, voce abre os logs do servico A e encontra a mensagem “Erro ao processar requisicao”. Mas qual requisicao? De onde veio? Para onde foi? Voce pula para o servico B, depois para o C, e tem tres terminais abertos tentando correlacionar timestamps manualmente, torcendo para que os relogios estejam sincronizados.
Se isso te parece familiar, bem-vindo ao clube. Esse e o pesadelo classico de quem trabalha com sistemas distribuidos sem distributed tracing.
A boa noticia? Voce nao precisa de Jaeger, Zipkin, OpenTelemetry SDK ou qualquer dependencia pesada para resolver isso. Em Python puro, com cerca de 150 linhas de codigo, e possivel implementar um sistema de rastreamento distribuido que gera trace IDs, propaga contexto entre servicos e agrupa logs automaticamente.
Neste artigo, vou te mostrar como construi um tracer minimalista que salvou minha pele em producao mais vezes do que posso contar.
O Problema Real: Logs Sem Contexto Sao Inuteis
Vamos ser honestos: logs sao essenciais. Mas logs sem correlacao sao como paginas de um livro embaralhadas. Voce tem todas as informacoes, mas nenhuma narrativa.
Considere este cenario real que enfrentei semana passada:
- Usuario clica em “Finalizar Compra”
- API Gateway recebe a requisicao e valida o token
- Servico de Pedidos cria o pedido no banco
- Servico de Pagamento processa a cobranca
- Servico de Estoque reserva os produtos
- Servico de Notificacao envia o email de confirmacao
Se o email nao chega, onde voce comeca a investigar? Sem um identificador unico que percorra toda essa cadeia, voce esta basicamente adivinhando.

A Solucao: Trace ID Propagation
O conceito e simples: cada requisicao recebe um ID unico (trace_id) na origem. Esse ID e propagado atraves de todos os servicos envolvidos, e cada log gerado inclui esse identificador.
Na pratica, funciona assim:
- Geracao: O servico de entrada cria um UUID unico
- Propagacao: O ID e passado via header HTTP (X-Trace-ID)
- Contexto: Cada servico extrai o ID do header e o inclui em todos os logs
- Agregacao: Ferramentas de log agregam mensagens pelo trace_id
A beleza dessa abordagem e que ela nao exige nenhuma biblioteca externa. Voce so precisa de tres ingredientes:
contextvars(nativo do Python 3.7+)- Headers HTTP (qualquer framework web suporta)
- Um logger minimamente customizado
Implementacao em Python Puro
Vamos ao codigo. Primeiro, precisamos de um mecanismo para armazenar o trace_id no contexto da requisicao atual. Python tem o modulo contextvars (disponivel desde 3.7) que e perfeito para isso:
import contextvars
import uuid
import logging
import time
from functools import wraps
# Variavel de contexto para armazenar o trace_id
trace_id_var = contextvars.ContextVar('trace_id', default=None)
class TracedLogger:
def __init__(self, name):
self.logger = logging.getLogger(name)
def _format_message(self, msg):
trace_id = trace_id_var.get()
if trace_id:
return f"[{trace_id}] {msg}"
return msg
def info(self, msg, *args, **kwargs):
self.logger.info(self._format_message(msg), *args, **kwargs)
def error(self, msg, *args, **kwargs):
self.logger.error(self._format_message(msg), *args, **kwargs)
def warning(self, msg, *args, **kwargs):
self.logger.warning(self._format_message(msg), *args, **kwargs)
def debug(self, msg, *args, **kwargs):
self.logger.debug(self._format_message(msg), *args, **kwargs)
Simples, nao? Agora todo log gerado por esse logger incluira automaticamente o trace_id se ele estiver no contexto. Zero dependencias externas.
Middleware para Propagacao Automatica
Para frameworks web como Flask ou FastAPI, podemos criar middlewares que extraem o trace_id dos headers HTTP e o colocam no contexto:
# Middleware para Flask
from flask import request
def trace_middleware(app):
@app.before_request
def before_request():
# Extrai trace_id do header ou gera um novo
trace_id = request.headers.get('X-Trace-ID')
if not trace_id:
trace_id = str(uuid.uuid4())[:8]
trace_id_var.set(trace_id)
@app.after_request
def after_request(response):
# Adiciona trace_id na resposta para debug
trace_id = trace_id_var.get()
if trace_id:
response.headers['X-Trace-ID'] = trace_id
return response
E para FastAPI, o padrao e similar:
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
class TraceMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
trace_id = request.headers.get('X-Trace-ID')
if not trace_id:
trace_id = str(uuid.uuid4())[:8]
trace_id_var.set(trace_id)
response = await call_next(request)
response.headers['X-Trace-ID'] = trace_id
return response
Para chamadas HTTP entre servicos, precisamos propagar o trace_id nos headers de saida:
import urllib.request
import urllib.error
class TracedHTTPClient:
@staticmethod
def request(url, method='GET', data=None, headers=None):
headers = headers or {}
# Adiciona trace_id aos headers se existir no contexto
trace_id = trace_id_var.get()
if trace_id:
headers['X-Trace-ID'] = trace_id
req = urllib.request.Request(
url, data=data, headers=headers, method=method
)
try:
with urllib.request.urlopen(req) as response:
return response.read(), response.status
except urllib.error.HTTPError as e:
return e.read(), e.code
Span: Medindo Duracao de Operacoes
Alem do trace_id, e util medir quanto tempo cada operacao leva. Isso nos permite identificar gargalos. Vamos criar um decorador de span:
def traced_span(operation_name):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
logger = TracedLogger(func.__module__)
start_time = time.time()
logger.info(f"Iniciando operacao: {operation_name}")
try:
result = func(*args, **kwargs)
duration = time.time() - start_time
logger.info(
f"Operacao {operation_name} concluida em {duration:.3f}s"
)
return result
except Exception as e:
duration = time.time() - start_time
logger.error(
f"Operacao {operation_name} falhou apos {duration:.3f}s: {e}"
)
raise
return wrapper
return decorator
Uso pratico:
@traced_span("processar_pagamento")
def processar_pagamento(pedido_id, valor):
# Simula processamento
time.sleep(0.5)
return {"status": "aprovado", "pedido_id": pedido_id}

Exemplo Completo: Sistema de Pedidos com 3 Servicos
Vamos juntar tudo em um exemplo pratico. Imagine tres servicos: API Gateway, Servico de Pedidos e Servico de Pagamento.
API Gateway (porta 8000)
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
trace_middleware(app)
logger = TracedLogger('gateway')
@app.route('/pedidos', methods=['POST'])
def criar_pedido():
logger.info("Recebendo requisicao para criar pedido")
dados = request.get_json()
client = TracedHTTPClient()
response, status = client.request(
'http://servico-pedidos:8001/pedidos',
method='POST',
data=json.dumps(dados).encode(),
headers={'Content-Type': 'application/json'}
)
if status == 201:
logger.info("Pedido criado com sucesso")
return jsonify(json.loads(response)), 201
else:
logger.error(f"Falha ao criar pedido: status {status}")
return jsonify({"erro": "Falha ao processar pedido"}), 500
if __name__ == '__main__':
app.run(port=8000)
Servico de Pedidos (porta 8001)
app = Flask(__name__)
trace_middleware(app)
logger = TracedLogger('pedidos')
@app.route('/pedidos', methods=['POST'])
def processar_pedido():
logger.info("Processando novo pedido")
dados = request.get_json()
pedido_id = str(uuid.uuid4())[:8]
client = TracedHTTPClient()
response, status = client.request(
'http://servico-pagamento:8002/pagamentos',
method='POST',
data=json.dumps({
"pedido_id": pedido_id,
"valor": dados['valor']
}).encode(),
headers={'Content-Type': 'application/json'}
)
if status == 200:
logger.info(f"Pedido {pedido_id} aprovado")
return jsonify({
"pedido_id": pedido_id,
"status": "aprovado"
}), 201
else:
logger.error(f"Pagamento recusado para pedido {pedido_id}")
return jsonify({"erro": "Pagamento recusado"}), 400
Servico de Pagamento (porta 8002)
app = Flask(__name__)
trace_middleware(app)
logger = TracedLogger('pagamento')
@traced_span("validar_cartao")
def validar_cartao(valor):
time.sleep(0.3)
return True
@traced_span("processar_cobranca")
def processar_cobranca(pedido_id, valor):
time.sleep(0.2)
return {"transacao_id": str(uuid.uuid4())[:8]}
@app.route('/pagamentos', methods=['POST'])
def processar_pagamento():
logger.info("Processando pagamento")
dados = request.get_json()
pedido_id = dados['pedido_id']
valor = dados['valor']
logger.info(f"Validando cartao para pedido {pedido_id}")
if not validar_cartao(valor):
logger.warning(f"Cartao recusado para pedido {pedido_id}")
return jsonify({"erro": "Cartao recusado"}), 400
logger.info(f"Processando cobranca para pedido {pedido_id}")
resultado = processar_cobranca(pedido_id, valor)
logger.info(f"Pagamento aprovado: transacao {resultado['transacao_id']}")
return jsonify({"status": "aprovado", **resultado}), 200
Analisando Logs com Trace ID
Agora, quando voce faz uma requisicao ao gateway, todos os logs dos tres servicos terao o mesmo trace_id. Veja como isso aparece na pratica:
# Requisicao de exemplo
curl -X POST http://localhost:8000/pedidos \
-H "Content-Type: application/json" \
-d '{"produto_id": 123, "valor": 99.90}'
Logs gerados (todos com o mesmo trace_id a1b2c3d4):
# API Gateway
[21:00:01] [a1b2c3d4] INFO gateway: Recebendo requisicao para criar pedido
[21:00:01] [a1b2c3d4] INFO gateway: Pedido criado com sucesso
# Servico de Pedidos
[21:00:01] [a1b2c3d4] INFO pedidos: Processando novo pedido
[21:00:01] [a1b2c3d4] INFO pedidos: Pedido x7y8z9w0 aprovado
# Servico de Pagamento
[21:00:01] [a1b2c3d4] INFO pagamento: Processando pagamento
[21:00:01] [a1b2c3d4] INFO pagamento: Iniciando operacao: validar_cartao
[21:00:01] [a1b2c3d4] INFO pagamento: Operacao validar_cartao concluida em 0.301s
[21:00:01] [a1b2c3d4] INFO pagamento: Iniciando operacao: processar_cobranca
[21:00:01] [a1b2c3d4] INFO pagamento: Operacao processar_cobranca concluida em 0.201s
[21:00:01] [a1b2c3d4] INFO pagamento: Pagamento aprovado: transacao m4n5o6p7
Para investigar qualquer problema, basta filtrar pelo trace_id:
grep "a1b2c3d4" /var/log/servicos/*.log
E voce tem a historia completa da requisicao, de ponta a ponta. Sem adivinhacao, sem correlacao manual de timestamps.
Agregacao Automatica com Python
Para facilitar ainda mais a analise, podemos criar um script que agrupa logs por trace_id automaticamente:
import re
from collections import defaultdict
def agrupar_logs_por_trace(log_files):
traces = defaultdict(list)
pattern = re.compile(r'\[([a-f0-9]{8})\]')
for log_file in log_files:
with open(log_file, 'r') as f:
for line in f:
match = pattern.search(line)
if match:
trace_id = match.group(1)
traces[trace_id].append(line.strip())
for trace_id in traces:
traces[trace_id].sort()
return dict(traces)
def mostrar_trace(trace_id, traces):
if trace_id not in traces:
print(f"Trace {trace_id} nao encontrado")
return
print(f"\n{'='*60}")
print(f"TRACE: {trace_id}")
print(f"{'='*60}")
for log_line in traces[trace_id]:
print(log_line)
print(f"{'='*60}\n")
# Uso
log_files = [
'/var/log/gateway.log',
'/var/log/pedidos.log',
'/var/log/pagamento.log'
]
traces = agrupar_logs_por_trace(log_files)
mostrar_trace('a1b2c3d4', traces)
Visualizacao ASCII de Spans
Para ter uma visao mais clara da cadeia de chamadas e suas duracoes, podemos gerar uma visualizacao ASCII direto no terminal:
def visualizar_trace_ascii(trace_id, traces):
logs = traces.get(trace_id, [])
print(f"\nTrace: {trace_id}\n")
spans = []
for log in logs:
if 'Iniciando operacao:' in log:
op_name = log.split('Iniciando operacao:')[1].strip()
spans.append({'name': op_name})
elif 'concluida em' in log:
duration = log.split('concluida em')[1].split('s')[0].strip()
if spans:
spans[-1]['duration'] = float(duration)
for span in spans:
duration = span.get('duration', 0)
bar_length = int(duration * 50)
bar = '#' * bar_length + '-' * (50 - bar_length)
print(f"{span['name']:<25} |{bar}| {duration:.3f}s")
# Saida:
# Trace: a1b2c3d4
#
# validar_cartao |###############-----------------------------------| 0.301s
# processar_cobranca |##########----------------------------------------| 0.201s
Isso e incrivelmente util quando voce esta investigando um incidente as 3 da manha e precisa de uma visao rapida do que aconteceu.
Integracao com Logs Estruturados (JSON)
Para ambientes de producao, logs estruturados (JSON) sao muito mais faceis de processar. Vamos adaptar nosso logger:
import json
from datetime import datetime
class StructuredTracedLogger:
def __init__(self, service_name):
self.service_name = service_name
def _log(self, level, msg, **extra_fields):
trace_id = trace_id_var.get()
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'level': level,
'service': self.service_name,
'trace_id': trace_id,
'message': msg,
**extra_fields
}
print(json.dumps(log_entry))
def info(self, msg, **extra):
self._log('INFO', msg, **extra)
def error(self, msg, **extra):
self._log('ERROR', msg, **extra)
def warning(self, msg, **extra):
self._log('WARNING', msg, **extra)
# Uso
logger = StructuredTracedLogger('pagamento')
logger.info("Processando pagamento", pedido_id="x7y8z9w0", valor=99.90)
# Saida:
# {"timestamp": "2026-07-28T21:00:01", "level": "INFO",
# "service": "pagamento", "trace_id": "a1b2c3d4",
# "message": "Processando pagamento",
# "pedido_id": "x7y8z9w0", "valor": 99.90}
Com logs estruturados, ferramentas como ELK Stack, Loki ou ate mesmo jq tornam-se extremamente poderosas:
# Filtrar todos os logs de um trace especifico
cat /var/log/servicos/*.log | jq 'select(.trace_id == "a1b2c3d4")'
# Contar erros por servico
cat /var/log/servicos/*.log | \
jq 'select(.level == "ERROR")' | \
jq -s 'group_by(.service) | map({service: .[0].service, count: length})'
# Top 10 traces mais lentos
cat /var/log/servicos/*.log | \
jq -s 'group_by(.trace_id) | map({trace: .[0].trace_id, logs: length}) | sort_by(.logs) | reverse | .[:10]'
Sampling Inteligente: Nem Todo Trace Precisa Ser Armazenado
Em producao com alto volume, armazenar 100% dos traces e caro e desnecessario. Um sampler inteligente mantem os traces importantes e descarta os rotineiros:
import random
class SmartSampler:
def __init__(self, sample_rate=0.1, always_keep_errors=True):
self.sample_rate = sample_rate
self.always_keep_errors = always_keep_errors
self._has_error = False
def should_sample(self):
return random.random() < self.sample_rate
def mark_error(self):
self._has_error = True
def should_keep_trace(self):
# Sempre mantem traces com erros
if self.always_keep_errors and self._has_error:
return True
# Amostragem aleatoria para o resto
return self.should_sample()
# Uso no middleware
sampler = SmartSampler(sample_rate=0.1)
@app.errorhandler(500)
def handle_error(e):
sampler.mark_error()
return jsonify({"error": str(e)}), 500
A regra e simples: traces com erros sao sempre armazenados. Os outros seguem uma taxa de amostragem configuravel. Voce mantem 100% da visibilidade sobre problemas e reduz drasticamente o custo de armazenamento.
Limitacoes e Quando Usar Ferramentas Especializadas
Essa implementacao minimalista e perfeita para:
- Projetos pequenos a medios (ate 20 servicos)
- Prototipagem rapida e validacao de arquitetura
- Ambientes com restricoes de dependencias
- Aprendizado dos conceitos fundamentais de distributed tracing
Para sistemas maiores, considere migrar para:
- OpenTelemetry: Padrao da industria, suporte a multiplas linguagens, exportadores para Jaeger, Zipkin, Grafana Tempo
- Jaeger: Visualizacao avancada de traces com timeline interativa
- Zipkin: Alternativa leve ao Jaeger, facil de operar
- Grafana Tempo: Integracao nativa com Grafana e Loki
A boa noticia? Os conceitos que voce aprendeu aqui (trace_id, span, propagacao de contexto, sampling) sao exatamente os mesmos usados por essas ferramentas. A migracao e muito mais suave quando voce entende os fundamentos.
Conclusao: Simplicidade Que Funciona
Distributed tracing nao precisa ser complicado. Com contextvars, um logger adaptado e headers HTTP, voce tem 80% do valor de solucoes enterprise com 1% da complexidade.
O que construímos aqui:
- TracedLogger - Logger que inclui trace_id automaticamente
- TraceMiddleware - Extrai/gera trace_id em cada requisicao
- TracedHTTPClient - Propaga trace_id em chamadas entre servicos
- traced_span - Decorador que mede duracao de operacoes
- SmartSampler - Sampling inteligente que prioriza erros
- Agregador de logs - Agrupa e visualiza traces completos
Tudo isso sem instalar uma unica dependencia externa. O melhor debug e aquele que voce entende de ponta a ponta.
Se voce curtiu esse post, da uma olhada nos artigos sobre Log Sampler Adaptativo e Log Fingerprint com SimHash que publicamos recentemente. Eles complementam perfeitamente o tracing que construímos hoje.
E voce? Qual foi o bug mais dificil de rastrear em um sistema distribuido? Conta ai nos comentarios.
Qual automacao de observabilidade voce quer ver no proximo post? Deixa sua sugestao e eu transformo em tutorial pratico.
