Alerte d'erreur Python avec Twilio et SendGrid

May 25, 2021
Rédigé par
Renato Byrro
Contributeur
Les opinions exprimées par les contributeurs de Twilio sont les leurs

Alerte d'erreur Python avec Twilio et SendGrid

La détection des erreurs est un élément clé de toute application déployée dans le cloud. Quel que soit le degré d'attention que nous accordons aux tests et à la qualité des logiciels, il y aura toujours des facteurs, parfois indépendants de notre volonté, qui pourront faire échouer nos applications.

Dans cet article, nous allons construire une solution native Python qui étend la librairie de logging standard pour envoyer des alertes d'échec d'application par le biais des SMS programmables et/ou SendGrid Email avec les APIs de Twilio.

Prérequis

Nous utiliserons Python 3.9, mais toute version ultérieure à Python 3.6 devrait tout aussi bien fonctionner. Vous pouvez télécharger Python depuis le site Web officiel python.org.

Comme notre code utilisera les services Twilio et SendGrid pour envoyer des alertes d'erreur, vous aurez besoin d'un compte sur au moins un des deux services (idéalement les deux). Nous expliquons ci-dessous comment vous inscrire ou vous connecter, mais aussi comment collecter les données dont vous aurez besoin depuis chaque compte.

Compte Twilio

