O módulo logging é uma ferramenta poderosa e padrão do Python para registrar eventos durante a execução de um programa. Diferente do print, que apenas exibe texto no console, o logging permite controlar a severidade das mensagens, direcioná-las para diferentes destinos (arquivo, e-mail, console) e formatá-las com timestamps e informações contextuais. Nesta aula, vamos explorar os conceitos fundamentais do logging, como configurá-lo adequadamente e quando é mais vantajoso usá-lo em vez de print.

O logging é essencial em aplicações reais, porque fornece um histórico detalhado da execução, facilitando a depuração e o monitoramento. Vamos aprender desde a criação de um logger básico até a configuração avançada com múltiplos handlers e níveis personalizados.

Módulo logging

O módulo logging foi introduzido no Python 2.3 e desde então se tornou o padrão de fato para registro de logs. Ele segue o estilo de logging do Java e do C, oferecendo uma API flexível e extensível. O componente central é o Logger, que é responsável por criar mensagens de log. Para obter um logger, usamos a função getLogger(), que retorna uma instância única para cada nome dado.

O logger organiza as mensagens em níveis de severidade, que definem a importância de cada evento. Os níveis padrão são: DEBUG, INFO, WARNING, ERROR e CRITICAL. A hierarquia é: DEBUG (10), INFO (20), WARNING (30), ERROR (40), CRITICAL (50). Quando definimos um nível para o logger, apenas mensagens com nível igual ou superior serão processadas. Isso permite filtrar mensagens facilmente.

Um exemplo básico de uso do módulo:

import logging

# Cria um logger com o nome do módulo atual
logger = logging.getLogger(__name__)

# Emite mensagens de diferentes níveis
logger.debug("Mensagem de debug")
logger.info("Mensagem informativa")
logger.warning("Mensagem de aviso")
logger.error("Mensagem de erro")
logger.critical("Mensagem crítica")

Por padrão, o logger raiz tem nível WARNING e imprime no console. Por isso, apenas as mensagens de warning, error e critical aparecerão. Para ver as mensagens de debug e info, precisamos configurar o nível do logger e do handler.

Níveis e handlers

Os níveis de log são fundamentais para classificar a importância das mensagens. O nível DEBUG é usado para informações detalhadas, geralmente em desenvolvimento. INFO para eventos normais, como início de uma operação. WARNING para situações que não impedem a execução, mas merecem atenção. ERROR para falhas que interrompem uma operação específica. CRITICAL para erros graves que podem causar a interrupção do programa.

Os handlers são responsáveis por enviar as mensagens para destinos específicos. O Python fornece vários tipos: StreamHandler (console), FileHandler (arquivo), RotatingFileHandler (arquivo com rotação), SocketHandler (rede), entre outros. Cada handler pode ter seu próprio nível e formatador.

Um exemplo com múltiplos handlers:

import logging

# Cria um logger
logger = logging.getLogger('meu_app')
logger.setLevel(logging.DEBUG)

# Handler para console
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)

# Handler para arquivo
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.ERROR)

# Formatador
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

# Adiciona handlers ao logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# Exemplos de uso
logger.debug('Debug no console')
logger.info('Info no console')
logger.error('Erro no console e no arquivo')

Neste exemplo, o console recebe todas as mensagens a partir de DEBUG, enquanto o arquivo recebe apenas ERROR e superiores. Isso é útil para manter um log detalhado em desenvolvimento e um log de erros em produção.

Configuração

A configuração do logging pode ser feita de três formas: programaticamente, via arquivo de configuração (INI, JSON ou YAML) e via dicionário. A configuração programática é a mais simples e direta, como mostrado anteriormente. No entanto, para aplicações maiores, é comum usar um arquivo de configuração para separar a lógica de logging do código.

Um exemplo de configuração via dicionário usando dictConfig:

import logging.config

LOGGING_CONFIG = {
    'version': 1,
    'formatters': {
        'default': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'level': 'DEBUG',
            'formatter': 'default',
        },
        'file': {
            'class': 'logging.FileHandler',
            'filename': 'app.log',
            'level': 'ERROR',
            'formatter': 'default',
        },
    },
    'root': {
        'level': 'DEBUG',
        'handlers': ['console', 'file'],
    },
}

logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger(__name__)
logger.debug('Debug')
logger.error('Erro')

Essa abordagem permite centralizar a configuração e alterá-la sem modificar o código. Além disso, o módulo oferece a função basicConfig() para configuração rápida, mas ela não é recomendada para aplicações complexas porque só pode ser chamada uma vez e não oferece tanta flexibilidade.

vs print

