Le module de journalisation logging Python : trouver les erreurs de script

Python est un langage de programmation orienté objet qui est très utilisé. Beaucoup d’utilisateurs apprécient Python parce qu’il permet, lorsqu’il est utilisé comme langage de script, de créer des programmes plus vite qu’avec des langages compilés comme Java. Par rapport aux langages de programmation procéduraux classiques comme Perl, Python présente l'avantage d’être bien lisible et facile à modifier. Que ce soit pour de l’intelligence artificielle, une interface graphique d’utilisateur ou de l’administration de système, Python peut être utilisé dans le cadre d’un grand nombre de tâches. Mais plus un langage de programmation est fréquemment utilisé, plus il est important qu’il lui soit associée une bonne culture de l'erreur. La journalisation des erreurs devrait avoir lieu depuis la première phase de développement jusqu'à la mise à disposition de l’utilisateur final.

Dans la bibliothèque Python, il existe un module pratique de journalisation. Appliqué à un simple débogage ou à une journalisation centralisée à partir de différents serveurs, ce module logging peut considérablement faciliter le travail des développeurs et opérateurs.

Qu’est-ce que la journalisation ?

La journalisation est un protocole destiné à recueillir dans un fichier toutes les informations importantes sur les incidents de fonctionnement. En fonction de ce qui doit être examiné, la journalisation peut concerner uniquement certains types d’actions ou d’incidents ou bien concerner toutes les actions.

Quand on apprend un nouveau langage de programmation, on est inévitablement amené à faire des erreurs. Bien que Python présente des structures familières pour un programmateur connaissant déjà le C++ ou Java (comme la forme des loops), chaque langage possède, malgré tout, ses particularités. Par exemple, Python délimite le code par un système d’indentation. Si on oublie une espace en cours de programmation, l'application la plus simple ne pourra pas fonctionner. Un protocole d’erreur invite les développeurs inexpérimentés à déboguer la ligne correspondante en affichant le message d’erreur « unexpected Indentation ». Dans une telle situation, la journalisation Python enregistre les erreurs de script simples et génère un message signalant l’erreur. Mais ce n’est pas tout. Les développeurs utilisent la journalisation en programmation pour des applications complètement différentes :

  • Débogage : tout le code source est examiné à la recherche d’erreurs de façon à garantir que le programme fini fonctionnera sans problème.
  • Recherche et correction des failles de sécurité : les risques de sécurité sont identifiés de manière anticipée et traités.
  • Informatique légale : l’informatique légale permet de déterminer la cause d’incidents critiques, comme des attaques de pirates informatiques, en utilisant les fichiers journaux.
  • Audit informatique : un audit informatique établit si la sécurité et l’intégrité des données est garantie ; il compare les objectifs de l'entreprise concernée à ses structures informatiques existantes pour évaluer leur compatibilité et analyse l'efficacité des programmes et des systèmes d'exploitation en place.
  • Comparaison des différentes versions des journaux : dans la mesure où chaque itération génère son propre fichier journal, il est possible de procéder à des comparaisons.

Beaucoup de données peuvent être déduites de la journalisation, surtout si on écrit une application complexe en Python. Les développeurs recueillent ces données grâce à la journalisation Python sur fichier (c’est-à-dire à un fichier journal fourni par le module logging Python et enrichi d’informations de journalisation via un pointeur). À cet effet, il est important que le fichier journal fonctionne en asynchronisation. Autrement, la journalisation en Python risque de bloquer l’exécution du script.

Analyse des erreurs avec le logging Python : les 5 niveaux de journalisation Python des fichiers

Certains développeurs utilisent la fonction print pour vérifier si leur script contient des erreurs. Ils se concentrent pour cela sur les passages où ils craignent la présence d’une erreur. D'autres utilisent print dans leur script à titre préventif. Cette méthode présente un problème : cela les oblige à vérifier a posteriori l’ensemble du code pour placer des commentaires sur les commandes concernées ou pour résoudre les problèmes. Dans le cas contraire, le fichier texte de sortie risque d'apparaître lorsque l’utilisateur lance le programme. En outre, cela rend le code source assez peu lisible.

À l'aide d’une simple journalisation, on peut à la fois s’épargner ce travail supplémentaire et mettre en œuvre une solution d'analyse des erreurs plus élégante. La journalisation Python distingue cinq différents degrés de gravité pour les erreurs de code : level of Severity (« degré de sévérité ») Si on souhaite définir son propre filtrage, on peut le faire très simplement. Le module de journalisation Python de Vinay Sajip intégré propose une classification des degrés de gravité qui nous semble judicieuse :

Nom du niveau de journalisation Python Utilisation Possibilité d’édition de message
Debug Diagnostic des problèmes, très détaillé Indentation inattendue à la ligne XY
Info Confirme que le système fonctionne correctement La fonction 1*1 est exécutée
Warning L'application travaille correctement d’une manière générale, mais une situation inattendue s’est produite, ou un problème à venir est signalé Espace disque faible
Error Une fonction n'a pas pu être effectuée car un problème s’est produit Une erreur s’est produite et l'action a été annulée
Critical Un problème grave s’est produit. Il faudra peut-être arrêter l'application. Erreur fatale : le programme ne peut plus se connecter à ce service et doit être arrêté

