triagem de logs de erro em automações Python

Triagem de Logs de Erro em Automações Python: Pare de Caçar Falha no Escuro

Triagem de logs de erro em automações Python é o tipo de coisa que ninguém coloca no roadmap porque não fica bonito no print do LinkedIn. Mas, quando a automação falha às 7h13, manda três mensagens duplicadas, deixa um arquivo pela metade e ainda diz apenas Exception: failed, todo mundo vira religioso por alguns minutos.

Eu já perdi tempo demais abrindo log como quem revira lixo em dia quente: procurando um cheiro familiar, torcendo para achar a causa antes que alguém pergunte “mas isso não era automático?”. Automação sem triagem de erro não é automação. É uma aposta com cron.

Neste guia, vamos montar uma triagem de logs de erro em automações Python que transforma falhas soltas em casos investigáveis. Nada de dashboard milionário, nada de empilhar ferramenta para parecer maduro. A ideia é simples: capturar contexto, classificar erro, gerar resumo humano, manter histórico e apontar a próxima ação provável.

triagem de logs de erro em automações Python com alerta visual de falha
Erro sem contexto é só um bilhete malcriado deixado pelo seu próprio sistema.

O problema real: logs existem, mas ninguém entende rápido

O primeiro engano é achar que “ter log” resolve alguma coisa. Log cru é matéria-prima. Se ele não responde rapidamente o que quebrou, onde quebrou, qual dado estava envolvido e se já aconteceu antes, ele é apenas texto ocupando disco com autoestima.

Em automações pequenas, o caos começa humilde. Um script baixa relatórios, outro processa CSV, outro chama uma API, outro manda notificação. Cada um tem seu próprio jeito de escrever erro. Um imprime no terminal. Outro joga em arquivo. Outro engole exceção com um pass, que é basicamente esconder o cadáver debaixo do tapete e chamar isso de arquitetura.

A triagem resolve um gargalo específico: reduzir o tempo entre “algo falhou” e “sei o que fazer agora”. Ela não substitui observabilidade completa, mas cria uma camada prática para quem roda automações em servidor barato, VPS, computador doméstico, container ou qualquer ambiente onde o orçamento não veio com confete.

Se você gostou do raciocínio por trás de logs úteis, vale cruzar com o texto sobre observabilidade de logs para debugging em produção. Aqui, porém, vamos descer um degrau: menos filosofia, mais script que separa incêndio real de barulho.

O desenho da solução

A arquitetura que eu uso para triagem de logs de erro em automações Python tem cinco peças. Primeiro, cada automação escreve logs em JSON Lines, um evento por linha. Segundo, um coletor lê os arquivos recentes. Terceiro, um classificador identifica assinatura, severidade e recorrência. Quarto, um banco SQLite guarda o histórico. Quinto, um relatório em Markdown ou HTML mostra os casos do dia.

Por que JSON Lines? Porque é simples, legível, incremental e não exige subir um serviço novo. Dá para abrir no terminal, filtrar com ferramentas comuns e processar com Python sem drama. CSV para log estruturado envelhece mal; texto livre vira charada; banco direto dentro de cada automação cria acoplamento chato.

O fluxo fica assim:

  • A automação executa uma tarefa e registra eventos com job, step, status, duration_ms e context.
  • Quando ocorre exceção, o log inclui error_type, error_message, traceback e um correlation_id.
  • O triador calcula uma assinatura estável do erro para detectar repetição.
  • O histórico permite responder: “isso é novo ou é aquele velho teatro de sempre?”.
  • O relatório final sugere a próxima investigação.

Esse modelo combina bem com uma fila de automação com SQLite, porque cada job passa a deixar rastros consistentes. Quando a fila falha, você não quer um romance russo no terminal; quer um prontuário.

Padronize o log antes de tentar ser esperto

Antes de classificar erro, precisamos escrever erro direito. Esse é o pedaço menos glamouroso e mais importante. Um log ruim força o triador a adivinhar. E adivinhação em produção é como soldar cano com vela: pode até parecer iniciativa, mas você está criando outra emergência.

Comece com uma função pequena para emitir eventos JSON. Ela deve ser chata, previsível e usada por todos os scripts. Se você já tem automações antigas, não precisa reescrever tudo de uma vez. Coloque esse padrão nos pontos críticos: início de job, fim de job, chamada de API, leitura de arquivo, escrita de resultado e exceções.

import json
import time
import uuid
import traceback
from pathlib import Path

