Aller au contenu

Comment tracer les informations de débogage dans le code ?

Mise à jour :

Le logging est un élément essentiel pour surveiller, déboguer et maintenir les applications en production. En Python, la bibliothèque de logging intégrée permet de créer et de gérer des journaux de manière flexible, facilitant ainsi la détection et la résolution des problèmes.

C’est quoi le logging ?

Le logging est le processus d’enregistrement des événements, des messages ou des erreurs générées par un programme lors de son exécution. Ces enregistrements, appelés logs, permettent de suivre le déroulement de l’application, de comprendre le comportement du code et de détecter les problèmes ou les anomalies. Contrairement aux simples impressions sur la console avec print(), le logging offre plus de contrôle et de flexibilité, comme la gestion des niveaux de gravité des messages, l’enregistrement dans des fichiers et la personnalisation du format des messages.

Les logs peuvent être utilisés pour :

  • Surveiller l’application : suivre les actions importantes qui s’y déroulent, comme le traitement de données ou la réussite d’une opération.
  • Déboguer : repérer et comprendre les erreurs en enregistrant les détails de leur apparition.
  • Analyser la performance : examiner la durée et la fréquence de certaines opérations pour améliorer l’efficacité.
  • Auditer : garder un historique des actions effectuées pour des raisons de conformité ou de sécurité.

Grâce à la bibliothèque de logging de Python, vous pouvez capturer et formater ces informations de manière organisée, les envoyer à plusieurs destinations (console, fichiers, etc.), et les gérer selon les besoins de votre application.

Configurer un logger basique

La bibliothèque de logging intégrée à Python est simple à utiliser pour créer un système de logs efficace. Pour démarrer, il est essentiel de comprendre comment configurer un logger de base qui permet d’enregistrer des messages de différentes gravités dans la console.

Utilisation de basicConfig

La méthode logging.basicConfig() est le point de départ pour la configuration rapide d’un logger. Elle permet de définir le niveau minimal de log, le format des messages et la destination de sortie. Voici comment l’utiliser :

import logging
# Configuration basique du logger
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Exemples de messages de log
logging.debug("Ceci est un message de débogage.")
logging.info("Ceci est un message d'information.")
logging.warning("Ceci est un message d'avertissement.")
logging.error("Ceci est un message d'erreur.")
logging.critical("Ceci est un message critique.")
  • Explication des paramètres :
  • level : définit le niveau minimal de gravité des messages à enregistrer. Par exemple, si level est défini sur logging.INFO, les messages de niveau DEBUG ne seront pas affichés. Les niveaux, par ordre croissant de gravité, sont :

    • DEBUG : détails de débogage (utilisés pour le diagnostic)
    • INFO : messages d’information généraux
    • WARNING : messages indiquant des problèmes potentiels
    • ERROR : erreurs qui affectent le fonctionnement
    • CRITICAL : erreurs graves nécessitant une intervention immédiate
  • format : définit la mise en forme des messages de log. Les éléments communs incluent :

    • %(asctime)s : date et heure du message
    • %(levelname)s : niveau de gravité du message
    • %(message)s : contenu du message
  • Exemple pratique :

Imaginez que vous souhaitez suivre le déroulement de votre programme et repérer des points spécifiques où des erreurs peuvent survenir. La configuration basique peut être suffisante pour surveiller les événements importants :

import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
def division(a, b):
logging.info(f"Début de la division de {a} par {b}")
try:
result = a / b
logging.info(f"Résultat : {result}")
except ZeroDivisionError:
logging.error("Tentative de division par zéro")
return None
return result
division(10, 2)
division(5, 0)

Dans cet exemple, un message est enregistré avant et après l’opération de division, et un message d’erreur est enregistré si une division par zéro est tentée.

Quand utiliser chaque niveau de logging

  • DEBUG : Utilisé principalement lors de la phase de développement pour obtenir des informations détaillées sur le déroulement du code. Utile pour vérifier des valeurs de variables, l’exécution de boucles, etc.
  • INFO : Utilisé pour consigner des étapes importantes du programme qui font partie du flux normal. Par exemple, le démarrage ou l’arrêt d’un service.
  • WARNING : Signale un problème potentiel qui ne nécessite pas encore d’action immédiate mais qui doit être surveillé.
  • ERROR : Employé pour capturer les erreurs qui empêchent une partie du programme de fonctionner correctement, comme des exceptions non gérées ou des échecs de traitement.
  • CRITICAL : Réservé aux erreurs qui nécessitent une réponse urgente, comme l’arrêt complet d’un service essentiel.

