Fermer

décembre 5, 2018

Comment déboguer les erreurs Python –


Cet article a été publié à l'origine sur Sentry . Merci de votre soutien aux partenaires qui rendent SitePoint possible.

L’une des fonctionnalités les plus puissantes de Sentry est fournie avec des langages comme Python. En Python, PHP et la machine virtuelle Java, nous sommes en mesure d’analyser plus en profondeur le runtime et de vous fournir des données supplémentaires sur chaque image de votre pile d’appel. À un niveau élevé, cela vous permet de savoir des choses comme appeler des arguments à vos fonctions, ce qui vous permet de reproduire et de comprendre plus facilement une erreur. Voyons à quoi cela ressemble et comment cela fonctionne sous le capot.

Nous commencerons par ce qu'une erreur Python pourrait ressembler typiquement dans votre terminal ou dans un système de journalisation standard: [19659004] TypeError: chaîne ou tampon attendu
  Fichier "sentry / stacktraces.py", ligne 309, dans process_single_stacktrace
    processable_frame, processing_task)
  Fichier "sentry / lang / native / plugin.py", ligne 196, dans process_frame
    in_app = (in_app et pas self.sym.is_internal_function (raw_frame.get ('fonction')))
  Fichier "sentry / lang / native / symbolizer.py", ligne 278, dans is_internal_function
    return _internal_function_re.search (fonction) n'est pas None

Bien que cela nous donne une idée du type et de l’emplacement de l’erreur, cela ne nous aide malheureusement pas à comprendre ce qui la cause vraiment. Il est possible qu’il passe un entier ou un NoneType, mais, de façon réaliste, cela peut être un nombre quelconque de choses. En devinant, cela ne nous mènera que jusque-là, et nous avons vraiment besoin de savoir quelle est la fonction .

Une option facile et souvent très accessible pour ce faire consisterait à ajouter un peu de journalisation. Il existe quelques points d’entrée différents où nous pourrions mettre la journalisation, ce qui facilite sa mise en œuvre. Cela nous permet également de nous assurer que nous obtenons spécifiquement la réponse souhaitée. Par exemple, nous voulons savoir ce qu'est le type de :


# ...
logging.debug ("fonction est de type% s", type (fonction))

Un autre avantage de l'exploitation forestière comme celle-ci est qu'elle pourrait être transférée à la production. La conséquence est généralement que vous n'enregistrez pas d'instructions de journal de niveau DEBUG en production car le volume peut être important et peu utile. Vous devez également souvent planifier à l'avance pour vous assurer de consigner les différents cas d'échec pouvant survenir et le contexte approprié pour chacun.

Supposons que ce didacticiel suppose que nous ne pouvons pas le faire en production, 'planifiez cela à l'avance et essayez plutôt de déboguer et de reproduire cela en développement.

Le débogueur Python

Le Python Debugger (PDB) est un outil qui vous permet de faire des pas via votre pile d'appel en utilisant des points d'arrêt. L’outil lui-même est inspiré du débogueur GNU (GDB) de GNU et, bien que puissant, il peut souvent être écrasant s’il n’est pas familier. C'est certainement une expérience qui va devenir plus facile avec la répétition, et nous n'aborderons ici que quelques concepts de haut niveau, par exemple.

La première chose que nous voudrions faire est d'instrumenter notre code pour l'ajouter. un point d'arrêt. Dans notre exemple ci-dessus, nous pourrions effectivement nous instrumenter symbolizer.py . Ce n’est pas toujours le cas, car l’exception se produit parfois dans du code tiers. Peu importe où vous l’instrumentez, vous pourrez toujours sauter dans la pile. Commençons par changer ce code:

 def is_internal_function (self, function):
    # ajouter un point d'arrêt pour PDB
    essayer:
        return _internal_function_re.search (fonction) n'est pas None
    sauf exception:
        importer pdb; pdb.set_trace ()
        élever

Nous limitons cela à l'exception, car il est courant que le code soit exécuté avec succès la plupart du temps, parfois en boucle, et que vous ne souhaitiez pas suspendre l'exécution à chaque itération.

Une fois que nous avons atteint ce point d'arrêt (ce que set_trace () enregistre), nous allons être placés dans un environnement spécial semblable à un shell:

 # ...
(Pdb)

Il s'agit de la console PDB. Son fonctionnement est similaire à celui du shell Python. En plus de pouvoir exécuter la plupart des codes Python, nous les exécutons également dans un contexte spécifique de notre pile d’appels. Cet endroit est le point d'entrée. C’est plutôt là que vous avez appelé set_trace () . Dans l'exemple ci-dessus, nous sommes exactement là où nous devons être, nous pouvons donc facilement saisir le type de fonction :

 (Pdb) de type (fonction)

Bien sûr, nous pourrions aussi simplement saisir toutes les variables de portée locale en utilisant l’une des fonctions intégrées de Python:

 (Pdb) locals ()
{..., 'fonction': Aucune, ...}

Dans certains cas, il est possible que nous devions naviguer vers le haut et vers le bas dans la pile pour atteindre le cadre où la fonction est en cours d'exécution. Par exemple, si notre instrumentation set_trace () nous avait placés plus haut dans la pile, potentiellement au premier cadre, nous aurions utilisé vers le bas pour sauter dans les cadres intérieurs jusqu'à atteindre un emplacement contenant les informations nécessaires:

 (Pdb) down
