Logging no Python: o guia definitivo que ninguém te contou

Published on: 2025-10-06
Post image
pt logging python eventos logger level handler

Logging no Python é o processo de registrar eventos importantes da execução de um programa de forma consistente e pesquisável. Esse registro cria um histórico confiável, com hora, origem e nível de severidade, permitindo entender o comportamento do sistema sem interromper o fluxo com inspeções improvisadas. Diferentemente de saídas soltas no console, o logging tem estrutura, filtro e destinos configuráveis. Com uma base correta, o registro atende desde depuração simples até observabilidade em produção.

Este conteúdo apresenta os conceitos essenciais, a configuração mínima e recursos avançados do módulo logging da biblioteca padrão. As seções cobrem níveis, componentes, formatação, múltiplos destinos, rotação de arquivos, contexto adicional, logs estruturados e desempenho. Exemplos comentados mostram práticas seguras e reutilizáveis. O objetivo é construir um entendimento sólido e pragmático, do básico ao robusto.

O que é logging e por que importa

Logging registra informações durante a execução que ajudam a explicar o que ocorreu, quando ocorreu e onde ocorreu. Cada mensagem pode ter um nível de severidade como DEBUG ou ERROR, o que permite filtrar o volume de dados conforme a necessidade. Diferentemente de print, o logging envia saídas para vários destinos, com formato padronizado e controle central. Em produção, logs consistentes reduzem o tempo de diagnóstico e aumentam a confiabilidade do sistema.

O uso de logging também cria disciplina de comunicação entre código e operação. Mensagens bem escritas se tornam sinais úteis para monitoramento, auditoria e suporte. A padronização evita ruído e garante que cada evento tenha contexto suficiente para análise posterior. Esse cuidado transforma logs em uma fonte de verdade sobre o comportamento do software.

Componentes fundamentais do sistema de logging

O sistema se organiza em alguns blocos principais que trabalham em conjunto. O registrador (logger) recebe a chamada do código e decide se a mensagem passa ou não pelo nível configurado. O manipulador (handler) envia a mensagem para um destino, como console, arquivo ou fila. O formatador (formatter) define a aparência do texto final, incluindo hora, nível e mensagem. Filtros opcionais aplicam regras adicionais, e o registro (LogRecord) carrega todos os campos da mensagem.

A lista a seguir resume o papel de cada componente dentro do fluxo de geração de logs. Isso ajuda a visualizar onde cada decisão é tomada e onde configurar cada parte de forma apropriada.

  • Registrador (logger): ponto de entrada; tem nome hierárquico e nível.
  • Manipulador (handler): destino da saída, como StreamHandler ou FileHandler.
  • Formatador (formatter): template de texto, como “%(asctime)s - %(levelname)s - %(message)s”.
  • Nível (level): filtro por severidade; define o mínimo que passa.
  • Filtro (filter): lógica adicional, por exemplo, incluir ou excluir mensagens por contexto.
  • LogRecord: objeto com campos ricos (tempo, módulo, linha, exceção e outros).

Níveis de severidade: quando usar cada um

Os níveis organizam a importância das mensagens e evitam excesso de informação. DEBUG serve para diagnóstico detalhado durante desenvolvimento. INFO sinaliza eventos esperados, como início e fim de tarefas. WARNING aponta algo inesperado, mas que não interrompe a execução. ERROR indica falhas que afetaram uma operação, e CRITICAL representa situações graves que exigem atenção imediata.

A lista a seguir descreve usos típicos de cada nível, facilitando escolhas consistentes ao escrever mensagens. Essa padronização evita discussões recorrentes e melhora a leitura dos registros no longo prazo.

  • DEBUG: variáveis internas, consultas geradas, caminhos de decisão.
  • INFO: eventos de negócio esperados, checkpoints e resumos.
  • WARNING: deprecações, quedas controladas, tentativas repetidas.
  • ERROR: exceções tratadas que impedem a tarefa atual.
  • CRITICAL: indisponibilidade, corrupção de dados, falhas globais.

Configuração mínima com basicConfig e registradores nomeados

Uma configuração mínima usa basicConfig e um registrador nomeado para dar origem e formato às mensagens. O trecho a seguir mostra como inicializar o sistema, definir nível e emitir uma primeira mensagem informativa. Esse ponto de partida já cria consistência para depuração e análise básica durante o desenvolvimento. A partir daí, outras opções podem ser acrescentadas sem quebrar a estrutura inicial.

