Pular para o conteúdo

    Capítulo 41, Intermediário

    Logging e linha de comando com argparse

    `print` serve para depurar na sua máquina. Em produção você precisa de níveis, destinos e formato, e é para isso que existe o `logging`. E um programa útil quase sempre recebe argumentos pelo terminal.

    Código deste capítulo: intermediario/cap41_logging_cli.py

    Níveis e formato

    O logging tem cinco níveis: DEBUG, INFO, WARNING, ERROR e CRITICAL. O basicConfig define o nível mínimo e o formato. Mensagens abaixo do nível são descartadas. E eu passo os valores como argumentos (%d), e não como f-string, para que a mensagem só seja montada se for realmente emitida:

    intermediario/cap41_logging_cli.pylinhas 10 a 21
    import logging
    import sys
    
    logging.basicConfig(
        stream=sys.stdout,
        level=logging.INFO,
        format="%(levelname)s %(name)s: %(message)s",
    )
    log = logging.getLogger("pedidos")
    log.debug("não aparece, nível abaixo de INFO")
    log.info("pedido recebido")
    log.warning("estoque baixo: %d unidades", 3)
    
    Saída
    INFO pedidos: pedido recebido
    WARNING pedidos: estoque baixo: 3 unidades
    

    Um logger por módulo

    A convenção é logger = logging.getLogger(__name__) no topo de cada módulo. Assim o nome do logger mostra de onde veio a mensagem, e quem usa a sua biblioteca decide o nível e o destino sem editar o seu código. Bibliotecas nunca devem chamar basicConfig, que é uma decisão da aplicação:

    intermediario/cap41_logging_cli.pylinhas 26 a 27
    logger = logging.getLogger(__name__)
    logger.info("módulo %s", __name__)
    
    Saída
    INFO __main__: módulo __main__
    

    Para registrar uma exceção com o traceback completo, use logger.exception(...) dentro do except. Ele grava no nível ERROR e inclui a pilha de chamadas:

    intermediario/cap41_logging_cli.pylinhas 29 a 32
    try:
        1 / 0
    except ZeroDivisionError:
        log.exception("falha ao calcular")
    

    Saída estruturada

    Em sistemas com agregação de logs, texto livre é difícil de pesquisar. Um formatador que emite uma linha de JSON por mensagem resolve. Cada logger pode ter o seu próprio destino:

    intermediario/cap41_logging_cli.pylinhas 37 a 58
    import json
    
    
    class FormatoJson(logging.Formatter):
        def format(self, record):
            return json.dumps(
                {
                    "nivel": record.levelname,
                    "logger": record.name,
                    "mensagem": record.getMessage(),
                },
                ensure_ascii=False,
            )
    
    
    manipulador = logging.StreamHandler(sys.stdout)
    manipulador.setFormatter(FormatoJson())
    auditoria = logging.getLogger("auditoria")
    auditoria.addHandler(manipulador)
    auditoria.propagate = False
    auditoria.setLevel(logging.INFO)
    auditoria.info("login realizado")
    
    Saída
    {"nivel": "INFO", "logger": "auditoria", "mensagem": "login realizado"}
    

    Argumentos de linha de comando

    O argparse converte os argumentos do terminal, valida, gera a ajuda (--help) e as mensagens de erro. Eu sempre escrevo a função main recebendo a lista de argumentos como parâmetro, o que a torna testável sem abrir um terminal:

    intermediario/cap41_logging_cli.pylinhas 63 a 84
    import argparse
    
    
    def criar_parser():
        parser = argparse.ArgumentParser(description="Saudação em linha de comando")
        parser.add_argument("nome", help="quem cumprimentar")
        parser.add_argument("--vezes", type=int, default=1, help="quantas vezes repetir")
        parser.add_argument("--gritar", action="store_true", help="usar maiúsculas")
        return parser
    
    
    def main(argv=None):
        args = criar_parser().parse_args(argv)
        texto = f"Olá, {args.nome}!"
        if args.gritar:
            texto = texto.upper()
        for _ in range(args.vezes):
            print(texto)
        return 0
    
    
    main(["Ana", "--vezes", "2", "--gritar"])
    
    Saída
    OLÁ, ANA!
    OLÁ, ANA!
    

    Quando argv é None, o argparse lê sys.argv sozinho. Para executar o programa de verdade, o final do arquivo fica assim, e o código de saída do main vira o código de saída do processo:

    Final do arquivo de um programa
    if __name__ == "__main__":
        raise SystemExit(main())
    

    Argumentos inválidos fazem o argparse imprimir o erro e terminar com o código 2, levantando SystemExit. Dá para capturar em teste:

    intermediario/cap41_logging_cli.pylinhas 86 a 89
    try:
        criar_parser().parse_args(["Ana", "--vezes", "muitas"])
    except SystemExit as codigo:
        print("saiu com código", codigo.code)
    
    Saída
    saiu com código 2
    

    Para programas com vários comandos, como git add e git commit, o argparse oferece subcomandos:

    intermediario/cap41_logging_cli.pylinhas 91 a 100
    def parser_com_subcomandos():
        parser = argparse.ArgumentParser(prog="tarefas")
        sub = parser.add_subparsers(dest="comando", required=True)
        adicionar = sub.add_parser("adicionar")
        adicionar.add_argument("titulo")
        sub.add_parser("listar")
        return parser
    
    
    print(parser_com_subcomandos().parse_args(["adicionar", "estudar"]))
    
    Saída
    Namespace(comando='adicionar', titulo='estudar')
    

    Exercício 1

    Somar números pela linha de comando

    Escreva main_soma(argv) que receba um ou mais números como argumentos e devolva a soma.