Configuration via un fichier de configuration

Configurer le logging directement dans le code Python est pratique, mais cela peut rendre la maintenance difficile lorsque les besoins en logs évoluent. Pour une approche plus flexible et modifiable, il est préférable d’utiliser un fichier de configuration. Cette méthode permet de changer la configuration des logs sans modifier le code source de l’application.

Formats de fichiers de configuration

Python prend en charge deux formats principaux pour configurer le logging :

  1. Fichier de configuration INI (format .ini).
  2. Fichier de configuration YAML (avec des bibliothèques tierces comme PyYAML).

Le format YAML est plus lisible et est souvent utilisé dans des applications plus modernes. Vous aurez besoin de la bibliothèque PyYAML pour le lire.

logging.yaml :

version: 1
formatters:
simple:
format: '%(levelname)s - %(message)s'
detailed:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: simple
stream: ext://sys.stdout
file:
class: logging.FileHandler
level: WARNING
formatter: detailed
filename: app.log
loggers:
simpleLogger:
level: INFO
handlers: [console, file]
propagate: no
root:
level: DEBUG
handlers: [console]

Pour utiliser ce fichier YAML, chargez-le comme suit :

import logging.config
import yaml
# Charger la configuration depuis le fichier logging.yaml
with open('logging.yaml', 'r') as file:
config = yaml.safe_load(file)
logging.config.dictConfig(config)
# Créer un logger
logger = logging.getLogger('simpleLogger')
# Exemple de messages de log
logger.info("Ceci est un message d'information")
logger.error("Ceci est un message d'erreur")

Personnaliser le format des logs

La personnalisation du format des logs permet d’afficher les informations pertinentes de manière structurée et lisible. Python offre la possibilité de modifier le format des messages de log pour inclure des détails tels que la date, le niveau de gravité, le nom du module, et bien plus encore. Un format bien pensé peut faciliter le débogage et le suivi de votre application.

La méthode logging.basicConfig() permet de spécifier un format personnalisé pour vos messages de log grâce au paramètre format. Voici un exemple de configuration simple :

import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logging.info("Ceci est un message d'information")

Voici quelques placeholders utiles pour personnaliser le format des logs :

  • %(asctime)s : Affiche la date et l’heure du message.
  • %(levelname)s : Affiche le niveau de gravité du message (DEBUG, INFO, etc.).
  • %(message)s : Contenu du message de log.
  • %(name)s : Nom du logger qui enregistre le message.
  • %(filename)s : Nom du fichier source du message.
  • %(funcName)s : Nom de la fonction où le message est enregistré.
  • %(lineno)d : Numéro de la ligne où le log est appelé.

Journaliser dans des fichiers

Enregistrer les logs dans des fichiers plutôt que de les afficher uniquement sur la console permet de conserver un historique des événements et facilite l’analyse à long terme. Cette approche est particulièrement utile pour les applications en production, où il est indispensable de pouvoir retracer les erreurs et comprendre le comportement de l’application après coup.

Pour enregistrer les logs dans un fichier, il suffit de configurer un Handler de type FileHandler ou d’utiliser basicConfig avec le paramètre filename. Voici un exemple simple de configuration :

import logging
# Configurer le logger pour écrire dans un fichier
logging.basicConfig(
filename='app.log', # Nom du fichier de log
level=logging.DEBUG, # Niveau de log
format='%(asctime)s - %(levelname)s - %(message)s' # Format du log
)
# Exemple de messages de log
logging.debug("Ceci est un message de débogage")
logging.info("Ceci est un message d'information")
logging.warning("Ceci est un avertissement")
logging.error("Ceci est une erreur")
logging.critical("Ceci est un message critique")

Avec cette configuration, tous les messages de log seront enregistrés dans le fichier app.log et non dans la console. Si le fichier n’existe pas, Python le créera automatiquement.

Mettre en place de la rotation de logs

Pour éviter que le fichier de log ne devienne trop volumineux et difficile à gérer, il est conseillé d’utiliser un système de rotation. Python propose la classe RotatingFileHandler dans le module logging.handlers, qui permet de limiter la taille des fichiers de log et de créer de nouveaux fichiers lorsqu’une taille maximale est atteinte.