import logging

# Configuração mínima: nível, formato e data
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - [%(levelname)s] - %(name)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)

# Registrador nomeado para identificar a origem
registrador = logging.getLogger("aplicacao")

registrador.debug("Mensagem de depuração.")          # não aparece com nível INFO
registrador.info("Sistema iniciado com sucesso.")     # aparece

Essa configuração usa o registrador raiz implicitamente e adiciona um registrador nomeado para a aplicação. O nível controla o filtro mínimo, e o formato define campos úteis como tempo, nível e nome. Chamar basicConfig mais de uma vez não tem efeito se o sistema já foi configurado, o que evita duplicação acidental. Para personalizações maiores, manipuladores e formatadores podem ser criados manualmente.

Um utilitário reutilizável de logging

Centralizar a inicialização em uma função reduz repetição e elimina divergências entre módulos. O código a seguir implementa um “wrapper” simples que garante configuração única e retorna registradores por nome. Esse padrão torna o logging previsível em toda a base de código, com um ponto único para ajustes. Alterações no nível, no formato ou na data são feitas em um só lugar.

import logging

def obter_registrador(nome: str = __name__):
    """Retorna um registrador configurado, sem duplicar configuração."""
    # Garante que a configuração global exista apenas uma vez
    if not logging.getLogger().hasHandlers():
        logging.basicConfig(
            level=logging.INFO,
            format="%(asctime)s - [%(levelname)s] - %(name)s - %(message)s",
            datefmt="%Y-%m-%d %H:%M:%S"
        )
    return logging.getLogger(nome)

registrador = obter_registrador("aplicacao")
registrador.info("Logging inicializado de forma simples e reutilizável.")

Esse utilitário consulta o registrador raiz para checar a existência de manipuladores antes de configurar. Com isso, evita-se o efeito comum de mensagens duplicadas quando basicConfig é chamado em vários pontos. O nome passado ao registrador ajuda a rastrear a origem das mensagens sem esforço adicional. A mesma função pode ser usada por bibliotecas internas para manter uniformidade.

Console, arquivo e rotação de logs

Enviar logs para mais de um destino é útil para depuração interativa e auditoria persistente. O exemplo a seguir cria manipuladores para console e arquivo, aplica um formatador comum e ajusta o nível no registrador raiz. A limpeza dos manipuladores existentes evita duplicações quando o código roda mais de uma vez no mesmo processo. Esse arranjo cobre os casos diários de desenvolvimento e execução simples em servidores.

import logging

# Manipuladores separados para console e arquivo
manipulador_console = logging.StreamHandler()
manipulador_arquivo = logging.FileHandler("aplicacao.log", encoding="utf-8")