Les différents niveaux présentent les informations sur les incidents par ordre croissant de gravité. Les niveaux de journalisation Python sont des fonctions statiques. En programmation orientée objet, il s'agit des contenus d’une classe. Pour chaque instance de la classe à l’intérieur d’un objet, les fonctions statiques sont toujours identiques. Elles ne changent pas et sont également disponibles lorsqu’aucune instance n’est appelée. Par exemple, Error communique dans chaque instance un message d’erreur. Si cette instance est appelée dans le même objet d’exécution, le message d'erreur associé reste le même. Pour les autres actions, un autre message d’erreur peut être déterminé.

Debug est le niveau le plus faible ; c’est pourquoi des informations de moindre priorité y sont également émises. Cela ne signifie cependant pas que le degré de gravité d’une erreur est plus élevé qu’au niveau Critical. Debug comprend tous les autres niveaux et émet ainsi toutes les communications jusqu'au niveau Critical Error.

Le module de journalisation logging de Python

Le module de journalisation Python fait partie de la bibliothèque Python. L'interface de journalisation assure que les interactions avec le reste du code source sont fluides ; en outre, elle reste en permanence opérationnelle. La journalisation simple et l’envoi des informations sur un fichier sont rapidement inscrits dans le code existant à l'aide du pointeur. Le module logging dispose d'autres fonctions grâce auxquelles vous pouvez adapter cet outil. Voici les principaux éléments du module de journalisation :

  • Logger
  • Handler
  • Filter
  • Formatter

Les instances sont récapitulées dans l'instance LogRecord et s'échangent à l’intérieur de l’instance.

Logger

Logger enregistre les actions pendant que le programme fonctionne. Il n'apparaît pas directement en tant qu'instance : il faut l'appeler avec la fonction logging.getLogger(Loggername). Assignez un nom au Logger, par exemple pour figurer les hiérarchies de façon structurée. En Python, on distingue sous-paquets et paquets en les séparant par des points (.). Le paquet log peut posséder des sous-paquets log.bam ou log.bar.loco. De la même manière, Logger fonctionne de telle sorte que l’objet « log » contient les informations concernant ses sous-paquets « log.bam » et « log.bar.loco ».

Handler

Handler récupère les informations de Logger et les transfère ailleurs. Handler est une classe de base qui détermine la façon dont l’interface de l’instance Handler agit. La classe Handler vous permet de déterminer la cible. StreamHandler envoie les informations dans un flux de sortie ; FileHandler les envoie sur un fichier. Pour un programme donné, il est possible d’utiliser plusieurs Handler qui envoient les informations du même Logger. C’est utile si on souhaite par exemple lire des informations de débogage dans la console ou des messages d'erreur dans un fichier séparé.

À l'aide de la méthode setLevel (), vous pouvez définir le degré de gravité le plus faible qu’une information de journal pourra noter et renvoyer à Handler. Plutôt que logger.setLevel (qui détermine le niveau de journalisation Python), la méthode utilisée s'appelle alors [handlername].setLevel (cf. présentation de code ligne 5 : fh.setLevel).

Formatter

Contrairement à Handler, l’objet de formatage Formatter s’utilise directement en tant qu’instance dans le code d'application. Ces instances permettent de déterminer le format de journalisation python où votre notification sera émise dans le fichier journal. Si vous n’effectuez pas de formatage, seule l'information spécifique à Logger apparaît. La fonction suivante vous permet d'appeler Formatter et de déterminer le format des informations et de la date :

logging.Formatter.__init__(fmt=[formatmessage], datefmt=[formatdate])
#ou bien :
logging.Formatter.__init__(fmt=None, datefmt=None)

Si vous ne précisez pas de format pour la date dans l'attribut, Formatter fournit le format et la date à l’américaine : « année-mois-jour heures:minutes:secondes ».

Filter

Filter permet de déterminer de façon encore plus précise les informations livrées. Définissez d'abord Filter, puis ajoutez-le dans le Handler correspondant ou dans Logger par la méthode addFilter(). Si la valeur de Filter, déterminée sur la base des propriétés de l’information, est False, l’information n’est pas transmise. Utilisez la fonction logging.Filter(name=fh), où l'attribut fh fait référence à un nom de Logger, pour pouvoir autoriser les fichiers journaux d’un Logger donné et bloquer tous les autres Loggers.

Le module de journalisation Python avec un exemple

Python met à la disposition des développeurs l'outil graphique Turtle, pour tester des commandes de base. Dans l’exemple suivant, le testeur utilise Turtle. L'outil graphique doit courir tout droit sur un arrière-plan vert, tourner vers la gauche, continuer tout droit puis décrire un cercle. Dans cet exemple, nous intégrons les commandes de journalisation Python Info et Error :