LOG_FILE = Path("logs/automations.jsonl")
RUN_ID = str(uuid.uuid4())

def log_event(job, step, status, **extra):
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    event = {
        "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "run_id": RUN_ID,
        "job": job,
        "step": step,
        "status": status,
        **extra,
    }
    with LOG_FILE.open("a", encoding="utf-8") as f:
        f.write(json.dumps(event, ensure_ascii=False) + "\n")

def log_exception(job, step, exc, **context):
    log_event(
        job,
        step,
        "error",
        error_type=type(exc).__name__,
        error_message=str(exc),
        traceback=traceback.format_exc(),
        context=context,
    )

Repare no run_id. Ele parece detalhe, mas salva vidas sociais. Sem um identificador de execução, você mistura eventos de rodadas diferentes e começa a culpar o script errado. O context também é essencial: nome do arquivo, endpoint chamado, ID do cliente, número da tentativa, tamanho do payload. Não coloque segredo ali. Token, senha, cookie e dado sensível precisam ser mascarados antes de chegar ao arquivo.

Capture contexto, não só exceção

O erro “timeout” sozinho não diz quase nada. Timeout em qual API? Depois de quantos segundos? Em qual tentativa? O payload era grande? A rede caiu ou o serviço remoto respondeu lento? Era o primeiro job do dia ou o décimo segundo depois de uma sequência de falhas?

Contexto é a diferença entre “reinicia aí” e “o fornecedor mudou o limite de requisições às 6h”. Se a automação fala com APIs externas, registre status HTTP, tempo de resposta, endpoint sem parâmetros sensíveis e um resumo do corpo de resposta. Se processa arquivo, registre caminho, tamanho, quantidade de linhas e hash. Se escreve no banco, registre tabela, operação e quantidade afetada.

def summarize_file(path):
    p = Path(path)
    return {
        "file_name": p.name,
        "size_bytes": p.stat().st_size if p.exists() else None,
        "exists": p.exists(),
    }

try:
    info = summarize_file("entrada/clientes.csv")
    log_event("importar_clientes", "read_input", "started", context=info)
    # processar_csv(...)
except Exception as exc:
    log_exception("importar_clientes", "read_input", exc, context=info)
    raise

Eu prefiro repetir um pouco de contexto no log a depender de memória externa. Quando o erro acontece, o arquivo pode ter mudado, a API pode ter voltado, o container pode ter morrido. Log bom é cápsula do tempo: congela o cenário do crime técnico.

Crie assinaturas de erro para parar de redescobrir o óbvio

Depois que os logs estão estruturados, entra a triagem. A primeira coisa é transformar cada erro em uma assinatura. Não use a mensagem inteira, porque ela pode conter IDs, horários, nomes de arquivo e outros detalhes variáveis. Use campos estáveis: nome do job, step, tipo da exceção e a primeira linha útil do traceback.

Essa assinatura vira uma chave. Com ela, o sistema sabe se está diante de um erro novo, recorrente ou resolvido que voltou para assombrar o calendário. E aqui mora um ganho absurdo: parar de investigar o mesmo erro do zero toda semana.

import hashlib
import re

def normalize_message(message):
    message = re.sub(r"\b\d+\b", "N", message or "")
    message = re.sub(r"/[\w./-]+", "/PATH", message)
    return message[:160]

def error_signature(event):
    parts = [
        event.get("job", ""),
        event.get("step", ""),
        event.get("error_type", ""),
        normalize_message(event.get("error_message", "")),
    ]
    raw = "|".join(parts)
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]

A normalização acima é simples de propósito. Ela remove números e caminhos absolutos para evitar que o mesmo erro gere assinatura diferente só porque o ID mudou. Em ambientes mais complexos, você pode extrair frames do traceback e ignorar linhas de bibliotecas. Mas comece pequeno. Sofisticação prematura é só procrastinação usando crachá.

Guarde o histórico em SQLite

Sem histórico, triagem vira horóscopo. Você até consegue dizer que algo parece grave, mas não consegue provar frequência, primeira ocorrência, última ocorrência ou impacto. SQLite resolve isso com elegância suficiente para 90% das automações pessoais e internas.

A tabela mínima precisa guardar assinatura, job, step, tipo, mensagem, primeira ocorrência, última ocorrência, contador e um exemplo de contexto. Se quiser melhorar, adicione status de resolução, anotação manual e link para issue. Mas não complique antes de usar.

import sqlite3

