https://d226lax1qjow5r.cloudfront.net/blog/blogposts/building-a-python-error-alerting-tool/python_error-alert_1200x600.png

Construire un outil d'alerte d'erreur en Python

Publié le March 30, 2021

Temps de lecture : 20 minutes

Quelle que soit l'importance que nous accordons à la qualité et aux tests, il est presque certain que les logiciels connaîtront des problèmes à un moment ou à un autre. C'est pourquoi il est essentiel de disposer de journaux de surveillance pour suivre l'état de santé des applications.

Il existe certainement de nombreux services et projets open source qui s'occupent de la surveillance des journaux d'application. D'après mon expérience, cependant, ils sont généralement soit chers, soit longs à intégrer, soit bourrés de fonctionnalités que je n'utiliserai guère. Lorsque je déploie de petits projets qui ne nécessitent pas de surveillance sophistiquée, j'aimerais parfois disposer d'une solution native en Python pour recevoir des alertes simples lorsque quelque chose ne va pas dans mon code.

Le but de ce tutoriel est précisément de répondre à ce besoin. Nous allons construire un outil d'alerte d'erreur Python simple et flexible qui peut être inséré dans n'importe quel projet. Un objet gestionnaire HTTP de journalisation enverra de manière asynchrone des alertes via l'API SMS API de Vonage de Vonage sur nos téléphones lorsque de nouvelles erreurs ou de nouveaux avertissements, par exemple, arrivent.

Exigences

Nous utiliserons Python 3.9.1 (la dernière version stable) dans ce tutoriel, mais le code devrait également fonctionner avec Python 3.6+. Python est disponible sur Linux, macOS et Windows. Pour le télécharger et l'installer, suivez les instructions sur le site officiel.

Vous aurez également besoin d'un Account Vonage pour recevoir des alertes d'erreur par SMS. Créez un Account si vous n'êtes pas encore inscrit. Vonage offre aux nouveaux abonnés des crédits de 2,00 € pour tester gratuitement les API.

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

La clé et le secret de l'API de Vonage seront également nécessaires ; assurez-vous de les saisir dans les Paramètres du tableau de bord:

Vonage Dashboard

PyPI http-logging sera utilisée pour la mise en cache des journaux et la communication asynchrone avec l Vonage API. Elle permet d'éviter que notre application Python principale ne soit perturbée par le mécanisme d'alerte.

Préparation de l'environnement local

Virtualenv et dépendances

Créer un répertoire pour le projet :

~$ mkdir vonage-alerts ~$ cd vonage-alerts

Création d'un environnement virtuel est souvent une bonne pratique, alors commençons par cela :

~/vonage-alerts$ python3.9 -m venv .env ~/vonage-alerts$ source .env/bin/activate

Sur un ordinateur Windows, remplacez la commande source dans la dernière ligne ci-dessus par :

~/vonage-alerts$ .venv\Scripts\activate

Assurez-vous que l'environnement fonctionne comme prévu :

(.env) ~/vonage-alerts$ python --version Python 3.9.1

Créons maintenant notre fichier de dépendances Python :

(.env) ~/vonage-alerts$ touch requirements.txt

Ouvrez-le avec votre éditeur de texte préféré et ajoutez les lignes suivantes :

http-logging
vonage

Fermez le fichier et installez les dépendances avec la commande pip install avec la commande

(.env) ~/vonage-alerts$ pip install -r requirements.txt

Variables d'environnement

Notre logique de journalisation personnalisée nécessitera certaines informations qui seront fournies par le biais de variables d'environnement.

La clé API de Vonage est nécessaire pour l'authentification avec le service SMS. Un numéro de téléphone sera également nécessaire pour envoyer des SMS.

(.env) ~/vonage-alerts$ export VONAGE_API_KEY="abc123" (.env) ~/vonage-alerts$ export VONAGE_API_SECRET="xyz123" (.env) ~/vonage-alerts$ export ALERT_PHONE_NUMBER="+1234567890"

La commande export devrait fonctionner sous Linux et macOS. Sous Windows, utilisez set à la place. Si vous utilisez une commande PowerShell cette commande devrait faire le travail :

$Env: VONAGE_API_KEY = "abc123"
$Env: VONAGE_API_SECRET = "xyz123"
$Env: export ALERT_PHONE_NUMBER = "+1234567890"

Gestionnaire de journalisation HTTP

Comme nous l'avons déjà mentionné, nous nous appuierons sur l'option http-logging pour connecter nos journaux aux API de Vonage.

Un gestionnaire gestionnaire HTTP de journalisation de la bibliothèque standard de Python ferait également l'affaire. Cependant, nous n'allons pas l'utiliser car il génère des requêtes HTTP bloquantes, ce qui peut avoir un impact négatif sur l'exécution de notre application Python principale.