import logging
from logging.handlers import RotatingFileHandler
# Configurer un RotatingFileHandler
handler = RotatingFileHandler(
'app_rotated.log', # Nom du fichier de log
maxBytes=5000, # Taille maximale du fichier en octets (ici, 5 Ko)
backupCount=3 # Nombre de fichiers de sauvegarde
)
# Configurer le format et le niveau
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
# Créer un logger et ajouter le handler
logger = logging.getLogger('mon_logger')
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)
# Exemple de messages de log
logger.debug("Log de débogage")
logger.info("Log d'information")
logger.warning("Log d'avertissement")

Avec cette configuration, un nouveau fichier de log sera créé lorsque la taille maximale de 5 Ko sera atteinte. Jusqu’à trois fichiers de sauvegarde seront conservés (app_rotated.log.1, app_rotated.log.2, etc.).

D’autres Handlers

En utilisant différents Handlers, vous pouvez diversifier les sorties de vos logs en les envoyant simultanément vers plusieurs destinations, comme des bases de données, ou même des services distants.

  1. SMTPHandler : Envoie les messages de log par e-mail via un serveur SMTP. Utile pour les notifications en cas d’erreurs critiques.

    from logging.handlers import SMTPHandler
    smtp_handler = SMTPHandler(
    mailhost=('smtp.example.com', 587),
    fromaddr='noreply@example.com',
    toaddrs=['admin@example.com'],
    subject='Erreur critique détectée',
    credentials=('user', 'password'),
    secure=()
    )
    smtp_handler.setLevel(logging.CRITICAL)
    smtp_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
    logger.addHandler(smtp_handler)
  2. HTTPHandler : Envoie les messages de log à un serveur web via une requête HTTP. Utile pour intégrer les logs dans un système de gestion centralisée.

    from logging.handlers import HTTPHandler
    http_handler = HTTPHandler(
    'http://example.com/log', # URL du serveur
    '/submit', # Chemin du script qui reçoit les logs
    method='POST'
    )
    http_handler.setLevel(logging.ERROR)
    logger.addHandler(http_handler)
  3. SysLogHandler : Permet d’envoyer des messages de log vers un serveur Syslog, couramment utilisé pour la collecte et la gestion centralisée des logs.

    from logging.handlers import SysLogHandler
    # Configurer le SysLogHandler pour envoyer les logs au démon Syslog local
    syslogHandler = logging.handlers.SysLogHandler(address=("<your remote server>",514))
    syslog_handler.setLevel(logging.INFO)
    syslog_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
    logger.addHandler(syslog_handler)

La documentation officielle de Python vous donne plus d’informations sur les Handlers disponibles.

Utilisation des filtres

Les filtres dans le module logging de Python permettent d’affiner la sélection des messages à enregistrer ou à afficher en fonction de critères spécifiques. Cela vous offre un contrôle précis sur quels messages de log doivent être traités par un Handler.

Pour créer un filtre personnalisé, il suffit de définir une classe qui hérite de logging.Filter et de redéfinir la méthode filter() :

import logging
class CustomFilter(logging.Filter):
def filter(self, record):
# Exemple : n'autoriser que les messages qui contiennent un mot spécifique
return 'spécial' in record.msg
# Configurer un logger
logger = logging.getLogger('filtered_logger')
logger.setLevel(logging.DEBUG)
# Créer un handler pour la console
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
# Appliquer le filtre au handler
custom_filter = CustomFilter()
console_handler.addFilter(custom_filter)
# Ajouter le handler au logger
logger.addHandler(console_handler)
# Exemple de messages de log
logger.info("Ceci est un message spécial")
logger.info("Ceci est un message ordinaire")

Dans cet exemple, seul le message contenant le mot “spécial” sera affiché, car il passe le filtre.

Outre les filtres personnalisés, Python offre la possibilité d’utiliser des filtres intégrés, comme logging.Filter() pour restreindre les logs à un module spécifique :

import logging
# Configurer un logger
logger = logging.getLogger('module_filtré')
logger.setLevel(logging.DEBUG)
# Créer un handler pour la console
console_handler = logging.StreamHandler()
# Créer un filtre pour le module
module_filter = logging.Filter('module_filtré')
console_handler.addFilter(module_filter)
# Ajouter le handler et le filtre au logger
logger.addHandler(console_handler)
# Exemple de log
logger.debug("Message du module_filtré")

Dans cet exemple, le Handler n’enregistrera que les messages provenant du module module_filtré.

Les filtres peuvent également être utilisés pour ajouter des métadonnées aux logs ou pour modifier légèrement les messages avant leur enregistrement :