Muitos iniciantes usam print para depurar, mas o logging é superior em vários aspectos. Primeiro, o print não tem níveis de severidade, então todas as mensagens são exibidas sempre, o que pode poluir a saída. Com logging, podemos filtrar mensagens por nível, permitindo que apenas informações relevantes apareçam em produção.

Segundo, o print vai apenas para a saída padrão (stdout), enquanto o logging pode ser direcionado para arquivos, soquetes, e-mail, etc., sem alterar o código. Isso é crucial para aplicações que rodam em servidores e precisam de logs persistentes.

Terceiro, o logging inclui informações contextuais como timestamp, nome do módulo e linha do código, que são essenciais para depuração. O print não oferece isso automaticamente.

Exemplo comparativo:

# Usando print
def divide(a, b):
    print(f"Dividindo {a} por {b}")
    if b == 0:
        print("Erro: divisão por zero!")
        return None
    return a / b

# Usando logging
import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

def divide_log(a, b):
    logging.debug(f"Dividindo {a} por {b}")
    if b == 0:
        logging.error("Divisão por zero!")
        return None
    return a / b

Neste exemplo, o logging fornece timestamp e nível, tornando o log muito mais informativo. Além disso, podemos desativar mensagens de debug em produção simplesmente alterando o nível para INFO.

Boas práticas

Ao usar logging, siga estas recomendações: use nomes de logger hierárquicos (por exemplo, meuapp.modulo) para facilitar o controle; nunca use print para logs em código de produção; configure os níveis adequadamente (DEBUG em desenvolvimento, INFO ou WARNING em produção); utilize formatadores consistentes para facilitar a leitura; e evite capturar exceções sem registrá-las com logger.exception().

Referências

Exercícios

  1. Crie um logger que escreva mensagens de nível DEBUG em um arquivo chamado 'debug.log' e mensagens de nível ERROR no console.
  2. ✓ Resposta:
    import logging
    
    logger = logging.getLogger('meu_logger')
    logger.setLevel(logging.DEBUG)
    
    # Handler para arquivo
    file_handler = logging.FileHandler('debug.log')
    file_handler.setLevel(logging.DEBUG)
    
    # Handler para console
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.ERROR)
    
    # Formatador
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)
    
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    
    # Teste
    logger.debug('Mensagem de debug')
    logger.error('Mensagem de erro')
    
  3. Use dictConfig para configurar um logger com um handler para arquivo (nível INFO) e um para console (nível WARNING), com formatadores diferentes.
  4. ✓ Resposta:
    import logging.config
    
    config = {
        'version': 1,
        'formatters': {
            'simples': {'format': '%(levelname)s - %(message)s'},
            'completo': {'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'},
        },
        'handlers': {
            'console': {
                'class': 'logging.StreamHandler',
                'level': 'WARNING',
                'formatter': 'simples',
            },
            'file': {
                'class': 'logging.FileHandler',
                'filename': 'app.log',
                'level': 'INFO',
                'formatter': 'completo',
            },
        },
        'root': {
            'level': 'INFO',
            'handlers': ['console', 'file'],
        },
    }
    
    logging.config.dictConfig(config)
    logger = logging.getLogger(__name__)
    logger.info('Info')
    logger.warning('Warning')
    
  5. Explique a diferença entre print e logging e cite um cenário onde cada um é mais apropriado.
  6. ✓ Resposta:

    print é apropriado para scripts simples e interativos, onde você quer ver a saída imediatamente no console. logging é apropriado para aplicações que precisam de registro persistente, controle de níveis, múltiplos destinos e informações contextuais. Por exemplo, em um servidor web, use logging para registrar erros e acessos; em um script de teste rápido, use print.

  7. Escreva uma função que registre uma exceção usando logger.exception() e explique o que ela faz de diferente.
  8. ✓ Resposta:
    import logging
    
    logging.basicConfig(level=logging.ERROR)
    
    def funcao_que_erro():
        try:
            1 / 0
        except ZeroDivisionError:
            logging.exception("Erro na divisão")
    
    funcao_que_erro()
    

    logger.exception() é chamado dentro de um bloco except e inclui o traceback da exceção na mensagem de log, o que é extremamente útil para depuração.

  9. Configure o logging para usar um RotatingFileHandler que mantém 3 arquivos de backup de 1 KB cada.
  10. ✓ Resposta:
    import logging
    from logging.handlers import RotatingFileHandler
    
    logger = logging.getLogger('rotativo')
    logger.setLevel(logging.DEBUG)
    
    handler = RotatingFileHandler('app.log', maxBytes=1024, backupCount=3)
    handler.setLevel(logging.DEBUG)
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    
    # Teste: escreva muitas mensagens para ver a rotação
    for i in range(100):
        logger.debug(f"Mensagem {i}")