# Formatação padronizada
formatador = logging.Formatter(
    fmt="%(asctime)s - [%(levelname)s] - %(name)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)
manipulador_console.setFormatter(formatador)
manipulador_arquivo.setFormatter(formatador)

# Registrador raiz com nível global
registrador_raiz = logging.getLogger()
registrador_raiz.setLevel(logging.DEBUG)
registrador_raiz.handlers.clear()  # evita duplicações
registrador_raiz.addHandler(manipulador_console)
registrador_raiz.addHandler(manipulador_arquivo)

logging.getLogger("aplicacao").info("Mensagens vão para console e arquivo.")

Para limitar tamanho de arquivo, a rotação automática cria novos arquivos quando um limite é atingido. O trecho a seguir demonstra a troca do manipulador de arquivo por um rotativo baseado em tamanho, com cópias de segurança. A rotação por tempo também é possível, usando intervalos como minuto, hora ou dia. Essas estratégias mantêm o histórico sob controle sem consumo ilimitado de disco.

import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler

# Rotação por tamanho
manipulador_rotativo = RotatingFileHandler(
    "aplicacao.log",
    maxBytes=5_000_000,   # 5 MB
    backupCount=5,
    encoding="utf-8"
)

# Rotação por tempo (ex.: diário)
manipulador_diario = TimedRotatingFileHandler(
    "aplicacao_diario.log",
    when="D",             # S, M, H, D, W0-W6, midnight
    interval=1,
    backupCount=7,
    encoding="utf-8"
)

Formatação, campos de contexto e logs estruturados

O formatador pode incluir campos padrão do LogRecord, como asctime, name, levelname, module, lineno e funcName. O trecho a seguir mostra um formato detalhado que facilita rastrear a origem exata de cada evento. Esse estilo é valioso durante a investigação de problemas em código complexo. A escolha dos campos deve equilibrar clareza e concisão.

import logging

formatador = logging.Formatter(
    fmt="%(asctime)s %(levelname)s %(name)s %(module)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)

manipulador = logging.StreamHandler()
manipulador.setFormatter(formatador)

registrador = logging.getLogger("pedido.servico")
registrador.setLevel(logging.INFO)
registrador.handlers.clear()
registrador.addHandler(manipulador)

registrador.info("Processo iniciado.")

Campos adicionais podem ser anexados via extra ou por um LoggerAdapter, preservando o formato escolhido. Em cenários que exigem integração com ferramentas, um formatador personalizado produz JSON estruturado. Os dois exemplos a seguir mostram inclusão de contexto e um formatador JSON simples, facilitando análise automatizada. A estruturação organizada melhora buscas, agregações e correlação entre serviços.

import logging

# Adicionando contexto via 'extra'
registrador = logging.getLogger("pagamento")
manipulador = logging.StreamHandler()
manipulador.setFormatter(logging.Formatter("%(asctime)s %(levelname)s - %(message)s [pedido=%(id_pedido)s]"))

registrador.handlers.clear()
registrador.addHandler(manimulador := manipulador)
registrador.setLevel(logging.INFO)

registrador = logging.LoggerAdapter(registrador, {"id_pedido": "A123"})
registrador.info("Autorização iniciada.")
import logging
import json

# Formatador JSON básico
class FormatadorJSON(logging.Formatter):
    def format(self, registro: logging.LogRecord) -> str:
        dados = {
            "tempo": self.formatTime(registro, self.datefmt),
            "nivel": registro.levelname,
            "nome": registro.name,
            "mensagem": registro.getMessage(),
        }
        # Inclui campos extras se existirem
        if hasattr(registro, "id_pedido"):
            dados["id_pedido"] = registro.id_pedido
        return json.dumps(dados, ensure_ascii=False)

manipulador = logging.StreamHandler()
manipulador.setFormatter(FormatadorJSON(datefmt="%Y-%m-%d %H:%M:%S"))

registrador = logging.getLogger("pagamento")
registrador.handlers.clear()
registrador.addHandler(manipulador)
registrador.setLevel(logging.INFO)

registrador = logging.LoggerAdapter(registrador, {"id_pedido": "A123"})
registrador.info("Autorização concluída.")

Exceções, rastros de pilha e integração com avisos

Durante falhas, registrar a pilha de chamadas acelera a investigação. O exemplo a seguir usa logging.exception para incluir automaticamente o traceback ao lidar com uma exceção. Alternativamente, o parâmetro exc_info=True em qualquer chamada de log adiciona o rastro quando útil. Esses recursos tornam o registro de falhas completo e confiável.

import logging

registrador = logging.getLogger("servico")
registrador.setLevel(logging.ERROR)
registrador.addHandler(logging.StreamHandler())

try:
    # Código que pode falhar
    1 / 0
except Exception:
    registrador.exception("Erro ao processar solicitação.")  # inclui traceback

# Também é possível:
try:
    int("abc")
except ValueError as erro:
    registrador.error("Conversão inválida.", exc_info=erro)

Muitos projetos também emitem avisos via módulo warnings. A captura desses avisos pelo logging unifica a visualização e o armazenamento. O trecho a seguir ativa essa integração, garantindo que avisos se tornem entradas de log padronizadas. Essa abordagem evita canais paralelos de diagnóstico.

import logging
import warnings

logging.basicConfig(level=logging.WARNING)
logging.captureWarnings(True)

warnings.warn("Uso de parâmetro obsoleto.", category=DeprecationWarning)
logging.getLogger("aplicacao").warning("Aviso convertido em log.")

Configuração por ambiente e com dictConfig

Ambientes diferentes exigem níveis distintos para equilibrar ruído e utilidade. O exemplo a seguir escolhe DEBUG em desenvolvimento e INFO em produção com base em variável de ambiente. Essa seleção pode ser aplicada antes de qualquer emissão de log para garantir consistência. A lógica simples já oferece ganho imediato de controle.

import logging
import os

ambiente = os.getenv("AMBIENTE", "PRODUCAO").upper()
nivel = logging.DEBUG if ambiente == "DESENVOLVIMENTO" else logging.INFO

logging.basicConfig(
    level=nivel,
    format="%(asctime)s - [%(levelname)s] - %(name)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)

logging.getLogger("app").info(f"Ambiente: {ambiente}")

Para configurações mais completas, dictConfig descreve formatadores, manipuladores e registradores em um único dicionário. O trecho a seguir monta uma configuração típica com console e arquivo, pronta para evoluir. Essa estrutura é fácil de serializar para JSON ou YAML quando necessário. A centralização facilita manutenção em sistemas maiores.

import logging
import logging.config

configuracao = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "padrao": {
            "format": "%(asctime)s - [%(levelname)s] - %(name)s - %(message)s",
            "datefmt": "%Y-%m-%d %H:%M:%S"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "padrao",
            "level": "DEBUG"
        },
        "arquivo": {
            "class": "logging.FileHandler",
            "formatter": "padrao",
            "level": "INFO",
            "filename": "aplicacao.log",
            "encoding": "utf-8"
        }
    },
    "loggers": {
        "": {  # registrador raiz
            "level": "INFO",
            "handlers": ["console", "arquivo"]
        },
        "negocio": {  # registrador específico
            "level": "DEBUG",
            "handlers": ["console"],
            "propagate": False
        }
    }
}

logging.config.dictConfig(configuracao)
logging.getLogger("negocio").debug("Configuração aplicada com dictConfig.")

Evitando armadilhas comuns

Alguns problemas aparecem com frequência e têm soluções diretas. Mensagens duplicadas geralmente indicam múltiplos manipuladores anexados ao mesmo registrador. Ausência de saída costuma apontar para nível alto demais ou handlers ausentes. Propagação não intencional faz mensagens aparecerem duas vezes se o registrador filho e o raiz processarem o mesmo evento. Atenção a esses pontos evita confusão durante a integração.

A lista a seguir reúne armadilhas típicas e ações curtas para corrigi-las. Essas medidas simples estabilizam o comportamento dos logs em ambientes variados e durante testes.

  • Duplicação: limpar handlers antes de adicionar novos (handlers.clear()).
  • Sem saída: conferir nível do registrador e do manipulador; garantir que exista pelo menos um handler.
  • Propagação indesejada: definir propagate=False no registrador específico.
  • basicConfig ineficaz: lembrar que só funciona se nenhum handler existir ainda.
  • Biblioteca sem handler: anexar NullHandler em bibliotecas para evitar aviso no consumidor.

Desempenho, concorrência e filas de log

Construções de mensagem custosas devem ser evitadas quando o nível não permite a emissão. O padrão de placeholders do logging faz interpolação apenas se a mensagem for emitida, reduzindo trabalho desnecessário. Em cálculos pesados, a verificação com isEnabledFor é adequada antes de preparar dados caros. Essas práticas mantêm o impacto de logging baixo em caminhos críticos de execução.

import logging

registrador = logging.getLogger("desempenho")
registrador.setLevel(logging.DEBUG)
registrador.addHandler(logging.StreamHandler())

valor = 42
total = 100

# Interpolação preguiçosa: só formata se o nível permitir
registrador.debug("Valor processado: %s de %s", valor, total)

# Para operações caras, checar o nível antes
if registrador.isEnabledFor(logging.DEBUG):
    resumo = ", ".join(str(n) for n in range(1000))  # operação cara
    registrador.debug("Resumo detalhado: %s", resumo)

Em aplicações com threads ou processos, filas dedicadas isolam a gravação e evitam contenção. O exemplo a seguir usa QueueHandler e QueueListener para enviar mensagens a um consumidor único. Esse padrão melhora latência e previsibilidade em cenários de alto volume. A configuração é feita uma vez e beneficia todo o aplicativo.

import logging
from logging.handlers import QueueHandler, QueueListener
from queue import Queue

fila = Queue()

# Consumidor: aplica formatador e escreve no destino
manipulador_console = logging.StreamHandler()
manipulador_console.setFormatter(logging.Formatter("%(asctime)s %(levelname)s - %(message)s"))

ouvinte = QueueListener(fila, manipulador_console)
ouvinte.start()

# Produtor: envia mensagens para a fila
registrador = logging.getLogger("concorrencia")
registrador.setLevel(logging.DEBUG)
registrador.handlers.clear()
registrador.addHandler(QueueHandler(fila))

registrador.info("Mensagem via fila.")
ouvinte.stop()