Rediriger des logs de Python vers un canal Teams
- 2021-04-21
- Publié par : Christophe DELEUZE
- Catégorie : Python
Quand on a besoin d’écrire des logs en Python, la librairie incontournable à utiliser est logging. C’est la librairie qui répond à la quasi-totalité des besoins que nous pourrions avoir en termes de journalisation : écriture dans un fichier, formatage des logs, envois des entrées sur un serveur syslog, à une URL (via POST ou GET), etc.
Dans cet article, je vous propose de découvrir ensemble cette librairie indispensable. Puis en complément, nous allons voir comment afficher une partie des logs dans un canal Microsoft Teams pour faciliter le monitoring de votre application.
De print() à logging
L’usage de la fonction print() est très limité. Dès que vous allez avoir besoin de générer des logs pour faire de la journalisation, vous allez vite devoir remplacer cette fonction par autre chose. Par exemple, quelque chose qui s’occupera d’écrire le contenu de vos print() dans un fichier de logs.
Heureusement pour nous, la librairie standard logging existe et répond à tous nos besoins.
Cette librairie fonctionne avec un objet Logger (Enregistreur). Le Logger expose l’interface que le code de votre application va utiliser pour générer des logs. Comme les besoins en journalisation ne sont pas les mêmes pendant la phase de développement et en production, cette librairie permet d’adapter la journalisation à l’aide de niveaux de messages. En effet, Le Logger a comme propriété de regarder si les niveaux de logs que vous avez définis sont suffisamment élevés pour continuer avant de passer les messages à des handlers.
Un handler (Gestionnaire) est une routine / fonction / méthode spécialisée dans un certain type de données ou axée sur certaines tâches spéciales. Dans la librairie logging, la tâche des handlers est d’envoyé les entrées créées par les loggers vers les destinations voulues.
Chaque handler possède un objet formatter dont le rôle est de formater la nouvelle entrée du log. Le formateur par défaut écrit juste le message, mais on peut lui demander de l’enrichir avec des informations complémentaires tel que : le niveau du message, un timestamp, le nom du logger, ou d’autres éléments personnalisés.
Résumé en schéma du fonctionnement :
logging en pratique
Pour commencer, nous importons la librairie logging :
import logging
Puis, nous créons un objet logger à l’aide de la méthode .getLogger() et nous initialisons son niveau de message avec la méthode .setLevel() :
# Create logger object
logger = logging.getLogger()
# Set log level to DEBUG to write everything
LOG_LEVEL = getattr(logging, os.environ.get('LOG_LEVEL', 'DEBUG').upper())
logger.setLevel(LOG_LEVEL)
Vous noterez que j’ai utilisé, pour la gestion des niveaux de messages, une variable d’environnement nommée LOG_LEVEL. Celle-ci est récupérée à l’aide de os.environ() et est combinée avec getattr() pour récupérer automatiquement la valeur par défaut des variables définies dans la librairie logging. Donc, durant la phase de développement, il faudra veiller à ce que le niveau de message définit dans la variable d’environnement soit sûr : "DEBUG". Tandis qu’en production, il faudra être vigilant à ce que la valeur de la variable d’environnement soit a minima sur "WARNING" ou "INFO".
Après cette digression sur les niveaux de message, nous pouvons passer à l’étape suivante qui consiste à créer un formateur qui servira pour les handlers :
formatter = logging.Formatter('%(levelname)s - %(asctime)s - %(name)s - %(message)s')
Enfin, on ajoute au logger deux handlers. Le premier pour sauvegarder des logs dans un fichier et le second pour afficher les logs en console.
Après les avoir créés, il faut aussi initialiser leur niveau de message et leur préciser le formateur précédemment définit :
# Create a file handler on 'append' mode
file_handler = logging.FileHandler('mon_app.log', 'a')
# Set log level to DEBUG to write everything
file_handler.setLevel(LOG_LEVEL)
# Set generic formatter
file_handler.setFormatter(formatter)
# Add handler to logger
logger.addHandler(file_handler)
# Create a stream handler
stream_handler = logging.StreamHandler()
stream_handler.setLevel(LOG_LEVEL)
logger.addHandler(stream_handler)
Pour terminer, on peut maintenant générer des logs en appelant les méthodes suivantes au cas par cas :
logger.debug(f"Message de debug (valeur {logging.DEBUG}) : affiche tout ce qui est utile au debug mais pas en production")
logger.info(f"Message informatif (valeur {logging.INFO}) : informe de la marche du programme")
logger.warning(f"Message de warning (valeur {logging.WARNING}) : avertit que quelque chose mérite votre attention")
logger.error(f"Message d'erreur (valeur {logging.ERROR}) : prévient qu'une opération a généré une erreur")
logger.critical(f"Message critique (valeur {logging.CRITICAL}) : prévient que la stabilité globale du programme est compromise")
Le résultat attendu ressemblera à cela :
DEBUG - 2021-04-20 18:07:13,428 - root - Message de debug (valeur 10) : affiche tout ce qui est utile au debug mais pas en production
INFO - 2021-04-20 18:07:13,506 - root - Message informatif (valeur 20) : informe de la marche du programme
WARNING - 2021-04-20 18:07:13,567 - root - Message de warning (valeur 30) : avertit que quelque chose mérite votre attention
ERROR - 2021-04-20 18:07:13,641 - root - Message d'erreur (valeur 40) : prévient qu'une opération a généré une erreur
CRITICAL - 2021-04-20 18:07:13,731 - root - Message critique (valeur 50) : prévient que la stabilité globale du programme est compromise
Le fonctionnement du niveau de message est simple. Si le niveau du message qui est envoyé au logger est équivalent ou supérieur au niveau de message définit, alors le message est traité.
Faites l’essai en choisissant un niveau INFO au lieu de DEBUG et vous verrez.
Pour terminer, il existe plein de handlers différent et si les deux utilisés dans cet article ne répondent pas à vos besoins, n’hésitez pas à aller faire un tour sur la documentation : Documentation logging.handers.
Aller plus loin avec logging
Jusqu’ici, vous aurez noté que par défaut, le nom du logger est root (racine).
Cependant, rien ne vous oblige à utiliser ce nom, vous pouvez le changer en lui donnant un autre nom :
logger = logging.getLogger("MonPetitLogger")
Adapter le nom de vos logger est une pratique très pratique qui permet d’améliorer le suivi des logs. Par exemple, en donnant au logger le nom de la classe ou de la fonction dans lequel il est utilisé, vous saurez avec certitude qui a généré le message.
Pour rappel, on peut obtenir le nom d’une fonction à l’aide de l’attribut de classe __name__ :
logger = logging.getLogger(fonction.__name__)
Pour finir, il ne nous reste plus qu’à intégrer le tout dans une fonction dédiée qui facilitera la réutilisation du logger :
import os
import logging
LOG_LEVEL = getattr(logging, os.environ.get('LOG_LEVEL', 'DEBUG').upper())
def get_logger(name="root", log_level=LOG_LEVEL):
# Create logger object
logger = logging.getLogger()
# Don't redo the job each time
if not logger.handlers :
# Set log level
logger.setLevel(log_level)
# Create a generic formatter
formatter = logging.Formatter('%(levelname)s - %(asctime)s - %(name)s - %(message)s')
# Create a file handler on 'append' mode
file_handler = logging.FileHandler('mon_app.log', 'a')
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Create a stream handler
stream_handler = logging.StreamHandler()
stream_handler.setLevel(log_level)
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
return logger
logger = get_logger("test",teams_url = url)
logger.debug(f"Message de debug (valeur {logging.DEBUG}) : affiche tout ce qui est utile au debug mais pas en production")
logger.info(f"Message informatif (valeur {logging.INFO}) : informe de la marche du programme")
logger.warning(f"Message de warning (valeur {logging.WARNING}) : avertit que quelque chose mérite votre attention")
logger.error(f"Message d'erreur (valeur {logging.ERROR}) : prévient qu'une opération a généré une erreur")
logger.critical(f"Message critique (valeur {logging.CRITICAL}) : prévient que la stabilité globale du programme est compromise")
Attention, vous remarquerez que par défaut le paramètre log_level a pour valeur LOG_LEVEL. Ainsi, on se laisse l’opportunité d’avoir un niveau de message personnalisé qui n’utiliserait pas la variable d’environnement.
Envoyer des messages dans un canal Teams à partir d'un script Python
Maintenant que vous savez tout à propos de la journalisation, passons à Teams.
Avant tout, il vous faut créer puis sélectionner un canal teams. Une fois fait, il faut ouvrir le menu des connecteurs du canal (clic droit sur le canal) et rajouter un connecteur entrant de type webhook Entrant (Incoming Webhook).
Pour le configurer, il suffit de lui donner un nom. Enfin, on récupère l’URL générée par le connecteur et qui sera de la forme :
https://nom-de-domaine-de-lentreprise.webhook.office.com/webhookb2/df3cd52f-90e6-4a89-8454-26eb4fe29661@dfg544a7-3d63-4f23-9a29-fd8537395607/IncomingWebhook/5d0e0fff38a869cab93dbf6a9cf9b12c/25ee8178-2074-4658-a028-0bd53ad27cba
Enfin, on est prêt à envoyer des messages à partir de votre script Python vers Teams à l’aide de la librairie pymsteams.
Pour l’installer dans votre environnement dédié : pip install pymsteams
Maintenant, vous devriez pouvoir envoyer des messages :
import pymsteams
url = "https://nom-de-domaine-de-lentreprise.webhook.office.com/webhookb2/df3cd52f-90e6-4a89-8454-26eb4fe29661@dfg544a7-3d63-4f23-9a29-fd8537395607/IncomingWebhook/5d0e0fff38a869cab93dbf6a9cf9b12c/25ee8178-2074-4658-a028-0bd53ad27cba"
# Init teams connector
teams = pymsteams.connectorcard(url) if url else None
if teams :
teams.text("pymsteams est dans la place !")
teams.send()
Intégrer Teams à Logging
Comme nous savons créer des logger et envoyer des messages dans Teams, il ne nous reste plus qu’à faire le lien entre les deux.
C’est tout naturellement qu’on va donc créer un Handler.
Parmi les subtilités du Handler que nous allons créer, il faut particulièrement être vigilant avec la librairie pymsteams. En effet, celle-ci utilise aussi la librairie logging et envoi des messages au logger root. Si on autorise le logger root à envoyer un message à notre Handler, alors on engendra un comportement récursif infini.
Pour éviter cela, notre Handler doit simplement ignorer les messages du logger root. Au final, sacrifier root est un bon prix pour pouvoir envoyer des messages à Teams dans tous les autres cas.
Sans plus attendre, voici le code complet :
import os
import logging
import pymsteams
LOG_LEVEL = getattr(logging, os.environ.get('LOG_LEVEL', 'DEBUG').upper())
url = "https://nom-de-domaine-de-lentreprise.webhook.office.com/webhookb2/df3cd52f-90e6-4a89-8454-26eb4fe29661@dfg544a7-3d63-4f23-9a29-fd8537395607/IncomingWebhook/5d0e0fff38a869cab93dbf6a9cf9b12c/25ee8178-2074-4658-a028-0bd53ad27cba"
class TeamsHandler(logging.Handler):
def __init__(self, logger_name, teams_url= None):
# run the regular Handler __init__
logging.Handler.__init__(self)
#init Teams and logger name
self.teams = pymsteams.connectorcard(teams_url) if teams_url else None
self.logger_name = logger_name
def emit(self, record):
if self.logger_name :
msg = self.format(record)
self.teams.text(msg)
self.teams.send()
def get_logger(name=None, log_level=LOG_LEVEL, teams_url = None):
# Create logger object
logger = logging.getLogger(name)
# Don't redo the job each time
if not logger.handlers :
# Set log level
logger.setLevel(log_level)
# Create a generic formatter
formatter = logging.Formatter('%(levelname)s - %(asctime)s - %(name)s - %(message)s')
# Create a file handler on 'append' mode
file_handler = logging.FileHandler('mon_app.log', 'a')
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Create a stream handler
stream_handler = logging.StreamHandler()
stream_handler.setLevel(log_level)
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
# Create a teams handler
teams_handler = TeamsHandler(name, teams_url=teams_url)
teams_handler.setLevel(log_level)
teams_handler.setFormatter(formatter)
logger.addHandler(teams_handler)
return logger
logger = get_logger("test",teams_url = url)
logger.debug(f"Message de debug (valeur {logging.DEBUG}) : affiche tout ce qui est utile au debug mais pas en production")
logger.info(f"Message informatif (valeur {logging.INFO}) : informe de la marche du programme")
logger.warning(f"Message de warning (valeur {logging.WARNING}) : avertit que quelque chose mérite votre attention")
logger.error(f"Message d'erreur (valeur {logging.ERROR}) : prévient qu'une opération a généré une erreur")
logger.critical(f"Message critique (valeur {logging.CRITICAL}) : prévient que la stabilité globale du programme est compromise")
Le mot de la fin
Comme vous l’aurez remarqué, logger et envoyer des messages dans Teams est quelque chose de très simple.
Alors, pourquoi se priver de ne pas le faire quand cela peut vous apporter un peu de confort au quotidien ?