Définition
Un programme qui tourne sans personne devant l'écran finit tôt ou tard par rencontrer un cas imprévu. Le savoir arrive après coup : sans trace écrite pendant l'exécution, il ne reste que le résultat final, qui ne raconte jamais l'histoire qui y a mené.
logging est le module de la bibliothèque standard qui écrit cette histoire à mesure que le programme avance. Chaque message part avec une date, un niveau de gravité et le nom de l'endroit d'où il vient, puis atterrit là où la configuration l'envoie : terminal, fichier, service de collecte. Le code dit ce qui s'est passé, la configuration décide qui l'entend.
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Import démarré, %s lignes à traiter", 1200)
logger.warning("Ligne 47 ignorée, date illisible")Le __name__ passé à getLogger n'est pas un détail de style : il colle au message sa provenance exacte, et permettra plus tard de faire taire un fichier bavard sans faire taire les autres.
print ou logging
Le premier réflexe, en écrivant du code, est d'ajouter un print pour voir passer une valeur, ce qui marche tant que quelqu'un regarde le terminal. Une fois le programme livré, ces print deviennent du bruit permanent, ou des messages qu'on aurait voulu garder.
logging reprend le même geste, mais ajoute ce qu'un print ne sait pas faire seul : une date automatique, un niveau filtrable après coup, une destination qu'on peut changer sans toucher au code. Voici les deux côte à côte :
| Besoin | Avec print | Avec logging |
|---|---|---|
| Voir une valeur pendant qu'on écrit le code | Parfait | Inutilement lourd |
| Savoir à quelle heure la chose est arrivée | À écrire soi-même | Horodatage automatique |
| Couper le bavardage en production | Supprimer les lignes | Relever un seuil |
| Écrire dans un fichier | Rediriger le terminal | Ajouter un handler |
| Garder la pile d'un plantage | À la main | Une seule méthode |
Il existe des cas où logging n'apporte rien, un script de trente lignes par exemple. Plus important : un programme qui affiche son résultat doit utiliser print, jamais logging, le premier répondant à la personne qui attend, le second racontant en coulisses ce que le programme fait.
Les cinq niveaux
Écrire un message à chaque étape semble une bonne idée, jusqu'à ce que le terminal déborde et que l'important se noie dans le détail. logging répond en attachant un niveau à chaque message, comparé à un seuil configuré : seuls les messages qui l'atteignent s'affichent.
Voici les cinq niveaux, du plus courant au plus grave :
| Niveau | Ce qu'il raconte |
|---|---|
DEBUG | Le détail utile quand on cherche un bug, invisible le reste du temps |
INFO | Une étape normale : le traitement démarre, le fichier est écrit |
WARNING | Une anomalie que le programme a su contourner |
ERROR | Une opération a échoué, le programme continue |
CRITICAL | Le programme ne peut plus continuer |
Le seuil par défaut vaut WARNING. Un programme tout juste configuré n'affiche donc ni ses DEBUG ni ses INFO, ce qui surprend presque tout le monde la première fois.
Le piège du premier jour
« J'appelle logger.info et rien ne s'affiche. » C'est cette même surprise du seuil : le message n'est pas perdu, il est filtré. La correction tient en un appel, une seule fois, au point d'entrée du programme, avant tout autre message :
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s : %(message)s",
)Cet appel cache une deuxième surprise, plus sournoise : basicConfig ne fait rien si la journalisation est déjà configurée, et un premier message émis avant l'appel la configure par défaut, sans prévenir. Un appel trop tardif est donc ignoré en silence.
Pour qui écrit une bibliothèque destinée à d'autres, la règle est inverse : n'appelez jamais basicConfig à l'intérieur, sous peine d'imposer votre format au programme qui l'installe.
Enregistrer une erreur sans perdre le traceback
Dans un bloc except, le réflexe est de recopier le message de l'exception et de passer à la suite. Cela jette pourtant le plus utile : le traceback, qui dit à quelle ligne le problème est né. Sans lui, il faut reconstituer ce chemin de mémoire, ou reproduire le bug pour le revoir.
La méthode exception l'attache toute seule, et se réserve aux blocs de rattrapage :
try:
unitaire = facture["montant"] / facture["quantite"]
except ZeroDivisionError:
logger.exception("Quantité nulle sur la facture %s", facture["id"])
unitaire = 0Notez la virgule, là où une f-string aurait été tentante. Le message reste un gabarit constant, ce qui laisse un outil de collecte regrouper mille occurrences d'un même incident au lieu de mille lignes distinctes. Le gain de performance parfois mis en avant reste négligeable hors des boucles très chaudes.
Questions fréquentes
Faut-il un logger par fichier ou un seul pour tout le programme ?
Un par fichier, toujours avec la même ligne en haut : logger = logging.getLogger(__name__). Les loggers forment une hiérarchie fondée sur les points du nom, si bien que boutique.paiement hérite des réglages de boutique, ce qui permet de passer un seul module en DEBUG sans noyer le reste.
Faut-il écrire les journaux dans un fichier ?
Cela dépend surtout d'où le programme tourne. Sur un serveur classique, oui, avec un handler qui découpe le fichier au-delà d'une certaine taille, faute de quoi il devient illisible. Dans un conteneur, non : la sortie standard suffit, l'hébergeur ramasse et archive, et c'est ainsi que se déploie d'ordinaire une application FastAPI ou Django.
Que ne faut-il jamais écrire dans un journal ?
Un mot de passe, un jeton, un numéro de carte, le contenu complet d'un formulaire. Un journal se conserve des mois et finit lu par des personnes sans accès à la base de données. Enregistrez plutôt l'identifiant de l'utilisateur concerné que ses données, et évitez d'écrire une réponse json entière sans savoir précisément ce qu'elle contient.