SCHEMA = """
CREATE TABLE IF NOT EXISTS error_cases (
    signature TEXT PRIMARY KEY,
    job TEXT NOT NULL,
    step TEXT NOT NULL,
    error_type TEXT NOT NULL,
    error_message TEXT NOT NULL,
    first_seen TEXT NOT NULL,
    last_seen TEXT NOT NULL,
    occurrences INTEGER NOT NULL,
    sample_context TEXT
);
"""

def connect_db(path="logs/error_triage.sqlite3"):
    conn = sqlite3.connect(path)
    conn.execute(SCHEMA)
    return conn

def upsert_error(conn, event):
    sig = error_signature(event)
    conn.execute(
        """
        INSERT INTO error_cases
        (signature, job, step, error_type, error_message, first_seen, last_seen, occurrences, sample_context)
        VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)
        ON CONFLICT(signature) DO UPDATE SET
            last_seen = excluded.last_seen,
            occurrences = occurrences + 1,
            sample_context = excluded.sample_context
        """,
        (
            sig,
            event.get("job", ""),
            event.get("step", ""),
            event.get("error_type", ""),
            event.get("error_message", ""),
            event.get("ts", ""),
            event.get("ts", ""),
            json.dumps(event.get("context", {}), ensure_ascii=False),
        ),
    )
    conn.commit()
    return sig

Esse banco vira memória operacional. Ele também ajuda a identificar regressão: se uma assinatura ficou semanas sem aparecer e voltou hoje, isso merece mais atenção do que um erro conhecido que ocorre todo domingo porque uma API de terceiros tira cochilo.

Classifique severidade com regras explícitas

Nem todo erro merece o mesmo volume de sirene. Uma falha em job experimental às 3h pode ir para o relatório diário. Uma falha em backup, cobrança, segurança ou envio para cliente precisa pular na sua frente. A triagem de logs de erro em automações Python deve ter regras explícitas, não um “vou sentir no coração”. Coração é ótimo para música ruim, péssimo para incidente.

Eu costumo classificar em três níveis:

  • critical: perda de dados, duplicação, segurança, backup, pagamento, credencial, falha em cadeia.
  • warning: job falhou mas pode tentar depois, API externa instável, arquivo ausente esperado ocasionalmente.
  • info: ruído conhecido, falha em tarefa opcional, ambiente de teste, erro já mitigado.
CRITICAL_JOBS = {"backup_diario", "cobranca", "sync_credenciais"}
CRITICAL_ERRORS = {"PermissionError", "IntegrityError"}

def classify_severity(event, occurrences):
    job = event.get("job")
    error_type = event.get("error_type")
    message = (event.get("error_message") or "").lower()

    if job in CRITICAL_JOBS:
        return "critical"
    if error_type in CRITICAL_ERRORS:
        return "critical"
    if "duplicate" in message or "corrupt" in message:
        return "critical"
    if occurrences >= 5:
        return "warning"
    return "info"

O ponto aqui não é acertar tudo. É tornar a decisão auditável. Quando a regra errar, você ajusta. Quando a regra não existe, você vira refém do humor de quem acordou primeiro.

Gere um relatório que alguém realmente leia

O relatório não precisa ser bonito. Precisa ser útil em dois minutos. Eu gosto de separar por severidade, mostrar erros novos primeiro e destacar recorrências. Para cada caso, inclua job, step, mensagem normalizada, ocorrências, primeira e última ocorrência, contexto de amostra e sugestão de próxima ação.

Evite relatório que despeja traceback inteiro na primeira tela. Traceback é detalhe de investigação, não manchete. A manchete é: “backup_diario falhou em upload_s3 por PermissionError, primeira vez hoje, provável credencial ou política alterada”. Isso é informação. O resto é apêndice.

triagem de logs de erro em automações Python exibindo falha de autenticação
Uma falha de autenticação não deveria exigir arqueologia no terminal.
def suggest_next_action(case):
    msg = case["error_message"].lower()
    if "permission" in msg or case["error_type"] == "PermissionError":
        return "Verificar permissões, credenciais e mudanças recentes de política."
    if "timeout" in msg:
        return "Checar latência da API, limite de requisições e configurar backoff."
    if "no such file" in msg or "not found" in msg:
        return "Validar etapa anterior, caminho do arquivo e montagem de volume."
    return "Reproduzir com o mesmo contexto e adicionar regra específica de triagem."

