Registo Excepcional de Excepções em Python

Aaron Maxwell é autor de Powerful Python.

Excepções acontecem. E como desenvolvedores, nós simplesmente temos que lidar com elas. Mesmo quando escrevendo software para nos ajudar a encontrar burritos.

Espere, estou me adiantando… vamos voltar a isso. Como eu estava a dizer: A forma como lidamos com as excepções depende da linguagem. E para software operando em escala, o registro é uma das ferramentas mais poderosas e valiosas que temos para lidar com condições de erro. Vamos ver algumas formas de trabalharmos juntos.

O Padrão “Big Tarp”

Vamos começar com um extremo:

try: main_loop()except Exception: logger.exception("Fatal error in main loop")

Este é um amplo “catch-all”. É adequado para algum caminho de código onde você sabe que o bloco de código (ou seja, main_loop()) pode levantar uma série de exceções que você pode não prever. E ao invés de permitir que o programa termine, você decide que é preferível registrar as informações de erro, e continuar a partir daí.

A mágica aqui é com o método exception. (logger é o objeto logger da sua aplicação – algo que foi retornado de logging.getLogger(), por exemplo). Este maravilhoso método captura o traço completo da pilha no contexto do bloco except, e escreve-o por completo.

Nota que você não tem que passar o objeto exception aqui. Você passa uma string de mensagem. Isto irá registar o stack trace completo, mas prepende uma linha com a sua mensagem. Então a mensagem multilinha que aparece no seu log pode se parecer com isto:

Fatal error in main loopTraceback (most recent call last): File "bigtarp.py", line 14, in main_loop() File "bigtarp.py", line 9, in main_loop print(foo(x)) File "bigtarp.py", line 4, in foo return 10 // n ZeroDivisionError: integer division or modulo by zero

Os detalhes do stack trace não importam – este é um exemplo de brinquedo que ilustra uma solução adulta para um problema do mundo real. Repare que a primeira linha é a mensagem que passou para logger.exception(), e as linhas subsequentes são o stack trace completo, incluindo o tipo de excepção (ZeroDivisionError, neste caso). Ele irá capturar e registrar qualquer tipo de erro desta forma.

Por padrão, logger.exception usa o nível de log de ERROR. Alternativamente, você pode usar os métodos regulares de registro – logger.debug(), logger.info(), logger.warn(), etc.- e passar o parâmetro exc_info, definindo-o como True:

while True: try: main_loop() except Exception: logger.error("Fatal error in main loop", exc_info=True)

Configurando exc_info para True fará com que o registro inclua o stack trace…. completo exatamente como logger.exception() faz. A única diferença é que você pode facilmente mudar o nível de log para algo que não seja erro: Basta substituir logger.error por logger.warn, por exemplo.

Fun fact: O padrão da Grande Tarpa tem uma contrapartida quase diabólica, que você vai ler abaixo.

O Padrão “Pinpoint”

Agora vamos olhar para o outro extremo. Imagine que você está trabalhando com o SDK OpenBurrito, uma biblioteca resolvendo o problema crucial de encontrar uma junta de burrito no final da noite perto da sua localização atual. Suponha que ele tenha uma função chamada find_burrito_joints() que normalmente retorna uma lista de restaurantes adequados. Mas sob certas raras circunstâncias, ele pode levantar uma exceção chamada BurritoCriteriaConflict.

from openburrito import find_burrito_joints, BurritoCriteriaConflict# "criteria" is an object defining the kind of burritos you want.try: places = find_burrito_joints(criteria)except BurritoCriteriaConflict as err: logger.warn("Cannot resolve conflicting burrito criteria: {}".format(err.message)) places = list()

O padrão aqui é executar de forma otimizada algum código – a chamada para find_burrito_joints(), neste caso com um bloco de tentativa. Caso um tipo específico de exceção seja levantado, você registra uma mensagem, lida com a situação, e avança.

A diferença chave é a cláusula except. Com a Grande Tarpa, você está basicamente pegando e registrando qualquer possível exceção. Com a Pinpoint, você está pegando um tipo de exceção muito específico, que tem relevância semântica naquele lugar em particular no código.