Les http-logging s'exécute silencieusement dans un thread d'arrière-plan et est également capable de mettre en cache les journaux dans une base de données SQLite locale afin de réduire le nombre de requêtes réseau. Pour ces raisons, elle sera beaucoup moins intrusive qu'un gestionnaire HTTP natif.

La bibliothèque est basée sur la bibliothèque Python Logstash Asyncmais elle est généralisée pour fonctionner avec n'importe quel backend autre que Logstash (dans notre tutoriel, nous utiliserons Vonage). Pour en savoir plus, consultez la wiki de documentation du projet.

Transport HTTP de Vonage

La première chose à faire est de créer une classe de transport HTTP personnalisée. C'est elle qui contient les instructions sur la façon d'envoyer les journaux à l'API Vonage.

Avant de nous y plonger, créons un nouveau fichier Python qui contiendra notre code de journalisation personnalisé :

(.env) ~/vonage-alerts$ touch logging_vonage.py

Ouvrez maintenant ce fichier - il est temps de s'amuser avec Python !

Notre propre classe de transport HTTP héritera de la classe [http_logging.AsyncHttpTransport](https://github.com/hacktlib/py-async-http-logging/wiki/3.-HTTP-Transport-Class) de la classe Tout d'abord, importez les bibliothèques nécessaires au début du fichier, puis déclarez une nouvelle classe comme indiqué ci-dessous :

import logging
import os

from vonage import Sms

from http_logging import HttpHost, SupportClass
from http_logging.handler import AsyncHttpHandler
from http_logging.transport import AsyncHttpTransport

class VonageHttpTransport(http_logging.transport.AsyncHttpTransport):
    pass

Pour l'instant, cette classe se comporte exactement comme l'original. Ajoutons-lui quelques fonctionnalités personnalisées. La classe AsyncHttpTransport implémente une méthode send qui est responsable de l'envoi des journaux à un hôte distant. Initialement, elle utilise la méthode requêtes pour cela. Dans notre cas, nous avons le Vonage SDKqui nous facilite grandement la vie et nous évite de nous ennuyer avec le protocole HTTP.

Ok, assez parlé. Commençons à coder avec le SDK Vonage en déclarant une nouvelle send méthode :

class VonageHttpTransport(AsyncHttpTransport):

    def send(self, events: dict, **kwargs) -> None:
        batches = self._HttpTransport__batches(events)

        sms_logs = ', '.join([
            f"{log['level']['name']}: {log['message']}"
            for batch in batches
            for log in batch
        ])

        sms_message = f'[Python Logger {self.logger_name}] {sms_logs}'

        sms_client = Sms(
            key=self.vonage_api_key,
            secret=self.vonage_api_secret,
        )

        response = sms_client.send_message({
            'from': f'Python Logger {self.logger_name}',
            'to': self.alert_phone_number,
            'text': sms_message,
        })

        if not response['messages'][0]['status'] == 0:
            raise ConnectionError(response["messages"][0].get("error-text"))

La méthode send prend un argument events une liste qui est convertie dans un lot de journaux à l'aide de la méthode HttpTransport.__batches méthode. Les lots sont ensuite traités pour extraire les points de données de base dans une chaîne d'enregistrement.

Chaque chaîne de journal contient uniquement le nom du niveau de journal (par exemple, "Avertissement" ou "Erreur") et un message de journal. SMS est l'abréviation de Short Message Service (service de messages courts), c'est pourquoi nous voulons que notre message d'alerte soit court. Notre objectif premier est d'alerter, et non de prendre en charge, le débogage complet par SMS. Des informations minimales sont envoyées pour fournir un contexte et aider le développeur à démarrer le processus de débogage.

Les journaux sont ensuite concaténés à l'aide de la méthode string.join et préfixés avec le nom de l'enregistreur pour fournir des informations concernant le contexte de l'application (cela devrait être utile dans le cas où plusieurs projets utilisent cet outil d'alerte).

Enfin, nous instançons un vonage.Sms client à partir du SDK Vonage et l'utilisons pour envoyer le SMS à notre téléphone. L'état de la réponse est vérifié et, s'il n'est pas "OK", nous soulevons un message ConnectionError. Cette erreur levée permet de s'assurer que le mécanisme d'alerte du journal est réessayé plus tard et ne perturbera pas notre application Python principale, puisque la classe VonageHttpTransport s'exécutera dans un thread d'arrière-plan.

Remarquez que nous utilisons certains attributs de classe dans la nouvelle méthode send dans la nouvelle méthode : logger_name, vonage_api_key, vonage_api_secret, alert_phone_number. Surchargeons la méthode __init__ pour nous assurer qu'ils sont correctement définis lors de l'instanciation de la classe :

