Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Construir um agente ReAct que diagnostica incidentes P1 correlacionando logs, métricas e traces
Implementar buscar_traces() que retorna correlação realista entre serviços
Implementar sugerir_fix() que usa Claude para gerar plano de remediação estruturado
Analisar o incidente do lab: pool de conexões PostgreSQL → deadlock → cascata para api-gateway
Completar os 2 TODOs do starter: correlação de traces e fix automático
Por que isso importa
Em sistemas distribuídos, um incidente raramente tem causa única e visível. O incidente do lab é típico: o api-gateway retorna 503, mas o problema real está no PostgreSQL — um deadlock causado por slow queries sem índice que esgotou o pool de conexões do checkout-service, que por sua vez fez o api-gateway abrir o circuit breaker.
Um humano leva 20-45 minutos para correlacionar isso. O agente ReAct faz em segundos, porque pode chamar buscar_logs, obter_metricas, buscar_traces e verificar_dependencias em sequência, integrando todas as evidências em um diagnóstico coerente.
Conceitos Fundamentais
O Incidente do Lab
INCIDENTE = {
'id': 'INC-2024-001',
'titulo': 'API Gateway retornando 503 em 40% das requisições',
'severidade': 'P1',
'servicos_afetados': ['api-gateway', 'checkout-service', 'payment-service'],
}A cadeia de causa raiz:
PostgreSQL (max_connections=100, slow_queries=5, deadlock)
↓ esgotou pool
checkout-service (db_connections=10/10, waiting=25, p99=12s)
↓ timeout + error rate 65%
api-gateway (circuit breaker OPEN, error_rate=40%, 503s)O payment-service está OK (error_rate=2%) — confirmando que o problema não é infra geral.
TODO 1: buscar_traces(trace_id, servicos)
Distributed tracing (Jaeger, Tempo, X-Ray) correlaciona chamadas entre serviços por trace_id. O mock retorna uma correlação realista:
def buscar_traces(trace_id: str = 'recentes', servicos: list = None) -> str:
# Mock de resposta Jaeger-like
if servicos and 'checkout-service' in servicos:
return '''Traces correlacionados:
trace_id: abc123 (P99)
api-gateway → checkout-service: 8500ms [TIMEOUT]
checkout-service → postgres: 8200ms [SLOW QUERY + DEADLOCK]
trace_id: def456 (típico)
api-gateway → checkout-service: 250ms [OK]
checkout-service → postgres: 80ms [OK]
ANÁLISE: latência 34x acima da baseline. Gargalo: postgres.
Operação lenta: SELECT * FROM orders WHERE user_id=? (sem índice em user_id)'''
return '''Nenhum trace anômalo para os serviços especificados.
Todos os spans dentro do SLO (<500ms p99).'''Na integração real com Jaeger:
import requests
def buscar_traces_jaeger(servico: str, limite_ms: int = 5000) -> str:
response = requests.get(
'http://jaeger:16686/api/traces',
params={'service': servico, 'minDuration': f'{limite_ms}ms', 'limit': 10}
)
traces = response.json()['data']
return formatar_traces(traces)TODO 2: sugerir_fix(causa_raiz, servicos_afetados, urgencia)
A função já existe no starter e usa o LLM para gerar o plano. O stub não precisa de implementação — o que precisamos é completar o trace do mock com dados mais ricos. A função usa Claude diretamente:
def sugerir_fix(causa_raiz: str, servicos_afetados: list = None, urgencia: str = 'imediata') -> str:
response = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=600,
messages=[{
'role': 'user',
'content': f'''Gere um plano de remediação ESTRUTURADO para este incidente:
Causa raiz: {causa_raiz}
Serviços afetados: {', '.join(servicos_afetados or [])}
Urgência: {urgencia}
Formato obrigatório:
## Ações Imediatas (próximos 15 min)
- [lista numerada]
## Fix de Curto Prazo (próximas 4 horas)
- [lista numerada]
## Prevenção de Longo Prazo (próximo sprint)
- [lista numerada]
## Métricas de Verificação
- [como confirmar que o incidente foi resolvido]''',
}],
)
return response.content[0].textAprofundamento Técnico
ReAct em Diagnóstico de Incidentes
O padrão ReAct (Reasoning + Acting) é especialmente eficaz em troubleshooting porque o agente pode adaptar sua investigação baseado no que encontra:
Thought: "O api-gateway está com 503. Vou começar pelos logs."
Action: buscar_logs(servico='api-gateway', nivel='ERROR')
Observation: "circuit breaker OPEN for checkout-service (50% error rate)"
Thought: "Circuit breaker aberto → problema vem do checkout-service."
Action: buscar_logs(servico='checkout-service', nivel='ERROR')
Observation: "database connection pool exhausted; deadlock detected on table orders"
Thought: "Pool de DB esgotado + deadlock. Vou verificar as métricas do postgres."
Action: obter_metricas(servico='postgres')
Observation: {"active_connections": 100, "max_connections": 100, "long_queries": 5}
Thought: "100/100 conexões. Limite atingido. Vou buscar traces para confirmar."
Action: buscar_traces(trace_id='recentes', servicos=['checkout-service', 'postgres'])
Observation: "SELECT * FROM orders WHERE user_id=? 8200ms — sem índice"
Thought: "Causa raiz: query sem índice → deadlock → pool esgotado → cascata."
Action: sugerir_fix(causa_raiz='Query sem índice em orders.user_id causou deadlock...')O agente não vai em linha reta — ele usa cada observação para decidir a próxima ação.
Correlação: A Diferença entre Sintoma e Causa Raiz
Sintoma observado | Indicador real |
api-gateway 503s | circuit breaker aberto |
circuit breaker OPEN | checkout-service error_rate > 50% |
checkout-service erros | pool DB esgotado |
pool DB esgotado | slow queries + deadlock |
slow queries | query sem índice em user_id |
A causa raiz é a query sem índice — não o api-gateway, não o checkout-service. Sem correlação, você restart o pod errado.
Circuit Breaker como Sinal Diagnóstico
O log do api-gateway:
14:32:02 WARN circuit breaker OPEN for checkout-service (50% error rate)Isso é crucial: o circuit breaker OPEN significa que o api-gateway parou de tentar chamar o checkout-service após 50% de erros. É um sinal de proteção — e de que o problema upstream é severo o suficiente para acionar o breaker.
Exemplos Anotados
Exemplo 1: Loop do Agente Diagnóstico Completo
import anthropic, json, os
client = anthropic.Anthropic()
TOOLS_DIAG = [
{'name': 'buscar_logs', ...},
{'name': 'obter_metricas', ...},
{'name': 'buscar_traces', ...},
{'name': 'verificar_dependencias', ...},
{'name': 'sugerir_fix', ...},
]
def agente_diagnostico(incidente: dict) -> str:
descricao = f"""
INCIDENTE {incidente['id']} — {incidente['titulo']}
Severidade: {incidente['severidade']}
Serviços afetados: {', '.join(incidente['servicos_afetados'])}
Diagnostique a causa raiz e sugira o plano de remediação.
"""
mensagens = [{'role': 'user', 'content': descricao}]
for _ in range(10): # máximo 10 iterações
response = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=1500,
tools=TOOLS_DIAG,
# instrução ReAct explícita
system='Você é um SRE sênior. Use o padrão ReAct: Thought (analise) → Action (tool) → Observation (resultado). Identifique a causa raiz ANTES de sugerir fix.',
messages=mensagens,
)
if response.stop_reason == 'end_turn':
return next((b.text for b in response.content if hasattr(b, 'text')), 'Diagnóstico concluído.')
tool_blocks = [b for b in response.content if b.type == 'tool_use']
if not tool_blocks:
break
mensagens.append({'role': 'assistant', 'content': response.content})
results = []
for block in tool_blocks:
resultado = executar_tool_diag(block.name, block.input)
results.append({
'type': 'tool_result',
'tool_use_id': block.id,
'content': resultado,
})
mensagens.append({'role': 'user', 'content': results})
return 'Diagnóstico interrompido: máximo de iterações atingido'Exemplo 2: TODO 1 — buscar_traces com dados ricos
def buscar_traces(trace_id: str = 'recentes', servicos: list = None) -> str:
# Dados mock de traces Jaeger-like
traces_db = [
{
'trace_id': 'abc123',
'spans': [
{'service': 'api-gateway', 'operation': 'POST /checkout', 'duration_ms': 8500, 'status': 'error'},
{'service': 'checkout-service', 'operation': 'createOrder', 'duration_ms': 8200, 'status': 'error'},
{'service': 'postgres', 'operation': 'SELECT orders', 'duration_ms': 8100, 'status': 'slow'},
]
}
]
resultado = []
for trace in traces_db:
resultado.append(f"trace_id: {trace['trace_id']}")
for span in trace['spans']:
status_icon = '✓' if span['status'] == 'ok' else '⚠' if span['status'] == 'slow' else '✗'
resultado.append(f" {status_icon} {span['service']} → {span['operation']}: {span['duration_ms']}ms [{span['status']}]")
return '\n'.join(resultado) or 'Nenhum trace encontrado'Padrões e Armadilhas
Padrões
Padrão 1: Começar pelos logs do serviço que disparou o alerta, não pela causa raiz O alerta vem do api-gateway → agente começa por api-gateway → descobre circuit breaker → vai para checkout-service. Não assuma a causa raiz antes de investigar.
Padrão 2: verificar_dependencias antes de sugerir_fix
# Sequência correta
buscar_logs → obter_metricas → buscar_traces → verificar_dependencias → sugerir_fix
verificar_dependencias confirma que o postgres está DEGRADADO — sem isso, o fix pode ser impreciso.Padrão 3: sugerir_fix com causa_raiz específica
# VAGO — gera plano genérico
sugerir_fix(causa_raiz='banco de dados com problemas')
# ESPECÍFICO — gera plano acionável
sugerir_fix(
causa_raiz='Query SELECT * FROM orders WHERE user_id=? sem índice causa slow query 8s → deadlock → pool esgotado (100/100 conexões)',
servicos_afetados=['checkout-service', 'postgres'],
urgencia='imediata'
)Armadilhas
⚠️ Armadilha 1: Parar no primeiro serviço com erros (api-gateway) O api-gateway tem erros mas não é a causa raiz — é a vítima. Sempre siga a cadeia de dependências até encontrar o serviço sem dependências problemáticas upstream.
⚠️ Armadilha 2: Confiança excessiva no diagnóstico do LLM
# Sempre verificar empiricamente com métricas
diagnostico_llm = "causa raiz: api-gateway sobrecarregado"
metricas = obter_metricas('api-gateway')
# → error_rate=40%, rps=1200 (normal), p99=8500ms
# Métricas confirmam: api-gateway é vítima, não causa⚠️ Armadilha 3: sugerir_fix chamado muito cedo (antes de verificar_dependencias) Se o agente chamar sugerir_fix antes de descobrir que o postgres está em 100/100 conexões, o fix vai sugerir “reiniciar o checkout-service” em vez de “adicionar índice em orders.user_id + aumentar max_connections”.
Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
2 TODOs no starter.py:
TODO 1 — buscar_traces(trace_id, servicos) (linha ~123): Substitua o placeholder por uma resposta mock estruturada:
def buscar_traces(trace_id: str = 'recentes', servicos: list = None) -> str:
return '''Traces distribuídos correlacionados:
trace_id: abc123 [P99 — anomalia]
api-gateway → checkout-service: 8500ms [TIMEOUT]
checkout-service → postgres: 8200ms [SLOW QUERY]
Operação: SELECT * FROM orders WHERE user_id=? (sem índice)
trace_id: def456 [baseline normal]
api-gateway → checkout-service: 250ms [OK]
checkout-service → postgres: 80ms [OK]
Diagnóstico: latência 34x acima da baseline. Gargalo identificado: postgres (query sem índice)'''TODO 2 — a função sugerir_fix já usa LLM real — verifique que o prompt inclui o formato estruturado com seções “Ações Imediatas”, “Fix de Curto Prazo” e “Prevenção de Longo Prazo”.
Rode com python starter.py e observe o agente correlacionar logs → métricas → traces → dependências → fix.
Agora você está pronto para o lab.