class MetadataFilter(logging.Filter):
def filter(self, record):
record.user_id = 'Utilisateur123'
return True
logger = logging.getLogger('metadata_logger')
logger.setLevel(logging.DEBUG)
# Créer un handler et appliquer le filtre
console_handler = logging.StreamHandler()
metadata_filter = MetadataFilter()
console_handler.addFilter(metadata_filter)
formatter = logging.Formatter('%(asctime)s - %(user_id)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# Exemple de log
logger.info("Log avec métadonnées")

Ce filtre ajoute un champ user_id aux messages de log, ce qui est utile pour enrichir les journaux avec des détails contextuels sans modifier le code principal de l’application.

Logging pour les modules de l’application

Dans les applications complexes composées de plusieurs modules, il est important de configurer des loggers distincts pour chaque module afin de mieux suivre et diagnostiquer les événements. Chaque logger peut être configuré indépendamment pour capturer des informations spécifiques à un module, offrant ainsi une vue plus détaillée et organisée des logs.

Pour créer des loggers pour différents modules, il suffit d’utiliser logging.getLogger() avec le nom du module. Voici comment configurer des loggers dans une application composée de plusieurs modules.

fichier principal (main.py) :

import logging
import module_a
import module_b
# Configurer le logger principal
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# Créer un logger spécifique pour le module principal
logger = logging.getLogger('main')
logger.info("Démarrage de l'application")
module_a.fonction_a()
module_b.fonction_b()
logger.info("Fin de l'application")

fichier module_a.py :

import logging
# Créer un logger pour module_a
logger = logging.getLogger('module_a')
def fonction_a():
logger.debug("Message de débogage depuis module_a")
logger.info("Fonction A exécutée")
logger.warning("Avertissement dans module_a")

fichier module_b.py :

import logging
# Créer un logger pour module_b
logger = logging.getLogger('module_b')
def fonction_b():
logger.info("Fonction B en cours d'exécution")
logger.error("Erreur simulée dans module_b")

Ajuster les niveaux de log pour chaque module

Il est possible de définir des niveaux de log différents pour chaque logger afin de mieux contrôler la quantité d’informations enregistrées :

# Définir le niveau de log pour module_a
logging.getLogger('module_a').setLevel(logging.WARNING)
# Définir le niveau de log pour module_b
logging.getLogger('module_b').setLevel(logging.DEBUG)

Dans cet exemple, module_a n’enregistrera que les messages de niveau WARNING et supérieur, tandis que module_b enregistrera tous les messages de niveau DEBUG et supérieur.

Logging des exceptions

Lors du développement d’applications, il est nécessaire de consigner non seulement les messages informatifs mais aussi les exceptions qui surviennent. En enregistrant les exceptions, vous pouvez obtenir des informations détaillées sur les erreurs, y compris les stack traces, ce qui facilite le débogage et la correction des problèmes.

Utiliser logger.exception()

La méthode logger.exception() est particulièrement utile pour consigner les exceptions avec une stack trace complète. Cette méthode est similaire à logger.error(), mais elle ajoute automatiquement les détails de l’exception au message de log. Elle doit être utilisée uniquement dans un bloc except.

Exemple simple d’utilisation :

import logging
# Configurer le logger
logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
# Créer un logger
logger = logging.getLogger('exception_logger')
try:
1 / 0 # Provoquer une exception de division par zéro
except ZeroDivisionError:
logger.exception("Une exception de division par zéro est survenue")

Dans cet exemple, le log contiendra un message de type ERROR avec la stack trace détaillant l’endroit où l’exception a été déclenchée.

Utiliser exc_info=True

Si vous utilisez des méthodes comme logger.error() ou logger.critical(), vous pouvez toujours inclure la stack trace en ajoutant exc_info=True :

import logging
# Configurer le logger
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
# Créer un logger
logger = logging.getLogger('detailed_exception_logger')
try:
undefined_variable # Provoquer une exception de variable non définie
except NameError as e:
logger.error("Une erreur est survenue avec une variable non définie", exc_info=True)

Conclusion

La gestion des logs en Python est indispensable pour surveiller, déboguer et maintenir des applications de manière efficace. Qu’il s’agisse de consigner des événements importants, de capturer des erreurs ou de suivre le déroulement de votre code, la bibliothèque logging intégrée offre toute la flexibilité nécessaire. En comprenant et en maîtrisant les différentes configurations, niveaux de gravité et destinations des logs, vous pouvez construire un système de journalisation robuste qui facilite le diagnostic et l’amélioration continue de votre application.