def render_markdown(cases):
    lines = ["# Relatório de triagem de erros", ""]
    for case in cases:
        lines.append(f"## {case['severity'].upper()} - {case['job']} / {case['step']}")
        lines.append(f"- Tipo: `{case['error_type']}`")
        lines.append(f"- Ocorrências: {case['occurrences']}")
        lines.append(f"- Última vez: {case['last_seen']}")
        lines.append(f"- Mensagem: {case['error_message']}")
        lines.append(f"- Próxima ação: {suggest_next_action(case)}")
        lines.append("")
    return "\n".join(lines)

Se você mandar esse relatório por e-mail, Telegram, Slack ou salvar em HTML, tanto faz. Só não deixe preso em uma pasta que ninguém abre. Relatório que exige disciplina heroica para ser lido não é ferramenta; é decoração para culpa.

Inclua janelas de tempo e não escaneie o mundo toda vez

Um erro comum é fazer o triador ler todos os logs desde a invenção do boleto. Funciona no primeiro dia. Depois fica lento, duplica contagem e cria aquela sensação deliciosa de “acho que meu script de diagnóstico agora também precisa de diagnóstico”.

Use uma janela de tempo ou marque offset. Para uma rotina diária, processe apenas arquivos modificados nas últimas 24 ou 48 horas e grave o último timestamp processado. Se quiser robustez, grave também o nome do arquivo e a posição da última linha. Assim, você não perde evento quando o script roda no meio de um arquivo em crescimento.

from datetime import datetime, timedelta, timezone

def recent_events(log_path, hours=24):
    cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
    with Path(log_path).open(encoding="utf-8") as f:
        for line in f:
            event = json.loads(line)
            ts = datetime.fromisoformat(event["ts"].replace("Z", "+00:00"))
            if ts >= cutoff and event.get("status") == "error":
                yield event

Em ambientes com rotação de log, leia múltiplos arquivos. Em containers, pense no destino do volume. Em cron, lembre que diretório relativo muda conforme o contexto de execução. Sim, esse detalhe idiota já derrubou automação minha. Não foi meu momento mais elegante.

Transforme triagem em rotina operacional

A triagem só funciona se entrar no ciclo normal da automação. Rode ao final dos jobs principais, uma vez por hora ou no começo do expediente. A cadência depende do dano potencial. Backup e cobrança pedem alerta rápido. Relatório de leitura pessoal pode esperar.

Um exemplo simples de cron:

15 * * * * cd /srv/automacoes && /usr/bin/python3 triage_errors.py >> logs/triage.log 2>&1

Mas cron sozinho não é plano de operação. Defina o que acontece com cada severidade. Critical manda notificação imediata. Warning entra em relatório diário. Info fica no histórico e só aparece quando cruza limite de repetição. Sem isso, você cria alerta para tudo e, em duas semanas, seu cérebro cria filtro para nada. Parabéns, você automatizou a indiferença.

Para quem está montando uma central mais ampla, o artigo sobre central de produtividade aumentada com Python e SQLite mostra uma base boa para organizar rotinas, prioridades e filas. A triagem de erro pode virar mais uma etapa dessa central.

Erros que merecem tratamento especial

Alguns erros são tão comuns que valem regras específicas desde o começo. Timeout precisa registrar tentativa, tempo limite e endpoint. Rate limit precisa guardar cabeçalhos relevantes, como limite restante e tempo de reset. Falha de autenticação precisa diferenciar token expirado de permissão negada. Arquivo ausente precisa dizer se a etapa produtora rodou. Erro de parse precisa guardar uma amostra sanitizada da linha problemática.

Esse cuidado evita uma armadilha: tratar tudo como “bug no script”. Muitas falhas são ambientais. API fora do ar, disco cheio, permissão alterada, certificado vencido, DNS instável, volume não montado. Se o log não mostra ambiente, o desenvolvedor vira bode expiatório por padrão. Conveniente, mas burro.

Eu gosto de manter uma pequena tabela de padrões:

  • ConnectionError: verificar rede, DNS, proxy, firewall e status do serviço externo.
  • TimeoutError: verificar latência, payload, backoff e limite de concorrência.
  • PermissionError: verificar usuário do processo, permissões de arquivo e credenciais.
  • JSONDecodeError: salvar amostra sanitizada da resposta e checar se a API devolveu HTML de erro.
  • IntegrityError: checar duplicidade, idempotência e ordem de escrita.

Essa lista vira a primeira versão do seu manual de plantão, mesmo que o plantão seja você de chinelo tentando entender por que o servidor decidiu ter personalidade.

Privacidade: log bom não é log fofoqueiro

