Python >> Tutoriel Python >  >> Python

Signaler des exceptions dans les scripts Python avec Sentry

Les scripts Python sont le ciment qui permet à de nombreuses applications et à leur infrastructure de fonctionner, mais lorsque l'un de vos scripts génère une exception, vous ne le saurez peut-être pas immédiatement, sauf si vous disposez d'un emplacement central pour agréger les erreurs. C'est là que l'ajout de Sentrycan a résolu ce problème de journalisation des erreurs distribuées.

Dans ce didacticiel, nous verrons comment ajouter rapidement Sentry à un script Python nouveau ou existant pour signaler les erreurs dans un emplacement centralisé pour un débogage ultérieur.

Configuration de l'environnement de développement

Assurez-vous que Python 3 est installé. À l'heure actuelle, Python 3.8.3 est la dernière version de Python.

Au cours de ce tutoriel, nous allons également utiliser :

  • une instance Sentry hébergée sur sentry.io, à laquelle nous aurons besoin d'un compte pour accéder
  • la bibliothèque d'assistance Sentry Python pour envoyer des données d'exception à notre instance Sentry

Installez les bibliothèques de code ci-dessus dans un nouvel environnement virtuel Python à l'aide des commandes suivantes :

python -m venv sentryscript
source sentryscript/bin/activate

pip install sentry-sdk>=0.14.4

Notre environnement de développement est maintenant prêt et nous pouvons écrire du code qui lèvera des exceptions pour montrer comment utiliser Sentry.

Notez que tout le code de ce didacticiel se trouve dans le référentiel blog-code-examplesGit sur GitHub sous le répertoire python-script-sentry.

Un exemple de script pour charger des modules Python

Nous commencerons par écrire un petit script utile qui imprime les noms de tous les modules d'un package Python, puis nous y ajouterons Sentry lorsqu'il deviendra évident que la capture d'exceptions serait un ajout utile.

Créez un nouveau fichier nommé module_loader.py et écrivez-y les lignes de code suivantes pour nous permettre de l'exécuter facilement sur la ligne de commande.

import argparse

def import_submodules(package):
    return {}


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("package")
    args = parser.parse_args()

    package_to_load = args.package
    results = import_submodules(package_to_load)
    for r in results:
        print(str(r))

Le code ci-dessus prend un argument lorsque le script est invoqué à partir de la ligne de commande et utilise la valeur comme entrée dans le stubimport_submodules fonction qui contiendra le code pour parcourir l'arborescence des modules dans le package.

Ensuite, ajoutez les lignes de code en surbrillance suivantes pour utiliser importlib etpkgutil pour importer récursivement des modules depuis le paquet s'il en trouve un qui correspond au nom envoyé en tant que package arguments.

import argparse
import importlib
import pkgutil


def import_submodules(package):
    """Import all submodules of a module, recursively, including subpackages.

    :param package: package (name or actual module)
    :type package: str | module
    :rtype: dict[str, types.ModuleType]
    """
    if isinstance(package, str):
        package = importlib.import_module(package)
    results = {}
    for loader, name, is_pkg in pkgutil.walk_packages(package.__path__):
        full_name = package.__name__ + '.' + name
        try:
            results[full_name] = importlib.import_module(full_name)
            if is_pkg:
                results.update(import_submodules(full_name))
        except ModuleNotFoundError as mnfe:
            print("module not found: {}".format(full_name))
        except Exception as general_exception:
            print(general_exception)
    return results


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("package")
    args = parser.parse_args()

    package_to_load = args.package
    results = import_submodules(package_to_load)
    for r in results:
        print(str(r))

Le nouveau code ci-dessus parcourt tous les packages avec le walk_package fonction dans le pkgutil module de bibliothèque standard et essaie de l'importer en utilisant le import_module sur le nom du package plus le package sous forme de chaîne. Si le résultat est réussi, la fonction s'appellera récursivement pour importer des sous-modules dans le package importé. Si un module n'est pas trouvé, ou si un autre problème survient, des exceptions sont détectées afin que le script n'échoue pas mais puisse continuer à traiter les modules potentiels.

Testez le script complet pour voir ce qu'il affiche avec un package arbitraire sur la ligne de commande :

python module_loader.py importlib

L'exemple ci-dessus génère la sortie :

importlib._bootstrap
importlib._bootstrap_external
importlib.abc
importlib.machinery
importlib.resources
importlib.util

Essayer d'inspecter un paquet qui n'est pas installé donnera une erreur. Utilisez le script avec un package qui n'est pas installé dans votre environnement actuel.

python module_loader.py flask

La commande ci-dessus produit la trace suivante en raison d'un ModuleNotFoundError attendu .

Traceback (most recent call last):
  File "module_loader.py", line 35, in <module>
    results = import_submodules(package_to_load)
  File "module_loader.py", line 14, in import_submodules
    package = importlib.import_module(package)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'flask'

Si vous installez Flask dans votre environnement actuel, le module est trouvé et l'application parcourt la liste des modules et sous-modules.

Notre exemple de script est utilisable mais que se passe-t-il si nous exécutons ce code ou quelque chose de similaire sur un ou plusieurs serveurs que nous ne vérifions pas souvent ? C'est là qu'il serait utile d'avoir un moyen d'agréger une ou plusieurs sorties d'exception de scripts en un seul endroit. Sentry peut nous aider à atteindre cet objectif.

Ajout de rapports d'exception avec Sentry

Sentry peut soit être auto-hébergé, soit être utilisé comme service cloud via Sentry.io. Dans ce didacticiel, nous utiliserons la version hébergée dans le cloud car elle est plus rapide que la configuration de votre propre serveur et gratuite pour les petits projets.

