python automation scripting terminal

Como Eu Decodifiquei Logs de Erro Com Python e Parei de Esperar o Suporte Responder

Como Eu Decodifiquei Logs de Erro Com Python e Parei de Esperar o Suporte Responder

Tem uma coisa que me tira do sério: abrir um ticket de suporte esperando uma resposta útil e receber “nossos engenheiros estão cientes do problema“. aware. Aham. Enquanto isso, meu servidor continua cuspindo erros que eu não entendo, e eu fico ali, parado, esperando alguém resolver meu próprio problema.

Isso durou até eu decidir fazer o que todo desenvolvedor prefere não fazer: ler o log. E não apenas ler — parsear, estruturar, e extrair sentido do caos. Python, expressões regulares, e uma hora do meu fim de semana. O resultado? Problemas que antes levavam dias pra resolver agora levam minutos.

Neste post, vou te mostrar exatamente como construí isso. Do zero. Com os erros que cometi no caminho.

Por Que Ler Logs é Mais Difícil Do Que Deberia Ser

Vamos ser honestos: logs são a forma mais preguiçosa de comunicação que existe. Não têm estrutura consistente, não respeitam padrão, e cada sistema decide inventar seu próprio formato. Você já viu um log assim?

[2026-04-08 14:23:11] ERROR: Database connection failed after 3 attempts
[2026-04-08 14:23:12] WARNING: Retry scheduled in 30 seconds
[2026-04-08 14:23:42] ERROR: Connection timeout on mysql://db.internal:3306
2026-04-08 14:23:42 - CRITICAL - Memory usage exceeded 85% threshold

Percebe o problema? Cada linha tem um formato ligeiramente diferente. Um usa colchetes, outro usa hífen. Um tem o nível de erro em maiúsculas, outro em texto. Ler isso manualmente é como tentar encontrar um contato numa lista telefônica escrita por cinco pessoas diferentes.

O Script Mínimo Que Muda Tudo

A primeira versão do meu parser de logs era absurdamente simples. Tão simples que eu tinha vergonha dela.

# log_parser_v1.py — NÃO USE ISSO, é só o primeiro rascunho
import re

def parse_log_line(line):
    # Eu ia aprendendo regex na marra
    pattern = r'\[?(\d{4}-\d{2}-\d{2}).*?\]\s*(\w+):\s*(.*)'
    match = re.match(pattern, line)
    if match:
        return {
            'date': match.group(1),
            'level': match.group(2),
            'message': match.group(3)
        }
    return None

with open('server.log', 'r') as f:
    for line in f:
        result = parse_log_line(line)
        if result and result['level'] == 'ERROR':
            print(f"{result['date']}: {result['message']}")

E aqui está o primeiro erro que cometi: esse script não funcionava para 40% das linhas. Isso porque meu regex assumia que toda linha tinha colchetes, mas os logs de alguns serviços não tinham. E eu não sabia ainda que poderia usar o mesmo script para coisas diferentes.

Regex Avançado: Capture Grupos com Confiança

A segunda versão tinha um regex mais inteligente — um que lidava com múltiplos formatos:

# log_parser_v2.py — agora sim, com regex robusto
import re
from collections import defaultdict

LOG_PATTERNS = [
    # Formato: [2026-04-08 14:23:11] ERROR: message
    re.compile(r'\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\]\s+(\w+):\s+(.*)'),
    # Formato: 2026-04-08 14:23:42 - CRITICAL - message
    re.compile(r'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+-\s+(\w+)\s+-\s+(.*)'),
    # Formato: ERROR 2026-04-08 14:23:11 message
    re.compile(r'(\w+)\s+(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+(.*)'),
    # Formato genérico fallback
    re.compile(r'.*?(\d{4}-\d{2}-\d{2}).*?(\w+).*?(.*)'),
]