class VonageHttpTransport(AsyncHttpTransport):

    def __init__(
        self,
        logger_name: str,
        vonage_api_key: str,
        vonage_api_secret: str,
        alert_phone_number: str,
        *args,
        **kwargs,
    ) -> None:
        self.logger_name = logger_name
        self.vonage_api_key = vonage_api_key
        self.vonage_api_secret = vonage_api_secret
        self.alert_phone_number = alert_phone_number
        super().__init__(*args, **kwargs)

Notre nouvelle classe de transport HTTP est maintenant prête. Mais avant de passer à une véritable action de journalisation, nous devons d'abord créer la logique qui instanciera un véritable Logger à l'aide de la nouvelle classe VonageHttpTransport nouvelle classe.

Gestionnaire de journaux Vonage

La classe VonageHttpTransport a fière allure, mais elle ne peut pas aller au combat toute seule. Nous ne sommes pas en mesure de l'utiliser pour enregistrer quoi que ce soit dans nos Applications, alors faisons un pas de plus et rendons-la prête au combat.

La pièce manquante de notre puzzle est une classe de gestionnaire HTTP. Il devrait s'agir d'une classe http_logging.AsyncHttpHandlermais, bien sûr, instanciée avec la classe personnalisée VonageHttpTransport.

Créons une getLogger à l'intérieur de logging_vonage.pypour imiter le comportement natif de Python logging.getLogger de Python :

def getLogger(name: str) -> logging.Logger:
    pass

Comme la fonction getLogger de Python, la nôtre prend une chaîne de noms en argument et renvoie une instance de la classe logging.Logger . Ensuite, nous allons construire la fonctionnalité de cette fonction étape par étape.

Nous commençons par instancier un HttpHost. Ce n'est pas vraiment nécessaire pour l'API VonageHttpTransportpuisque nous déléguons les requêtes HTTP au SDK de Vonage, mais c'est une partie nécessaire de la signature API de la bibliothèque http-logging :

def getLogger(name: str) -> logging.Logger:
    host = HttpHost(name='vonage.com')

Ensuite, nous avons besoin d'un SupportClass qui contient notre objet de transport HTTP :

support_class = SupportClass(
        http_host=host,
        _transport=VonageHttpTransport(
            http_host=host,
            logger_name=name,
            vonage_api_key=os.environ.get('VONAGE_API_KEY'),
            vonage_api_secret=os.environ.get('VONAGE_API_SECRET'),
            alert_phone_number=os.environ.get('ALERT_PHONE_NUMBER'),
        ),
    )

Cet objet SupportClass est ensuite utilisé pour instancier notre AsyncHttpHandler:

vonage_handler = AsyncHttpHandler(
        http_host=host,
        support_class=support_class,
    )

Enfin, nous instancions un objet logging.Logger nous ajoutons l'objet vonage_handler comme gestionnaire et nous le renvoyons :

logger = logging.getLogger(name)
    logger.addHandler(vonage_handler)

    return logger

Au final, notre fonction getLogger devrait ressembler à ce qui suit :

def getLogger(name: str) -> logging.Logger:
    host = HttpHost(name='vonage.com')

    support_class = SupportClass(
        http_host=host,
        _transport=VonageHttpTransport(
            http_host=host,
            logger_name=name,
            vonage_api_key=os.environ.get('VONAGE_API_KEY'),
            vonage_api_secret=os.environ.get('VONAGE_API_SECRET'),
            alert_phone_number=os.environ.get('ALERT_PHONE_NUMBER'),
        ),
    )

    vonage_handler = AsyncHttpHandler(
        http_host=host,
        support_class=support_class,
    )

    logger = logging.getLogger(name)
    logger.addHandler(vonage_handler)

    return logger

Notez que la clé et le secret de l'API ainsi que le numéro de téléphone sont récupérés à partir des variables d'environnement que nous avons définies au début de ce tutoriel. Cela offre une certaine flexibilité au cas où nous voudrions utiliser ce code dans plusieurs projets, et évite également de coder en dur les secrets de l'API, ce qui n'est généralement pas une bonne idée ;)

Manipulateurs multiples

Le mécanisme de journalisation de Python est très puissant, et l'objet logging.Logger est suffisamment flexible pour l'étendre à de multiples gestionnaires.

Comme expliqué ci-dessus, la classe VonageHttpTransport enverra un minimum d'informations sur les journaux en raison des limitations inhérentes à la longueur du texte du système SMS. Néanmoins, dans le cas d'une erreur nécessitant un débogage plus approfondi, nous voudrons certainement récupérer l'intégralité de la trace de la pile, des informations sur la ligne de code qui a échoué, les horodatages exacts, etc.