Notice também, que eu uso logger.warn() em vez de logger.exception(). (Neste artigo, onde quer que você veja warn(), você pode substituir info(), ou error(), etc.) Em outras palavras, eu registro uma mensagem com uma determinada severidade ao invés de registrar todo o stack trace.

Por que eu estou jogando fora as informações do stack trace? Porque não é tão útil neste contexto, onde estou pegando um tipo específico de exceção, que tem um significado claro na lógica do código. Por exemplo, neste trecho:

characters = {"hero": "Homer", "villain": "Mr. Burns"}# Insert some code here that may or may not add a key called# "sidekick" to the characters dictionary.try: sidekick = charactersexcept KeyError: sidekick = "Milhouse"

Aqui, o KeyError não é apenas um erro qualquer. Quando ele é levantado, isso significa que uma situação específica ocorreu – não há um papel “sidekick” definido no meu elenco de personagens, então eu devo voltar a um padrão. Preencher o log com um stack trace não vai ser útil neste tipo de situação. E é aí que você vai usar Pinpoint.

O Padrão “Transformer”

Aqui, você está pegando uma exceção, registrando-a, e depois levantando uma exceção diferente. Primeiro, aqui está como funciona em Python 3:

try: something()except SomeError as err: logger.warn("...") raise DifferentError() from err

Em Python 2, você deve deixar o “from err”:

try: something()except SomeError as err: logger.warn("...") raise DifferentError()

(Isso acaba tendo grandes implicações. Mais sobre isso em um momento.) Você vai querer usar esse padrão quando uma exceção puder ser levantada que não mapeie bem a lógica da sua aplicação. Isso geralmente ocorre ao redor dos limites da biblioteca.

Por exemplo, imagine que você está usando o SDK openburrito para a sua aplicação assassina que permite que as pessoas encontrem junções de burritos no final da noite. A função find_burrito_joints() pode aumentar BurritoCriteriaConflict se estivermos sendo muito picuinhas. Esta é a API exposta pelo SDK, mas não mapeia convenientemente para a lógica de nível superior da sua aplicação. Um ajuste melhor neste ponto do código é uma exceção que você definiu, chamada NoMatchingRestaurants.

Nesta situação, você aplicará o padrão assim (para Python 3):

from myexceptions import NoMatchingRestaurantstry: places = find_burrito_joints(criteria)except BurritoCriteriaConflict as err: logger.warn("Cannot resolve conflicting burrito criteria: {}".format(err.message)) raise NoMatchingRestaurants(criteria) from err

Isto causa uma única linha de saída no seu log, e aciona uma nova exceção. Se nunca for pego, a saída de erro dessa exceção se parece com isto:

Traceback (most recent call last): File "transformerB3.py", line 8, in places = find_burrito_joints(criteria) File "/Users/amax/python-exception-logging-patterns/openburrito.py", line 7, in find_burrito_joints raise BurritoCriteriaConflictopenburrito.BurritoCriteriaConflictThe above exception was the direct cause of the following exception:Traceback (most recent call last): File "transformerB3.py", line 11, in raise NoMatchingRestaurants(criteria) from errmyexceptions.NoMatchingRestaurants: {'region': 'Chiapas'}

Agora isto é interessante. A saída inclui o stack trace para NoMatchingRestaurants. E relata também o instigante BurritoCriteriaConflict… especificando claramente qual foi o original.

No Python 3, as excepções podem agora ser encadeadas. A sintaxe raise ... from ... fornece isto. Quando você diz raise NoMatchingRestaurants(criteria) de err, isso levanta uma exceção de typeNoMatchingRestaurants. Esta excepção elevada tem um atributo chamado __cause__, cujo valor é a excepção instigante. Python 3 faz uso disso internamente ao relatar a informação de erro.

Como você faz isso em Python 2? Bem, você não pode. Esta é uma daquelas guloseimas que você só tem que atualizar para obter. Em Python 2, a sintaxe “raise … from” não é suportada, então sua saída de exceção incluirá apenas o stack trace para NoMatchingRestaurants. O padrão Transformer ainda é perfeitamente útil, claro.

O Padrão “Message and Raise”

Neste padrão, você registra que uma exceção ocorre em um determinado ponto, mas depois permite que ela se propague e seja manipulada em um nível superior:

try: something()except SomeError: logger.warn("...") raise

Você não está realmente manipulando a exceção. Você está apenas interrompendo temporariamente o fluxo para registrar um evento. Você fará isso quando você tiver um manipulador de nível mais alto para o erro, e quiser voltar atrás, mas também quiser registrar que o erro ocorreu, ou o significado dele, em um certo lugar no código.

Isso pode ser mais útil na resolução de problemas – quando você estiver recebendo uma exceção, mas tentando entender melhor o contexto de chamada. Você pode interceptar esta declaração de registro para fornecer informações úteis, e até mesmo implantar com segurança para produção se você precisar observar os resultados sob condições realistas.

O Anti-padrão “Mensagem Críptica”

Agora vamos voltar nossa atenção para alguns anti-padrões… coisas que você não deve fazer em seu código.

try: something()except Exception: logger.error("...")

Suponha que você ou alguém da sua equipe escreva este código, e então seis meses depois, você verá uma mensagem engraçada no seu registro. Algo como:

ERROR: something bad happened

Agora eu espero e rezo para que você não veja as palavras “algo ruim aconteceu” no seu log atual. No entanto, o texto do log real que você vê pode ser igualmente desconcertante. O que você faz a seguir?

Bem, a primeira coisa é descobrir onde na base de código esta vaga mensagem está sendo registrada. Se você tiver sorte, você será capaz de rapidamente entrar no código e encontrar exatamente uma possibilidade. Se você não tiver sorte, você pode encontrar a mensagem de log em vários caminhos de código completamente diferentes. O que lhe deixará com várias questões:

  • Qual delas está a desencadear o erro?
  • Or é várias delas? Ou TODAS elas?
  • Que entrada no log corresponde a que lugar?

Algumas vezes, no entanto, nem sequer é possível fazer o grep ou pesquisar através do código para encontrar onde está a acontecer, porque o texto do log está a ser gerado. Considere:

 what = "something" quality = "bad" event = "happened" logger.error("%s %s %s", what, quality, event)

Como você pesquisaria por isso? Você pode nem mesmo pensar nisso, a menos que você tenha pesquisado a frase completa primeiro e não tenha obtido nenhum resultado. E se você obteve um resultado, poderia facilmente ser um falso positivo.

A solução ideal é passar o argumento:exc_info>

try: something()except Exception: logger.error("something bad happened", exc_info=True)

Quando você faz isso, um rastreamento completo da pilha é incluído nos logs da aplicação. Isto diz-lhe exactamente que linha em que ficheiro está a causar o problema, quem o invocou, et cetera… toda a informação que precisa para começar a depurar.

The Most Diabolical Python Antipattern

Se alguma vez o vir fazer isto, irei a sua casa para confiscar os seus computadores, depois entrarei na sua conta github e apagarei todos os seus repos:

try: something()except Exception: pass

Na minha newsletter, refiro-me a isto como “The Most Diabolical Python Antipattern”. Note como isto não só não lhe dá qualquer informação de excepção útil. Ele também consegue esconder completamente o fato de que qualquer coisa está errada em primeiro lugar. Você pode nunca saber que você digitou uma variável errada – sim, isso na verdade mascara o NameError – até você ser paginado às 2 da manhã porque a produção está quebrada, de uma forma que leva você até o amanhecer para descobrir, porque todas as informações possíveis de solução de problemas estão sendo suprimidas.

Apenas não o faça. Se você sente que simplesmente precisa pegar e ignorar todos os erros, pelo menos jogue uma grande lona debaixo dela (ou seja, use logger.exception() ao invés de pass).

Mais padrões de registro de exceções

Existem muitos mais padrões para registro de informações de exceção em Python, com diferentes trade-offs, prós e contras. Que padrões você achou útil ou não? Deixe todos saberem nos comentários.

Saiba mais sobre o Python Logging Basics.

As marcas Loggly e SolarWinds, marcas de serviço, e logotipos são propriedade exclusiva da SolarWinds Worldwide, LLC ou de suas afiliadas. Todas as outras marcas registradas são propriedade de seus respectivos proprietários.

Deixe uma resposta

O seu endereço de email não será publicado.