Consignation exceptionnelle des exceptions en Python

Aaron Maxwell est l’auteur de Powerful Python.

Les exceptions se produisent. Et en tant que développeurs, nous devons tout simplement y faire face. Même lorsqu’on écrit des logiciels pour nous aider à trouver des burritos.

Attendez, je m’avance un peu… nous y reviendrons. Comme je le disais : La façon dont nous traitons les exceptions dépend du langage. Et pour les logiciels fonctionnant à l’échelle, la journalisation est l’un des outils les plus puissants et les plus précieux dont nous disposons pour traiter les conditions d’erreur. Regardons quelques façons dont ceux-ci fonctionnent ensemble.

Le Patron « Big Tarp »

Nous allons commencer à un extrême:

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

C’est un large fourre-tout. Il convient à un certain chemin de code où vous savez que le bloc de code (c’est-à-dire main_loop()) peut soulever un certain nombre d’exceptions que vous n’avez peut-être pas anticipées. Et plutôt que de permettre au programme de se terminer, vous décidez qu’il est préférable d’enregistrer les informations d’erreur, et de continuer à partir de là.

La magie ici est avec la méthode exception. (logger est l’objet logger de votre application – quelque chose qui a été retourné par logging.getLogger(), par exemple). Cette merveilleuse méthode capture la trace de pile complète dans le contexte du bloc except, et l’écrit en entier.

Notez que vous n’avez pas à passer l’objet exception ici. Vous passez une chaîne de message. Cela enregistrera la trace complète de la pile, mais fera précéder votre message d’une ligne. Ainsi, le message multiligne qui apparaît dans votre journal pourrait ressembler à ceci:

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

Les détails de la trace de la pile n’ont pas d’importance – il s’agit d’un exemple de jouet qui illustre une solution adulte à un problème du monde réel. Remarquez simplement que la première ligne est le message que vous avez transmis à logger.exception(), et que les lignes suivantes sont la trace de pile complète, y compris le type d’exception (ZeroDivisionError dans ce cas). Il attrapera et enregistrera tout type d’erreur de cette façon.

Par défaut, logger.exception utilise le niveau d’enregistrement de ERROR. Alternativement, vous pouvez utiliser les méthodes de journalisation régulières- logger.debug(), logger.info(), logger.warn(), etc.-et passer le paramètre exc_info, en le définissant à True:

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

Mettre exc_info à True fera que la journalisation inclura la trace de la pile complète…. exactement comme logger.exception() le fait. La seule différence est que vous pouvez facilement changer le niveau de journalisation à autre chose que l’erreur : Remplacez simplement logger.error par logger.warn, par exemple.

Fun fact : Le motif Big Tarp a une contrepartie presque diabolique, que vous lirez plus bas.

Le motif « Pinpoint »

Regardons maintenant l’autre extrême. Imaginez que vous travaillez avec le SDK OpenBurrito, une bibliothèque qui résout le problème crucial de trouver un joint de burrito de fin de soirée près de votre emplacement actuel. Supposons qu’elle possède une fonction appelée find_burrito_joints() qui renvoie normalement une liste de restaurants appropriés. Mais dans certaines circonstances rares, elle peut lever une exception appelée 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()

Le modèle ici est d’exécuter de manière optimiste un certain code – l’appel à find_burrito_joints(), dans ce cas – dans un bloc try. Dans le cas où un type d’exception spécifique est soulevé, vous enregistrez un message, traitez la situation et passez à autre chose.

La différence clé est la clause except. Avec la grande bâche, vous attrapez et enregistrez toutes les exceptions possibles. Avec Pinpoint, vous attrapez un type d’exception très spécifique, qui a une pertinence sémantique à cet endroit particulier du code.

Notez également, que j’utilise logger.warn() plutôt que logger.exception(). (Dans cet article, partout où vous voyez warn(), vous pouvez substituer info(), ou error(), etc.) En d’autres termes, j’enregistre un message à une gravité particulière au lieu d’enregistrer toute la trace de pile.

Pourquoi est-ce que je jette les informations de la trace de pile ? Parce qu’elle n’est pas aussi utile dans ce contexte, où j’attrape un type d’exception spécifique, qui a une signification claire dans la logique du code. Par exemple, dans ce snippet:

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"

Ici, la KeyError n’est pas n’importe quelle erreur. Lorsqu’elle est soulevée, cela signifie qu’une situation spécifique s’est produite – à savoir qu’il n’y a pas de rôle de  » sidekick  » défini dans ma distribution de personnages, je dois donc me rabattre sur un défaut. Remplir le journal avec une trace de pile ne va pas être utile dans ce genre de situation. Et c’est là que vous utiliserez Pinpoint.

Le motif « Transformer »

Ici, vous attrapez une exception, l’enregistrez, puis levez une autre exception. Tout d’abord, voici comment cela fonctionne en Python 3:

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

En Python 2, vous devez laisser tomber le « from err »:

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

(Cela s’avère avoir de grandes implications. Nous y reviendrons dans un moment.) Vous voudrez utiliser ce pattern lorsqu’une exception peut être levée qui ne correspond pas bien à la logique de votre application. Cela se produit souvent autour des frontières de la bibliothèque.

Par exemple, imaginez que vous utilisez le openburrito SDK pour votre killer app qui permet aux gens de trouver des joints de burrito tard dans la nuit. La fonction find_burrito_joints() peut lever BurritoCriteriaConflict si nous sommes trop pointilleux. C’est l’API exposée par le SDK, mais elle ne correspond pas commodément à la logique de plus haut niveau de votre application. Un meilleur ajustement à ce point du code est une exception que vous avez définie, appelée NoMatchingRestaurants.