Existe um limite importante: log não pode virar vazamento organizado. É tentador gravar payload completo “só para debug”. Aí um dia o arquivo inclui token, e-mail, CPF, cookie, endereço, chave de API ou conteúdo privado. Agora seu sistema de observabilidade virou um cofre sem porta.

Faça uma função de sanitização antes de registrar contexto. Mascare chaves suspeitas, corte textos longos e prefira hashes quando só precisa comparar igualdade. Para payloads grandes, guarde contagem, tipo, tamanho e amostra pequena. Log deve explicar o erro, não preservar a intimidade do universo.

SENSITIVE_KEYS = {"token", "password", "secret", "authorization", "cookie", "api_key"}

def sanitize(value):
    if isinstance(value, dict):
        clean = {}
        for key, item in value.items():
            if key.lower() in SENSITIVE_KEYS:
                clean[key] = "***"
            else:
                clean[key] = sanitize(item)
        return clean
    if isinstance(value, list):
        return [sanitize(item) for item in value[:20]]
    if isinstance(value, str) and len(value) > 300:
        return value[:300] + "...[cortado]"
    return value

Essa camada também reduz ruído. Contexto demais atrapalha quase tanto quanto contexto de menos. O objetivo é diagnóstico, não confissão.

Como saber que a triagem está funcionando

Você sabe que a triagem funciona quando ela muda comportamento. Se ninguém olha, se não reduz tempo de investigação, se não evita repetição, então é só mais um arquivo bonito no repositório. Métricas simples ajudam: tempo médio para identificar causa provável, quantidade de erros recorrentes sem dono, número de falhas críticas detectadas antes de usuário reclamar e volume de alertas ignorados.

Também vale fazer teste de incêndio. Crie falhas controladas: arquivo ausente, API falsa retornando 500, credencial inválida, JSON quebrado. Veja se o relatório aponta o caminho certo. Isso parece exagero até o primeiro domingo em que uma automação importante falha e você descobre que seu “sistema de logs” era basicamente uma caixa preta com luz piscando.

triagem de logs de erro em automações Python com tolerância a falhas
Permitir erro não é aceitar bagunça. É criar caminho para recuperação.

O pacote mínimo que eu colocaria hoje

Se eu tivesse que montar isso em uma tarde, sem teatro corporativo, faria assim: uma função padrão de log JSON Lines, um triador com assinatura de erro, um SQLite para histórico, um relatório Markdown e uma notificação para critical. Depois, só depois, eu pensaria em UI, dashboard, gráfico e outros doces que fazem gerente sorrir.

O pacote mínimo tem esta sequência:

  1. Adicionar log_event e log_exception nas automações mais importantes.
  2. Registrar contexto sanitizado nos pontos de I/O: API, arquivo, banco e fila.
  3. Criar triage_errors.py lendo as últimas 24 horas.
  4. Gerar assinatura estável por erro.
  5. Guardar histórico em SQLite.
  6. Classificar severidade por regras explícitas.
  7. Emitir relatório com próxima ação.
  8. Configurar cron e notificação para casos críticos.

Isso não transforma sua operação em nave espacial. Ainda bem. Nave espacial é cara e explode em documentário. O que isso cria é uma rotina honesta: quando algo quebra, você sabe onde olhar, o que comparar e qual hipótese testar primeiro.

Conclusão: automação madura assume que vai falhar

A diferença entre automação amadora e automação confiável não é ausência de erro. É qualidade da recuperação. Scripts falham. APIs mudam. Discos enchem. Tokens expiram. Arquivos chegam tortos. A pergunta adulta é: quando isso acontecer, você vai receber um mapa ou um enigma?

Triagem de logs de erro em automações Python é uma camada pequena, mas muda o jogo. Ela reduz pânico, encurta investigação, revela recorrência e cria memória operacional. É o tipo de infraestrutura discreta que ninguém nota quando funciona, mas todo mundo sente falta quando não existe.

Minha recomendação é começar hoje por uma automação que já te irritou. Não a mais bonita. A mais problemática. Coloque log estruturado, capture contexto, gere assinatura e faça um relatório simples. Em uma semana, você vai descobrir padrões que estavam escondidos atrás de mensagens genéricas e palavrões criativos.

E agora me diz: qual automação você quer ver destrinchada na próxima? Uma triagem para backups, robôs de planilha, APIs instáveis, filas com retries, monitoramento de servidor ou aquele script caseiro que “funciona faz meses” e por isso mesmo dá medo?

Posts Similares