Connectez-vous à votre compte Twilio (ou inscrivez-vous gratuitement si vous n'avez pas de compte) et notez le SID de votre compte et le token d'autorisation :

SID du compte Twilio et jeton d'autorisation

Assurez-vous qu'un numéro de téléphone est bien répertorié dans Phone Numbers > Manage Numbers (Numéros de téléphone > Gérer les numéros) et que la messagerie est bien activée pour ce numéro. Si vous n'avez pas de numéro, cliquez sur le bouton Buy a number (Acheter un numéro) dans le coin supérieur droit de l'écran pour en obtenir un.

Acheter un numéro de téléphone Twilio

Compte SendGrid

Connectez-vous à votre compte SendGrid (ou inscrivez-vous si vous n'avez pas de compte) et rendez-vous dans Settings > API Keys (Paramètres > Clés API) dans le menu de gauche. Cliquez sur Create API Key (Créer une clé API) dans le coin supérieur droit. Donnez un nom à la clé et cliquez sur Create & View (Créer et afficher). Notez la clé car SendGrid ne vous l'affichera plus.

Configuration du projet

Créez un répertoire pour le projet :

mkdir twilio-alert
cd twilio-alert

Une bonne pratique consistant souvent à créer un environnement virtuel et c'est ce que nous allons faire :

python3 -m venv .env
source .env/bin/activate

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

.venv\Scripts\activate

Un préfixe (.env) devrait désormais être ajouté à votre invite de commande, ce qui confirmera que votre environnement virtuel est entièrement configuré.

Installez sur votre environnement virtuel les librairies suivantes :

pip install http-logging sendgrid twilio

Vous pouvez également choisir ou non d'épingler les librairies installées vers un fichier de dépendances locales :

pip freeze > requirements.txt

La librairie http-logging est compatible avec la librairie de logging native de la librairie standard Python. Elle nous permet de créer un back-end personnalisé en vue de la réception de messages d'erreur, lequel, dans notre cas, enverra les erreurs aux API Twilio et SendGrid. Le gestionnaire de logging asynchrone de cette librairie est similaire à la classe HTTP Handler de Python, mais au lieu de générer des demandes de blocage, il s'exécute dans un thread d'arrière-plan afin d'éviter de bloquer le programme principal. Il envoie également les journaux par lots, est capable de conserver un cache local dans SQLite et gère les nouvelles tentatives en cas de défaillance de l'API distante.

Variables d'environnement

Nous utiliserons des variables d'environnement afin de récupérer les clés API secrètes dont le logger HTTP asynchrone a besoin pour communiquer avec les back-ends Twilio et SendGrid.

Là encore, si vous souhaitez n'utiliser qu'un seul des deux services, ignorez les variables d'environnement associées à l'autre. Par exemple : si vous souhaitez uniquement utiliser les SMS, ignorez SENDGRID_SENDER_EMAIL, SENDGRID_API_KEY et ALERT_EMAIL.

export TWILIO_ACCOUNT_SID="XXXXX"
export TWILIO_AUTH_TOKEN="XXXXX"
export TWILIO_SENDER_NUMBER="+1234567890"
export SENDGRID_SENDER_EMAIL="sent@from.com"
export SENDGRID_API_KEY="XXXXX"
export ALERT_PHONE="1234567890"
export ALERT_EMAIL="hello@world.com"

Linux et macOS devraient prendre en charge la commande d'exportation. Sous Windows, si vous utilisez une invite de commande, vous devriez utiliser set au lieu d'export. Dans la console PowerShell, utilisez $Env comme suit :

$Env: TWILIO_ACCOUNT_SID="XXXXX"
$Env: TWILIO_AUTH_TOKEN="XXXXX"
$Env: TWILIO_SENDER_NUMBER="+1234567890"
$Env: SENDGRID_SENDER_EMAIL="sent@from.com"
$Env: SENDGRID_API_KEY="XXXXX"
$Env: ALERT_PHONE="1234567890"
$Env: ALERT_EMAIL="hello@world.com"

Lorsque vous définissez des numéros de téléphone, veillez à saisir le numéro complet au format E.164, qui inclut le signe plus et le code pays.

Transport Twilio HTTP

Dans cette section, nous allons écrire une classe de transport HTTP personnalisée qui sera chargée de communiquer avec les API Twilio et SendGrid.

Créez un nouveau fichier Python appelé logging_twilio.py. Dans ce fichier, notre classe personnalisée héritera de http_logging.transport.AsyncHttpTransport. Importez toutes les librairies requises et déclarez une classe personnalisée comme indiqué ci-dessous :

from typing import List, Optional

from http_logging.transport import AsyncHttpTransport
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
from twilio.rest import Client as TwilioClient

class TwilioHttpTransport(AsyncHttpTransport):
    pass

Nous utiliserons certains attributs personnalisés qui ne font pas partie de l'implémentation de la classe parent. Pour cela, nous devons remplacer la méthode constructeur dans TwilioHttpTransport :

class TwilioHttpTransport(AsyncHttpTransport):

    def __init__(
        self,
        logger_name: str,
        twilio_account_sid: Optional[str] = None,
        twilio_auth_token: Optional[str] = None,
        twilio_sender_number: Optional[str] = None,
        sendgrid_sender_email: Optional[str] = None,
        sendgrid_api_key: Optional[str] = None,
        alert_phone: Optional[str] = None,
        alert_email: Optional[List[str]] = None,
        *args,
        **kwargs,
    ) -> None:
        self.logger_name = logger_name
        self.alert_context = f'Alert from logger: {logger_name}'

        self.twilio_account_sid = twilio_account_sid
        self.twilio_auth_token = twilio_auth_token
        self.twilio_sender_number = twilio_sender_number

        self.sendgrid_sender_email = sendgrid_sender_email
        self.sendgrid_api_key = sendgrid_api_key

        self.alert_phone = alert_phone
        self.alert_email = alert_email

        super().__init__(*args, **kwargs)

Pour personnaliser le comportement de la classe, nous devons surcharger la méthode send de son parent. Il nous faut pour cela un argument events, qui désigne une liste des événements consignés par nos applis Python.

class TwilioHttpTransport(AsyncHttpTransport):

    # ...

    def send(self, events: List[bytes], **kwargs) -> None:
        batches = list(self._HttpTransport__batches(events))

        if self.alert_phone:
            self.send_sms_alert(batches=batches)

        if self.alert_email:
            self.send_email_alert(batches=batches)

Implémentons maintenant les méthodes send_sms_alert et send_mail_alert, lesquelles utiliseront respectivement les API Twilio et SendGrid :

    def send_sms_alert(self, batches: List[dict]) -> None:
        twilio_client = TwilioClient(
            username=self.twilio_account_sid,
            password=self.twilio_auth_token,
        )

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

        twilio_client.messages.create(
            body=f'[{self.alert_context}] {sms_logs}',
            from_=self.twilio_sender_number,
            to=self.alert_phone,
        )

    def send_email_alert(self, batches: List[dict]) -> None:
        msg = '<hr>'.join([
            self.build_log_html(log)
            for batch in batches
            for log in batch
        ])

        message = Mail(
            from_email=self.sendgrid_sender_email,
            to_emails=self.alert_email,
            subject=self.alert_context,
            html_content=msg,
        )

        sg = SendGridAPIClient(self.sendgrid_api_key)
        response = sg.send(message)

    def build_log_html(self, log):
        return '<br>'.join([
            f'<b>{key}:</b> {val}'
            for key, val in log.items()
        ])

L'acronyme SMS signifiant Short Message Service (« service de messages courts »). Nous voulons évidemment que nos alertes restent courtes sur ce canal. Par ailleurs, il s'agit uniquement ici de signaler une alerte, et non de fournir tous les détails du problème. C'est la raison pour laquelle nous concaténons plusieurs événements (le cas échéant) en un seul message, dans lequel nous ne fournissons que le type d'erreur et le résumé.

Dans le canal e-mail, nous développons davantage et incluons la trace de la pile d'erreurs ainsi que des informations contextuelles qui pourraient aider à identifier la cause première et à résoudre le problème.

Le nom du logger est envoyé dans les deux canaux. De cette façon, nous pouvons identifier de quelle application proviennent les alertes.

Maintenant que nous disposons d'une classe de transport HTTP intégrée à Twilio et SendGrid, la prochaine étape concerne la logique permettant d'instancier un objet Logger basé sur la nouvelle fonction TwilioHttpTransport.

Gestionnaire HTTP Twilio

La classe de transport HTTP est prête, mais elle nécessite une classe de gestionnaire pour fonctionner correctement avec les mécanismes de logging natifs de Python.  Il devrait s'agir d'une instance de la classe http_logging.AsyncHttpHandler.

Créez un nouveau fichier appelé sample_app.py et saisissez le code suivant pour instancier les classes de transport et de gestionnaire HTTP Twilio :

import logging
import os

from http_logging.handler import AsyncHttpHandler

from logging_twilio import TwilioHttpTransport

APP_NAME = 'MyApp'

transport_class = TwilioHttpTransport(
    logger_name=APP_NAME,
    twilio_account_sid=os.environ.get('TWILIO_ACCOUNT_SID'),
    twilio_auth_token=os.environ.get('TWILIO_AUTH_TOKEN'),
    twilio_sender_number=os.environ.get('TWILIO_SENDER_NUMBER'),
    sendgrid_api_key=os.environ.get('SENDGRID_API_KEY'),
    sendgrid_sender_email=os.environ.get('SENDGRID_SENDER_EMAIL'),
    alert_phone=os.environ.get('ALERT_PHONE'),
    alert_email=os.environ.get('ALERT_EMAIL'),
)

twilio_handler = AsyncHttpHandler(transport_class=transport_class)

Après cela, nous instancions un objet logging.logger et ajoutons le twilio_handler en tant que son gestionnaire :

logger = logging.getLogger(APP_NAME)
logger.addHandler(twilio_handler)

Si vous disposez déjà d'un objet logger issu d'un autre package (par exemple app.logger issu du framework Flask), ignorez la première ligne ci-dessus et utilisez simplement logger.addHandler(twilio_handler), où logger est l'objet Logger que vous avez déjà dans votre application.

Notez que les secrets, les numéros de téléphone et les adresses e-mail sont récupérés à partir des variables d'environnement que nous avons définies au début de ce tutoriel. Cela offre une grande flexibilité dans l'éventualité où nous voudrions utiliser ce code dans plusieurs projets. Cela évite également de devoir coder les secrets API en dur, ce qui n'est pas une bonne idée.

Gestionnaires multiples

Le package de logging Python est très puissant. La classe logging.logger est suffisamment flexible pour être étendue avec des gestionnaires multiples.

Comme expliqué ci-dessus, la classe TwilioHttpTransport enverra des informations minimales sur les journaux en raison des limitations des SMS. Néanmoins, en cas d'erreur nécessitant un débogage supplémentaire, nous voudrions certainement récupérer l'ensemble de la trace de la pile, les informations sur la ligne de code ayant entraîné l'échec, les horodatages exacts, etc. Même si tout cela sera envoyé par e-mail, il est également conseillé de conserver ces informations dans les journaux locaux.

Pour ce faire, nous pouvons utiliser le Logger.addHandler et ajouter un (ou plusieurs) gestionnaire(s) à l'objet Logger.

Par exemple, pour envoyer des journaux non seulement vers notre téléphone et notre adresse e-mail, mais aussi vers la console, nous pourrons utiliser le Logging.StreamHandler, comme indiqué ci-dessous :

logger = logging.getLogger(APP_NAME)
logger.addHandler(twilio_handler)
logger.addHandler(logging.StreamHandler())

Tout ce qui sera consigné à l'aide de l'objet logger ci-dessus sera imprimé vers la console et envoyé sur notre téléphone et notre adresse e-mail via les API Twilio et SendGrid.

Un Logging.FileHandler peut être utilisé pour stocker les journaux dans le système de fichiers local si cela a du sens. Vous pourriez également utiliser une fois de plus le même http_logging.AsyncHttpHandler, mais dans ce cas, en envoyant les journaux vers un hôte back-end différent en plus de Twilio et SendGrid.

Test avec une application d’exemple

Pour tester notre nouveau système d'alerte d'erreur asynchrone, ajoutez les lignes suivantes à la fin du script sample_app.py afin de déclencher des messages de log et une erreur intentionnelle :

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)

Dans la console, exécutez ce script avec :

python sample_app.py

La sortie suivante devrait être imprimée vers la console :

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

Si tout est correctement configuré, vous devriez recevoir des SMS et des e-mails similaires aux captures d'écran ci-dessous. Si vous n'avez pas configuré les options pour l'un des services (Twilio SMS ou SendGrid Email), le code d'alerte d'erreur l'ignorera.

Exemple d'alerte SMS

Exemple d'alerte e-mail

Notez que le message de débogage 'Debugging...' n'a pas été imprimé vers la console ni concaténé dans les SMS et les e-mails. Cela est dû au fait que le niveau de logging par défaut dans la librairie de logging Python est défini sur WARNING. Le niveau DEBUG est inférieur au niveau WARNING, et est par conséquent ignoré.

Si vous souhaitez que les messages de niveau DEBUG soient également capturés, définissez le niveau en conséquence comme indiqué ci-dessous :

logger.setLevel(logging.DEBUG)

Bien que notre logger repose sur un gestionnaire personnalisé (http_logging.AsyncHttpHandler) et une classe de transport personnalisée (logging_twilio.TwilioHttpTransport), il se comporte comme n'importe quel autre objet Logger Python.

Il est ainsi entièrement compatible avec tous les Python que vous auriez actuellement, dans l'éventualité où vous souhaiteriez intégrer le mécanisme d'alerte par SMS et e-mail que nous venons de développer à l'échelle de l'ensemble de votre stack et dans tout projet futur.

Conclusion

Ainsi s'achève notre démonstration sur notre outil d'alerte Python simple mais puissant basé sur les SMS et les e-mails, et propulsé par les API Twilio et SendGrid. Celui-ci étant basé sur la fonction de logging Python native, nous pouvons utiliser l'API Python à laquelle nous sommes habitués et l'intégrer facilement à n'importe quel projet. L'un de ses avantages est qu'il n'entraîne aucun frais fixe, la seule chose qui nous est facturée étant l'envoi de SMS ou d'e-mails.

Un cache des journaux est stocké localement par la librairie http-logging. Dans l'éventualité où les API externes ou l'opérateur de téléphonie mobile subissaient une interruption de service ou une instabilité du réseau, notre logger réessaierait d'envoyer les alertes par SMS et par e-mail ultérieurement.

Développeur de logiciels back-end, je suis également père de deux enfants incroyables qui ne me laissent pas dormir. Je peux ainsi m'amuser toute la nuit à connecter des API. Restez à l'affût de mes autres projets matinaux sur mon profil Github. Pour un contact direct et des liens vers mes autres réseaux sociaux, voir : byrro.dev.