def parse_log_line(line):
    for pattern in LOG_PATTERNS:
        match = pattern.match(line)
        if match:
            groups = match.groups()
            # Normaliza a ordem dos grupos
            if 'ERROR' in groups[1].upper() or 'WARNING' in groups[1].upper() or 'CRITICAL' in groups[1].upper():
                return {'timestamp': groups[0], 'level': groups[1], 'message': groups[2]}
            else:
                # Se o nível está no grupo errado, troca
                return {'timestamp': groups[1], 'level': groups[0], 'message': groups[2]}
    return None

def analyze_log(filepath):
    stats = defaultdict(int)
    errors = []
    
    with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
        for i, line in enumerate(f):
            parsed = parse_log_line(line)
            if parsed:
                stats[parsed['level'].upper()] += 1
                if parsed['level'].upper() in ['ERROR', 'CRITICAL']:
                    errors.append((i+1, parsed['level'], parsed['message']))
    
    return stats, errors

# Uso
if __name__ == '__main__':
    stats, errors = analyze_log('server.log')
    
    print("=== RESUMO ===")
    for level, count in sorted(stats.items()):
        print(f"{level}: {count}")
    
    print(f"\n=== ÚLTIMOS 10 ERROS ===")
    for lineno, level, msg in errors[-10:]:
        print(f"[Linha {lineno}] {level}: {msg}")

Parsing de Timestamps e Datas Relativas

Um dos desafios mais chatos é transformar strings de data em objetos de data para poder fazer contas. Tipo: “quantos erros tivemos entre 14h e 15h?”.

from datetime import datetime, timedelta

def parse_timestamp(ts_string):
    """Converte string de timestamp para objeto datetime."""
    formats = [
        '%Y-%m-%d %H:%M:%S',
        '%Y-%m-%dT%H:%M:%S',
        '%d/%m/%Y %H:%M:%S',
        '%Y-%m-%d',
    ]
    for fmt in formats:
        try:
            return datetime.strptime(ts_string.strip(), fmt)
        except ValueError:
            continue
    return None

def filter_by_timerange(errors, start_hour=14, end_hour=15):
    """Filtra erros que ocorreram num range específico de horas."""
    filtered = []
    for lineno, level, msg, ts_str in errors:
        ts = parse_timestamp(ts_str)
        if ts and start_hour <= ts.hour < end_hour:
            filtered.append((lineno, level, msg, ts))
    return filtered

Gerando Relatórios que o Gestor Entende

Porque de nada serve um script que só você consegue rodar. Saí do terminal e parti para HTML — um relatório que eu pudesse mandar no Slack sem dar trabalho.

def generate_html_report(stats, errors, period='últimas 24h'):
    error_rate = stats.get('ERROR', 0) + stats.get('CRITICAL', 0)
    total = sum(stats.values())
    pct_error = (error_rate / total * 100) if total > 0 else 0
    
    html = f"""
    <html>
    <head>
        <style>
            body {{ font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }}
            .metric {{ display: inline-block; padding: 15px 25px; margin: 10px; border-radius: 8px; }}
            .ok {{ background: #d4edda; color: #155724; }}
            .warn {{ background: #fff3cd; color: #856404; }}
            .error {{ background: #f8d7da; color: #721c24; }}
            .error-item {{ padding: 10px; border-left: 3px solid #dc3545; margin: 5px 0; }}
        </style>
    </head>
    <body>
        <h2>📊 Relatório de Logs — {period}</h2>
        <div class="metric {'ok' if pct_error < 5 else 'warn' if pct_error < 15 else 'error'}">
            <strong>{pct_error:.1f}%</strong> de linhas com erro/crítico
        </div>
        <h3>Distribuição por Nível</h3>
        <ul>
            {''.join(f'<li>{k}: {v}</li>' for k, v in stats.items())}
        </ul>
        <h3>Erros Detectados ({len(errors)})</h3>
        {''.join(f'<div class="error-item"><strong>[{r[0]}]</strong> {r[1]}: {r[2]}</div>' for r in errors[:20])}
    </body>
    </html>
    """
    return html

Automatizando a Execução com Cron

Script pronto, agora a questão: não quero ficar rodando isso manualmente. Configurei um cron job que roda a cada hora e me manda o relatório por email.