Nous pouvons répondre à cette demande d'enregistrement détaillé en utilisant la fonction Logger.addHandler et en ajoutant un ou plusieurs gestionnaires supplémentaires à l'objet Vonage Logger à l'objet Vonage.

Par exemple, pour envoyer des logs non seulement à notre téléphone mais aussi à la console, nous pouvons utiliser la commande logging.StreamHandlercomme indiqué ci-dessous :

import logging
import logging_vonage

logger = logging_vonage.getLogger('')
logger.addHandler(logging.StreamHandler())

Tout ce qui est enregistré avec l'objet logger sera imprimé sur la console et envoyé à notre téléphone par l'intermédiaire de l'API SMS de Vonage.

A logging.FileHandler peut être utilisé pour stocker les journaux dans le système de fichiers local si cela s'avère judicieux dans le cadre d'une implémentation. Vous pouvez également utiliser le même http_logging.AsyncHttpHandler encore une fois, mais dans ce cas, en envoyant les journaux à un hôte différent de l'API Vonage. Test avec un exemple d'application Très bien, il est temps de voir un peu d'action réelle avec des cloches et des sifflets. Je plaisante, nous sommes sur le point de faire biper nos téléphones avec l'API SMS de Vonage :D

Créer un nouveau fichier dans le répertoire du projet appelé sample_app:

(.env) ~/vonage-alerts$ touch sample_app.py

Ouvrez-le et ajoutez le contenu suivant :

import logging
import logging_vonage


logger = logging_vonage.getLogger('sampleapp')

logger.addHandler(logging.StreamHandler())

logger.debug('Debugging...')
logger.warning('You\'ve been warned!')
logger.error('This is a test error')

try:
    1/0
except ArithmeticError as exc:
    logger.exception(exc)

Remarquez que nous instancions un objet logger à partir du module logging_vonage que nous avons construit plus tôt. L'élément logging.StreamHandler() est également utilisé pour que les traces complètes soient enregistrées dans notre console, et pas seulement envoyées à notre téléphone.

Dans la console, exécutez ce script avec :

(.env) ~/vonage-alerts$ python sample_app.py

La sortie suivante doit être imprimée sur la console :

You've been warned!
This is a test error
division by zero
Traceback (most recent call last):
  File "/home/vonage-alerts/sample_app.py", line 14
    1/0
ZeroDivisionError: division by zero

Avec un peu de chance, si vous avez tout configuré correctement (un Account Vonage et une clé/secret API), vous devriez recevoir sous peu un SMS contenant le texte suivant :

[Python Logger sampleapp] WARNING: You've been warned!, ERROR: This is a test error, ERROR: division by zero

Debug message

Remarquez que le message de débogage 'Debugging...' n'a pas été imprimé sur la console ni concaténé dans le message SMS. Cela est dû au fait que le niveau de journalisation par défaut de la bibliothèque de journalisation Python est WARNING. Le niveau DEBUG est considéré comme inférieur à WARNING et est donc rejeté.

Si vous souhaitez que le DEBUG soit capturé, réglez le niveau en conséquence, comme indiqué ci-dessous :

logger = logging_vonage.getLogger('sampleapp') logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG)

Exécutez à nouveau le sample_app.py et vous devriez voir le message de débogage imprimé sur la console et concaténé dans le message SMS.

Observez que, malgré notre logger nous nous appuyons sur un gestionnaire personnalisé (http_logging.AsyncHttpHandler) et une classe de transport (logging_vonage.VonageHttpTransport), il se comporte comme n'importe quel autre objet Python Logger Python. Il est donc parfaitement compatible avec tout projet Python que vous avez actuellement, au cas où vous souhaiteriez intégrer le mécanisme d'alerte par SMS que nous venons de développer dans votre pile et dans tout autre projet futur.

Conclusion

Et voilà ! Nous disposons maintenant d'un outil d'alerte Python simple et non intrusif pour rester au fait de ce qui se passe avec les applications que nous avons déployées. Il étend les fonctionnalités de base de Python logging pour utiliser la même API que celle à laquelle nous sommes habitués, et fonctionne partout où nos applications Python sont exécutées. Financièrement, il n'a pas de coûts fixes et sa maintenance est relativement peu coûteuse (frais de SMS uniquement).

La bibliothèque http-logging conserve un cache local des journaux, de sorte qu'en cas de panne de l'API Vonage ou de l'opérateur de téléphonie mobile ou d'instabilité du réseau, notre enregistreur peut réessayer d'envoyer les alertes SMS quelque temps plus tard.

Partager:

https://a.storyblok.com/f/270183/400x373/ed2dc20b00/renato-byrro.png
Renato Byrro

Renato is a backend software developer and a father of two amazing kids who won’t let him sleep so that he can enjoy spending nights connecting APIs around.