# -*- coding: UTF-8 -*-
import turtle
import logging
turtle.bgcolor("green")
turtle.fd(30)
turtle.lt(90)
turtle.fd(50)
logging.info('Ça marche, ou plutôt ça court.')
turtle.circle(50)
logging.error("Oups, ça n’a pas marché comme prévu.")

La capture d'écran ci-dessus montre à quoi ressemble le résultat. Le module Turtle (fenêtre de gauche) a accepté les commandes et fonctionne comme prévu. Dans la fenêtre de droite, à côté de la commande Turtle, le code comprend aussi les commandes de journalisation de niveau INFO et ERROR. Le format de sortie classique d’un message journal est le suivant : [Niveau de gravité]: [Origine du message]:[Information du message]

Toutefois, dans l’exemple, la console (Console 1/A) n'indique que le message de journalisation Python ERROR : Error:root:Oups, ça n’a pas marché comme prévu.

C’est dû au fait que la configuration de base du module logging de Python est réglée sur WARNING. Le module ignore toute indication plus détaillée tant que ces réglages ne sont pas modifiés.

Modifier le niveau de journalisation Python

Les commandes suivantes vous permettent de régler la configuration sur le niveau DEBUG :

import logging
logging.basicConfig(level=logging.DEBUG)

Dans l’image ci-dessus, la console montre la journalisation pour chaque nouvel appel. Si le programme est arrêté, la console supprime tous les enregistrements. Pour garder l'œil sur vos données d'inscription, vous devriez utiliser un fichier journal. Cette pratique porte en anglais le nom de Logging to File (« journalisation sur fichier »).

Consigner la journalisation Python sur un fichier

La journalisation Python sur fichier fonctionne de deux manières. Soit vous fournissez un fichier journal précisant la configuration de base, soit vous utilisez Handler. Si vous ne déterminez pas de cible, la journalisation Python dépose temporairement les informations dans la console.

Créez un fichier pour votre journalisation Python comme suit :

import logging
logging.basicConfig( level=logging.DEBUG, filename='example.log')

FileHandler est une instance de la classe Logging. Elle agit en même temps que l’instance logging. Elle sert à déterminer où chaque type de fichier de journalisation sera envoyé et sous quel format. En plus de FileHandler, il existe dans le module de journalisation de la bibliothèque Python d'autres Handlers de journalisation, comme StreamHandler et NullHandler. Toutefois, pour pouvoir poursuivre l'analyse des fichiers de journalisation, il est recommandé d’utiliser un fichier journal.

Créez donc un FileHandler qui consigne les messages Debug dans un fichier :

Dans l’image ci-dessus, la commande logging.getLogger() appelle le module journalisation Python. « fh » est défini comme FileHandler possédant l’attribut « debug.log ». « fh » crée ainsi le fichier journal « debug.log » et vous transmet les informations de journal. La méthode addHandler() assigne au Logger le Handler correspondant. Vous pouvez nommer le fichier journal comme vous voulez.

Vous pouvez utiliser les fonctions suivantes pour tester tout cela vous-même :

import logging
logger = logging.getLogger('Journal_exemple')
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler('debug.log')
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
logger.debug('Information-Debug')
logger.info('Message info')
logger.warning('avertissement')
logger.error(message-erreur)
logger.critical('erreur grave')

Si le fichier journal fourni avec la journalisation Python sur fichier livre des informations utiles pour certaines tâches, il arrive que des communications simples ne soient pas suffisantes. Un fichier d’horodatage (« Timestamp ») et le nom du Logger aident à mieux classer les indications. L’image suivante donne un exemple de la façon dont vous pouvez déterminer le format grâce à des attributs Formatter. Dans la fenêtre de Notepad debug.log, le texte indique les informations de journal en précisant la date, l’heure, le nom de Logger, le niveau de journal et l’information.

Voici à nouveau le code pour vous permettre de procéder à des tests :

import logging
logger = logging.getLogger('Journal_exemple')
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler('debug.log')
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)
logger.debug('Information Debug')
logger.info('Information Info')
logger.warning('Avertissement')
logger.error('Message d’erreur')
logger.critical('Erreur grave')

En résumé

La journalisation Python avec le module logging est un outil pratique de prévention des erreurs, de contrôle après piratage ou tout simplement d’analyse. Alors que d'autres langages de programmation ajoutent la journalisation a posteriori, le module de journalisation de Python est disponible dans la bibliothèque standard. Si vous insérez la méthode dans votre code, les informations de journal seront créées à différents niveaux : dans des fichiers et sur la console. Le formatage et Filter, tout comme Handler, permettent une configuration qui répond aux besoins de l’utilisateur. Dans ce contexte, pour vous simplifier le travail sur les fichiers journaux sous Python, veillez absolument à attribuer rapidement des noms à votre Logger et à ses enfants.