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:
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)
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:
logger = logging.getLogger(__name__)
logger.info("módulo %s", __name__)
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:
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:
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")
{"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:
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"])
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:
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:
try:
criar_parser().parse_args(["Ana", "--vezes", "muitas"])
except SystemExit as codigo:
print("saiu com código", codigo.code)
saiu com código 2
Para programas com vários comandos, como git add e git commit, o argparse oferece subcomandos:
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"]))
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.