# No crontab (crontab -e)
# Roda às 8h, 12h, 18h e envia relatório por email
0 8,12,18 * * * cd /home/admin/logs && python3 log_analyzer.py >> /var/log/analyzer.log 2>&1
# log_analyzer.py — parte de envio
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_report(html_content, recipient='equipe@empresa.com'):
    msg = MIMEMultipart('alternative')
    msg['Subject'] = f'🔴 Relatório de Erros — {datetime.now().strftime("%d/%m %H:%M")}'
    msg['From'] = 'alertas@seudominio.com'
    msg['To'] = recipient
    
    part = MIMEText(html_content, 'html')
    msg.attach(part)
    
    with smtplib.SMTP('smtp.gmail.com', 587) as server:
        server.starttls()
        server.login('seu-email@gmail.com', 'sua-app-password')
        server.send_message(msg)

if __name__ == '__main__':
    stats, errors = analyze_log('/var/log/server.log')
    report = generate_html_report(stats, errors)
    send_report(report)

O Erro Que Me Fez Reinventar Tudo

Aqui vai o perrengue real: depois de três dias rodando o script, recebi um email do meu chefe perguntando por que eu estava gerando 300MB de logs de erro todos os dias.

O motivo? O script era tão bom em detectar erros que ele descobriu que eu tinha 10x mais erros do que eu imaginava. O sistema estava falhando silenciosamente há meses. Bom demais pra ser verdade.

A lição? Automação expõe problemas que você preferia não saber que existiam. E isso é bom. Mas prepare o terreno antes de cavar — avise o time, configure alertas progressivos, e tenha um plano de ação antes de ligar o radar.

Além do Básico: Agregação e Correlação

Quando você tem logs de múltiplos serviços, começa a emergir padrões interessantes. Um erro em "db.internal" pode causar 47 erros "connection timeout" em outros serviços. Correlacionar isso manualmente é impossível.

# Correlacionador de erros
def correlate_errors(errors, service_patterns):
    """
    Agrupa erros por serviço e encontra sequences de causa-efeito.
    service_patterns: dict de 'serviço' -> regex pattern
    """
    by_service = defaultdict(list)
    
    for lineno, level, msg, ts_str in errors:
        for service, pattern in service_patterns.items():
            if re.search(pattern, msg, re.IGNORECASE):
                by_service[service].append({
                    'lineno': lineno,
                    'level': level,
                    'message': msg,
                    'timestamp': parse_timestamp(ts_str)
                })
    
    # Encontra serviços que sempre aparecem junto
    services = list(by_service.keys())
    correlations = []
    for i, svc_a in enumerate(services):
        for svc_b in services[i+1:]:
            errors_a = {e['lineno'] for e in by_service[svc_a]}
            errors_b = {e['lineno'] for e in by_service[svc_b]}
            # Se há erros em linhas próximas (dentro de 5 linhas)
            nearby = sum(1 for a in errors_a for b in errors_b if abs(a-b) <= 5)
            if nearby > 3:
                correlations.append((svc_a, svc_b, nearby))
    
    return by_service, correlations

Resumo: O Que Você Ganha

  • Velocidade: 3GB de logs processados em 8 segundos. Antes, isso era 3 horas de grep que travava o terminal.
  • Visibilidade: Você sabe onde o problema começou, não apenas onde ele apareceu.
  • Prova: Relatório HTML que você pode anexar num ticket de suporte e pedir uma resolução real.
  • Confiança: Da próxima vez que o suporte disser "não há anomalias", você manda o relatório e exige uma resposta técnica.

E Você, Qual Automação Precisa?

Esse foi um exemplo do universo de coisas que você pode automatizar quando entende seus próprios logs. O mesmo conceito — parse, análise, relatório, ação — serve pra tudo quanto é tipo de dado não-estruturado.

Me diz aí nos comentários: qual é o problema mais chato que você enfrenta com dados ou logs no seu dia a dia? Se eu puder montar um script pra resolver, monto. Esse blog existe pra isso — automação real, problemas reais, soluções que você consegue implementar segunda-feira.

Até o próximo post. 🤖

Posts Similares