-> in_app = (in_app et pas self.sym.is_internal_function (raw_frame.get ('fonction')))
(Pdb) bas
-> return _internal_function_re.search (fonction) n'est pas None
(Pdb) type (fonction)

Nous avons donc identifié le problème: function est un NoneType . Bien que cela ne nous explique pas vraiment pourquoi, il nous donne au moins des informations précieuses pour accélérer nos scénarios de test.

Débogage en production

L’APB fonctionne donc très bien en développement, mais qu’en est-il de la production? Examinons un peu plus en détail ce que Python nous a donné pour répondre à cette question.

Ce qui est génial avec le runtime CPython – c’est le runtime standard que la plupart des gens utilisent – c’est qu’il permet un accès facile à la pile d’appels actuelle. Certaines exécutions (telles que PyPy) fourniront des informations similaires, mais ce n’est pas garanti. Lorsque vous frappez une exception, la pile est exposée via sys.exc_info () . Voyons ce que cela donne comme exception typique:

 >>> essayez:
...   dix
... sauf:
... systèmes d'importation; sys.exc_info ()
...
(,
    ZeroDivisionError ('division entière ou modulo par zéro',),
    

Nous allons éviter d’aller trop loin dans ce sujet, mais nous avons un tuple de trois informations: la classe de l’exception, l’instance réelle de l’exception et un objet traceback. Le bit qui nous intéresse ici est l’objet traceback. Il est à noter que vous pouvez également récupérer ces informations en dehors des exceptions à l'aide du module traceback . de la documentation sur l’utilisation de ces structures, mais plongeons-nous en nous-mêmes pour essayer de les comprendre. Dans l’objet traceback, nous avons une foule d’informations à notre disposition, mais il faudra un peu de travail – et de magie – pour accéder à:

 >>> exc_type, exc_value, tb = exc_info
>>> tb.tb_frame

Une fois que nous avons obtenu un cadre, CPython expose des méthodes pour obtenir des locals de pile – c’est-à-dire que toutes les variables étendues sont associées à ce cadre d’exécution. Par exemple, regardez le code suivant:

 def foo (bar = None):
    foo = "bar"
    dix

Générons une exception avec ce code:

 essayez:
    foo ()
sauf:
    exc_type, exc_value, tb = sys.exc_info ()

Et enfin, accédons aux locaux par f_locals sur l’objet :

 >>> à partir de pprint import pprint
>>> pprint (tb.tb_frame.f_locals)
{'__builtins__': ,
    '__doc__': aucun,
    '__name__': '__main__',
    '__package__': Aucun,
    'exc_info': (,
                ZeroDivisionError ('division entière ou modulo par zéro',),
                ),
    'foo': ,
    'history': '/Users/dcramer/.pythonhist',
    'os': ,
    'pprint': ,
    'print_function': _Feature ((2, 6, 0, 'alpha', 2), (3, 0, 0, 'alpha', 0), 65536),
    'readline': ,
    'stack': [],
    'sys': ,
    'tb': ,
    'tb_next': aucun,
    'write_history': }

Ce que nous voyons ci-dessus n’est pas vraiment utile. La raison en est que nous sommes à un niveau supérieur de notre portée, nous voyons donc foo défini, mais rien qui soit réellement pertinent pour la fonction s’appelle. Ce ne sera pas toujours vrai, mais dans notre exemple trivial, il l’est. Pour trouver les informations que nous recherchons, nous devons aller plus loin:

 >>> inner_frame = tb.tb_next.tb_frame
>>> pprint (inner_frame.f_locals)
{'bar': aucun, 'foo': 'bar'}

Vous pouvez rapidement comprendre en quoi cela pourrait être utile si nous revenons à notre TypeError d'origine. Dans ce cas, avec l'introspection ci-dessus, nous découvrons que la fonction qui devrait être une chaîne, est en fait définie sur un NoneType . Nous savons que parce que Sentry a capturé cette erreur pour nous et extrait automatiquement les sections locales de pile pour chaque image:

 Exception

C’est l’une des premières fonctionnalités que nous avons intégrée à Sentry et, à ce jour, , il reste l’un des composants de débogage les plus précieux que nous puissions fournir. Bien que nous ne puissions pas toujours vous fournir les détails nécessaires pour reproduire une exception, notre expérience nous a fait savoir qu'il était très rare que nous ayons réellement besoin d'instrumenter manuellement quelque chose pour comprendre le contexte et, en fin de compte, résoudre le problème.

Etes-vous curieux de connaître l’implémentation complète, qui utilise également d’autres composants de la structure de retraçage de Python, vous pouvez toujours consulter le code source de notre Python SDK sur GitHub . En PHP et la machine virtuelle, l’approche est légèrement différente en raison des durées d’exécution. Si vous êtes intéressé, vous trouverez également ces référentiels dans Sentry’s GitHub . Si vous utilisez Sentry, nous allons généralement automatiquement instrumenter pour vous, même si la JVM nécessite un peu de configuration (documents à venir).




Source link