Allez sur la page d'accueil de Sentry.io.

Connectez-vous à votre compte ou créez un nouveau compte gratuit. Vous serez sur le tableau de bord du compte principal après vous être connecté ou avoir terminé le processus d'inscription Sentry.

Il n'y a pas encore d'erreurs enregistrées sur le tableau de bord de notre compte, ce qui est normal car nous n'avons pas encore connecté notre compte au Pythonscript.

Vous souhaiterez créer un nouveau projet Sentry uniquement pour cette application. Cliquez donc sur "Projets" dans la barre latérale gauche pour accéder à la page Projets.

Sur la page Projets, cliquez sur le bouton "Créer un projet" dans le coin supérieur droit de la page.

Sélectionnez Python, donnez un nom à votre nouveau projet, puis appuyez sur le bouton "Créer un projet". Notre nouveau projet est prêt à être intégré à notre script Python.

Nous avons besoin de l'identifiant unique de notre compte et de notre projet pour autoriser notre code Python à envoyer des erreurs à cette instance Sentry. Le moyen le plus simple d'obtenir ce dont nous avons besoin est d'accéder à la page de documentation de démarrage de Python et de faire défiler jusqu'à la section "Configurer le SDK".

Copiez le paramètre de chaîne pour le init et définissez-la comme une variable d'environnement plutôt que de l'exposer directement dans le code de votre application.

export SENTRY_DSN='https://yourkeygoeshere.ingest.sentry.io/project-number'

Assurez-vous de remplacer "votreclévaici" par votre propre identifiant unique et "numéro de projet" par l'ID qui correspond au projet que vous venez de créer.

Vérifiez que le SENTRY_DSN est défini correctement dans votre shell en utilisant le echo commande :

echo $SENTRY_DSN

Modifiez l'application pour envoyer des informations d'exception à Sentry maintenant que nous avons notre identifiant unique. Ouvrir module_loader.py à nouveau et mettez à jour les lignes de code en surbrillance suivantes.

import argparse
import importlib
import os
import pkgutil
import sentry_sdk
from sentry_sdk import capture_exception

# find on https://docs.sentry.io/error-reporting/quickstart/?platform=python
sentry_sdk.init(dsn=os.getenv('SENTRY_DSN'))


def import_submodules(package):
    """Import all submodules of a module, recursively, including subpackages.

    :param package: package (name or actual module)
    :type package: str | module
    :rtype: dict[str, types.ModuleType]
    """
    if isinstance(package, str):
        package = importlib.import_module(package)
    results = {}
    for loader, name, is_pkg in pkgutil.walk_packages(package.__path__):
        full_name = package.__name__ + '.' + name
        try:
            results[full_name] = importlib.import_module(full_name)
            if is_pkg:
                results.update(import_submodules(full_name))
        except ModuleNotFoundError as mnfe:
            print("module not found: {}".format(full_name))
            capture_exception(mnfe)
        except Exception as general_exception:
            print(general_exception)
            capture_exception(general_exception)
    return results


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("package")
    args = parser.parse_args()

    package_to_load = args.package
    results = import_submodules(package_to_load)
    for r in results:
        print(str(r))

Ces nouvelles lignes de code importent theSentry Python SDK et os bibliothèque (pour lire les variables d'environnement système). L'application initialise alors le Sentry SDK avec la chaîne trouvée dans le SENTRY_DSN variables d'environnement. En bas dans le import_submodules la fonction appelle alors le capture_exception Fonction SDK chaque fois qu'un ModuleNotFoundException est levée ou une autre exception qui serait attrapée dans le plus large Exception seau.

Maintenant que notre code est en place, testons la nouvelle intégration de Sentry.

Tester le script et afficher les exceptions

Le moyen le plus simple de tester si le code Sentry fonctionne ou non est d'essayer d'importer un module qui n'existe pas. Disons que vous faites une faute de frappe dans votre commande et essayez d'exécuter le script sur importliba au lieu de importlib (peut-être parce que vous utilisez un affreux clavier "papillon" Macbook Pro au lieu d'un clavier durable). Essayez-le et voyez ce qui se passe :

python module_loader.py importliba

Le script s'exécutera et se terminera mais il y aura des erreurs car ce module n'existe pas. Grâce à notre nouveau code, nous pouvons visualiser les erreurs dans Sentry.

Vérifiez le tableau de bord Sentry pour voir l'erreur.

Nous pouvons également cliquer sur l'erreur pour en savoir plus sur ce qui s'est passé.

Vous pouvez également recevoir des rapports par e-mail sur les erreurs qui se produisent afin que vous n'ayez pas à rester toujours connecté au tableau de bord.

Avec tout cela configuré, nous avons maintenant une excellente base pour étendre le script et créer une meilleure gestion des erreurs avec Sentry à mesure que notre application Python devient plus complexe.

Quelle est la prochaine ?

Nous venons de créer un exemple de script qui génère tous les modules et sous-modules d'un package, puis nous y avons ajouté Sentry afin qu'il signale toute exception à notre instance hébergée centrale.

Il ne s'agit que d'une simple introduction à Sentry. Vous voudrez donc lire l'un des articles suivants pour en savoir plus :

  • Documentation Python Sentry
  • Comment utiliser Sentry avec Flask
  • Intégration de Sentry dans les files d'attente de tâches Celery

Vous pouvez également avoir une idée de ce qu'il faut coder ensuite dans votre projet Python en lisant la page de table des matières Full Stack Python.

Des questions? Contactez-moi via Twitter@fullstackpythonor @mattmakai. Je suis également sur GitHub avec le nom d'utilisateur mattmakai.

Quelque chose ne va pas avec ce message ? La source de cette page sur GitHuband soumet une pull request.