Dans cette situation, vous appliquerez le modèle comme ceci (pour 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

Cela provoque une seule ligne de sortie dans votre journal, et déclenche une nouvelle exception. Si elle n’est jamais attrapée, la sortie d’erreur de cette exception ressemble à ceci:

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'}

Maintenant c’est intéressant. La sortie inclut la trace de la pile pour NoMatchingRestaurants. Et elle signale également l’instigateur BurritoCriteriaConflict… en spécifiant clairement lequel était l’original.

Dans Python 3, les exceptions peuvent maintenant être enchaînées. La syntaxe raise ... from ... le permet. Lorsque vous dites raise NoMatchingRestaurants(criteria) from err, cela lève une exception de typeNoMatchingRestaurants. Cette exception levée possède un attribut nommé __cause__, dont la valeur est l’exception instigatrice. Python 3 s’en sert en interne pour rapporter les informations d’erreur.

Comment faites-vous cela dans Python 2 ? Eh bien, vous ne pouvez pas. C’est l’un de ces goodies que vous devez juste mettre à niveau pour obtenir. En Python 2, la syntaxe « raise … from » n’est pas supportée, donc votre sortie d’exception n’inclura que la trace de pile pour NoMatchingRestaurants. Le patron Transformer est toujours parfaitement utile, bien sûr.

Le patron « Message and Raise »

Dans ce patron, vous enregistrez qu’une exception se produit à un point particulier, mais vous lui permettez ensuite de se propager et d’être traitée à un niveau supérieur :

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

Vous ne traitez pas réellement l’exception. Vous interrompez juste temporairement le flux pour enregistrer un événement. Vous ferez cela lorsque vous avez spécifiquement un gestionnaire de plus haut niveau pour l’erreur, et que vous voulez vous rabattre sur cela, tout en voulant également journaliser que l’erreur s’est produite, ou sa signification, à un certain endroit dans le code.

Cela peut être le plus utile dans le dépannage – lorsque vous obtenez une exception, mais que vous essayez de mieux comprendre le contexte d’appel. Vous pouvez interjeter cette déclaration de journalisation pour fournir des informations utiles, et même déployer en toute sécurité en production si vous avez besoin d’observer les résultats dans des conditions réalistes.

L’anti-modèle « Message cryptique »

Maintenant, nous allons porter notre attention sur certains anti-modèles… des choses que vous ne devriez pas faire dans votre code.

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

Supposons que vous ou quelqu’un de votre équipe écrivez ce code, et puis six mois plus tard, vous voyez un drôle de message dans votre journal. Quelque chose comme:

ERROR: something bad happened

Maintenant, j’espère et je prie que vous ne verrez pas les mots « quelque chose de mauvais est arrivé » dans votre journal réel. Cependant, le texte du journal réel que vous voyez peut être tout aussi déconcertant. Que faites-vous ensuite ?

Bien, la première chose est de déterminer où dans la base de code ce vague message est enregistré. Si vous êtes chanceux, vous serez en mesure de grep rapidement à travers le code et de trouver exactement une possibilité. Si vous n’avez pas de chance, vous trouverez peut-être le message d’enregistrement dans plusieurs chemins de code complètement différents. Ce qui vous laissera avec plusieurs questions :

  • Quel est celui qui déclenche l’erreur ?
  • Ou est-ce plusieurs d’entre eux ? Ou TOUS ?
  • Quelle entrée dans le journal correspond à quel endroit ?

Parfois, cependant, il n’est même pas possible de faire un grep ou une recherche dans le code pour trouver où cela se passe, parce que le texte du journal est généré. Considérez:

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

Comment pourriez-vous même rechercher cela ? Vous n’y penserez peut-être même pas, à moins que vous n’ayez d’abord recherché l’expression complète et que vous n’ayez obtenu aucun résultat. Et si vous obteniez un résultat, il pourrait facilement s’agir d’un faux positif.

La solution idéale est de passer l’argument exc_info:

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

Lorsque vous faites cela, une trace de pile complète est incluse dans les journaux de l’application. Cela vous indique exactement quelle ligne dans quel fichier cause le problème, qui l’a invoqué, et cetera… toutes les informations dont vous avez besoin pour commencer à déboguer.

Le plus diabolique des anti-modèles Python

Si jamais je vous vois faire celui-ci, je viendrai chez vous pour confisquer vos ordinateurs, puis je piraterai votre compte github et supprimerai tous vos repos :

try: something()except Exception: pass

Dans ma newsletter, je fais référence à ceci comme « Le plus diabolique des anti-modèles Python ». Remarquez comment cela ne parvient pas seulement à vous donner des informations utiles sur les exceptions. Il parvient également à cacher complètement le fait que quelque chose ne va pas en premier lieu. Il se peut que vous ne sachiez jamais que vous avez mal saisi un nom de variable – oui, cela masque en fait NameError – jusqu’à ce que vous soyez bipé à 2 heures du matin parce que la production est cassée, d’une manière qui vous prend jusqu’à l’aube pour comprendre, parce que toutes les informations de dépannage possibles sont supprimées.

Ne le faites simplement pas. Si vous sentez que vous devez simplement attraper et ignorer toutes les erreurs, au moins jeter une grande bâche sous elle (c’est-à-dire utiliser logger.exception() au lieu de pass).

Plus de patrons de journalisation des exceptions

Il y a beaucoup plus de patrons pour la journalisation des informations d’exception en Python, avec différents compromis, avantages et inconvénients. Quels sont les patterns que vous avez trouvés utiles, ou pas ? Faites-le savoir à tout le monde dans les commentaires.

En savoir plus sur les bases de la journalisation Python.

Les marques, marques de service et logos de Loggly et SolarWinds sont la propriété exclusive de SolarWinds Worldwide, LLC ou de ses sociétés affiliées. Toutes les autres marques sont la propriété de leurs propriétaires respectifs.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.