Tutoriel sur la journalisation

Auteur:Vinay Sajip <vinay_sajip at red-dove dot com>

Les bases de l'utilisation du module logging

La journalisation (logging en anglais) est une façon de suivre les événements qui ont lieu durant le fonctionnement d'un logiciel. Le développeur du logiciel ajoute des appels à l'outil de journalisation dans son code pour indiquer que certains événements ont eu lieu. Un événement est décrit par un message descriptif, qui peut éventuellement contenir des données variables (c'est-à-dire qui peuvent être différentes pour chaque occurrence de l'événement). Un événement a aussi une importance que la développeur lui attribue ; cette importance peut aussi être appelée niveau ou sévérité.

Quand utiliser logging

Le module logging fournit un ensemble de fonctions de commodités pour une utilisation simple du module. Ce sont les fonctions debug(), info(), warning(), error() et critical(). Pour déterminer quand employer la journalisation, voyez la table ci-dessous, qui vous indique, pour chaque tâche parmi les plus communes, l'outil approprié.

Tâche que vous souhaitez mener Le meilleur outil pour cette tâche
Afficher la sortie console d'un script en ligne de commande ou d'un programme lors de son utilisation ordinaire. print()
Rapporter des évènements qui ont lieu au cours du fonctionnement normal d'un programme (par exemple pour suivre un statut ou examiner des dysfonctionnements) logging.info() (ou logging.debug() pour une sortie très détaillée à visée diagnostique)
Émettre un avertissement (warning en anglais) en relation avec un évènement particulier au cours du fonctionnement

warnings.warn() dans le code de la bibliothèque si le problème est évitable et l'application cliente doit être modifiée pour éliminer ce warning

logging.warning() si l'application cliente ne peut rien faire pour corriger la situation mais l'évènement devrait quand même être noté

Rapporter une erreur lors d'un évènement particulier en cours d'exécution Lever une exception
Rapporter la suppression d'une erreur sans lever d'exception (par exemple pour la gestion d'erreur d'un processus de long terme sur un serveur) logging.error(), logging.exception() ou logging.critical(), au mieux, selon l'erreur spécifique et le domaine d'application

Les fonctions de journalisation sont nommées d'après le niveau ou la sévérité des évènements qu'elles suivent. Les niveaux standards et leurs applications sont décrits ci-dessous (par ordre croissant de sévérité) :

Niveau Quand il est utilisé
DEBUG Information détaillée, intéressante seulement lorsqu'on diagnostique un problème.
INFO Confirmation que tout fonctionne comme prévu
WARNING L'indication que quelque chose d'inattendu a eu lieu, ou de la possibilité d'un problème dans un futur proche (par exemple « espace disque faible »). Le logiciel fonctionne encore normalement.
ERROR Du fait d'un problème plus sérieux, le logiciel n'a pas été capable de réaliser une tâche.
CRITICAL Une erreur sérieuse, indiquant que le programme lui-même pourrait être incapable de continuer à fonctionner.

Le niveau par défaut est WARNING, ce qui signifie que seuls les évènements de ce niveau et au dessus sont suivis, sauf si le paquet logging est configuré pour faire autrement.

Les évènements suivis peuvent être gérés de différentes façons. La manière la plus simple est de les afficher dans la console. Une autre méthode commune est de les écrire dans un fichier.

Un exemple simple

Un exemple très simple est :

import logging
logging.warning('Watch out!')  # will print a message to the console
logging.info('I told you so')  # will not print anything

Si vous entrez ces lignes dans un script que vous exécutez, vous verrez :

WARNING:root:Watch out!

affiché dans la console. Le message INFO n'apparaît pas parce que le niveau par défaut est WARNING. Le message affiché inclut l'indication du niveau et la description de l'évènement fournie dans l'appel à logging, i.e. « Watch out! ». Ne vous préoccupez pas de la partie « root » pour le moment : nous détaillerons ce point plus bas. La sortie elle même peut être formatée de multiples manières si besoin. Les options de formatage seront aussi expliquées plus bas.

Enregistrer les évènements dans un fichier

Il est très commun d'enregistrer les évènements dans un fichier, c'est donc ce que nous allons regarder maintenant. Il faut essayer ce qui suit avec un interpréteur Python nouvellement démarré, ne poursuivez pas la session commencée ci-dessus :

import logging
logging.basicConfig(filename='example.log',level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')

Maintenant, si nous ouvrons le fichier et lisons ce qui s'y trouve, on trouvera les messages de log :

DEBUG:root:This message should go to the log file
INFO:root:So should this
WARNING:root:And this, too

Cet exemple montre aussi comment on peut régler le niveau de journalisation qui sert de seuil pour le suivi. Dans ce cas, comme nous avons réglé le seuil à DEBUG, tous les messages ont été écrits.

Si vous souhaitez régler le niveau de logging à partir d'une option de la ligne de commande comme :

--log=INFO

et que vous passez ensuite la valeur du paramètre donné à l'option --log dans une variable loglevel, vous pouvez utiliser :

getattr(logging, loglevel.upper())

de manière à obtenir la valeur à passer à basicConfig() à travers l'argument level. Vous pouvez vérifier que l'utilisateur n'a fait aucune erreur pour la valeur de ce paramètre, comme dans l'exemple ci-dessous :

# assuming loglevel is bound to the string value obtained from the
# command line argument. Convert to upper case to allow the user to
# specify --log=DEBUG or --log=debug
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
    raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level, ...)

L'appel à basicConfig() doit être fait avant un appel à debug(), info(), etc. Comme l'objectif est d'avoir un outil de configuration simple et d'usage unique, seul le premier appel aura un effet, les appels suivants ne font rien.

Si vous exécutez le script plusieurs fois, les messages des exécutions successives sont ajoutés au fichier example.log. Si vous voulez que chaque exécution reprenne un fichier vierge, sans conserver les messages des exécutions précédentes, vous pouvez spécifier l'argument filemode, en changeant l'appel à l'exemple précédent par :

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

La sortie est identique à la précédente, mais le texte n'est plus ajouté au fichier de log, donc les messages des exécutions précédentes sont perdus.

Employer logging à partir de différents modules

Si votre programme est composé de plusieurs modules, voici une façon d'organiser l'outil de journalisation :

# myapp.py
import logging
import mylib

def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    logging.info('Started')
    mylib.do_something()
    logging.info('Finished')

if __name__ == '__main__':
    main()
# mylib.py
import logging

def do_something():
    logging.info('Doing something')

Si vous exécutez myapp.py, vous verrez ceci dans myapp.log :

INFO:root:Started
INFO:root:Doing something
INFO:root:Finished

ce qui est normalement ce à quoi vous vous attendiez. Vous pouvez généraliser cela à plusieurs modules, en employant le motif de mylib.py. Remarquez qu'avec cette méthode simple, vous ne pourrez pas savoir, en lisant le fichier de log, d'où viennent les messages dans votre application, sauf dans la description de l'évènement. Si vous voulez suivre la localisation des messages, référerez vous à la documentation avancée Usage avancé de Logging.

Journalisation de données variables

Pour enregistrer des données variables, utilisez une chaîne formatée dans le message de description de l'évènement et ajoutez les données variables comme argument. Par exemple :

import logging
logging.warning('%s before you %s', 'Look', 'leap!')

affichera :

WARNING:root:Look before you leap!

Comme vous pouvez le voir, l'inclusion des données variables dans le message de description de l'évènement emploie le vieux style de formatage avec %. C'est pour assurer la rétrocompatibilité : le module logging est antérieur aux nouvelles options de formatage comme str.format() ou string.Template. Ces nouvelles options de formatage sont gérées, mais leur exploration sors du cadre de ce tutoriel, voyez Using particular formatting styles throughout your application pour plus d'information.

Modifier le format du message affiché

Pour changer le format utilisé pour afficher le message, vous devez préciser le format que vous souhaitez employer :

import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
logging.info('So should this')
logging.warning('And this, too')

Ce qui affiche :

DEBUG:This message should appear on the console
INFO:So should this
WARNING:And this, too

Notez que le root qui apparaissait dans les exemples précédents a disparu. Pour voir l'ensemble des éléments qui peuvent apparaître dans la chaîne de format, référez-vous à la documentation pour LogRecord attributes. Pour une utilisation simple, vous avez seulement besoin du levelname (la sévérité), du message (la description de l'évènement, avec les données variables) et peut-être du moment auquel l'évènement a eu lieu. Nous décrivons ces points dans la prochaine section.

Afficher l'horodatage dans les messages

Pour afficher la date ou le temps d'un évènement, ajoutez %(asctime) dans votre chaîne de formatage :

import logging
logging.basicConfig(format='%(asctime)s %(message)s')
logging.warning('is when this event was logged.')

Ce qui affichera quelque chose comme :

2010-12-12 11:41:42,612 is when this event was logged.

Le format par défaut de l'horodatage (comme ci-dessus) est donné par la norme ISO8601 ou RFC 3339. Pour plus de contrôle sur le formatage de l'horodatage, vous pouvez fournir à basicConfig un argument datefmt, comme dans l'exemple suivant :

import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.warning('is when this event was logged.')

Ce qui affichera quelque chose comme :

12/12/2010 11:46:36 AM is when this event was logged.

Le format de datefmt est le même que celui de time.strftime().

Étapes suivantes

Nous concluons ainsi le tutoriel basique. Il devrait suffire à vous mettre le pied à l'étrier pour utiliser logging. Le module logging a beaucoup d'autre cordes à son arc, mais pour en profiter au maximum, vous devez prendre le temps de lire les sections suivantes. Si vous êtes prêt, servez-vous votre boisson préférée et poursuivons.

If your logging needs are simple, then use the above examples to incorporate logging into your own scripts, and if you run into problems or don't understand something, please post a question on the comp.lang.python Usenet group (available at https://groups.google.com/forum/#!forum/comp.lang.python) and you should receive help before too long.

Vous êtes encore là ? Vous pouvez lire les prochaines section, qui donnent un peu plus de détails que l'introduction ci-dessus. Après ça, vous pouvez jeter un œil à Logging Cookbook.

Usage avancé de Logging

La bibliothèque de journalisation adopte une approche modulaire et offre différentes catégories de composants : loggers, handlers, filters et formatters.

  • Les enregistreurs (loggers en anglais) exposent l'interface que le code de l'application utilise directement.
  • Les gestionnaires (handlers) envoient les entrées de journal (crées par les loggers) vers les destinations voulues.
  • Les filtres (filters) fournissent un moyen de choisir finement quelles entrées de journal doivent être sorties.
  • Les formateurs (formatters) spécifient la structure de l'entrée de journal dans la sortie finale.

L'information relative à un événement est passée entre loggers, handlers et formatters dans une instance de la classe LogRecord.

La journalisation est réalisée en appelant les méthodes d'instance de la classe Logger (que l'on appelle ci-dessous loggers). Chaque instance a un nom, et sont organisées conceptuellement comme des hiérarchies dans l'espace des noms, en utilisant un point comme séparateur. Par exemple, un logger appelé scan est le parent des loggers scan.text, scan.html et scan.pdf. Les noms des loggers peuvent être ce que vous voulez et indiquer le sous-domaine d'une application depuis lequel le message enregistré a été émis.

Une bonne convention lorsqu'on nomme les loggers est d'utiliser un logger au niveau du module, dans chaque module qui emploie logging, nommé de la façon suivante :

logger = logging.getLogger(__name__)

Cela signifie que le nom d'un logger se rapporte à la hiérarchie du paquet et des modules, et il est évident où un événement à été enregistré simplement en voyant le nom du logger.

La racine de la hiérarchie des loggers est appelée le root logger. C'est le logger utilisé par les fonctions debug(), info(), warning(), error() et critical(), qui appelle en fait les méthodes du mêmes nom de l'objet root logger. Les fonctions et les méthodes ont la même signature. Le nom du root logger est affiché comme « root » dans la sortie.

Il est bien sûr possible d'enregistrer des messages pour des destinations différentes. Ce paquet permet d'écrire des entrées de journal dans des fichiers, des ressources HTTP GET/POST, par courriel via SMTP, des sockets génériques, des files d'attente, ou des mécanismes d'enregistrement spécifiques au système d'exploitation, comme syslog ou le journal d'événements de Windows NT. Les destinations sont servies par des classes handler. Vous pouvez créer votre propre classe de destination si vous avez des besoins spéciaux qui ne sont couverts par aucune classe handler prédéfinie.

Par défaut, aucune destination n'est prédéfinie pour les messages de journalisation. Vous pouvez définir une destination (comme la console ou un fichier) en utilisant basicConfig() comme dans les exemples donnés dans le tutoriels. Si vous appelez les fonctions debug(), info(), warning(), error() et critical(), celles-ci vérifient si une destination a été définie ; si ce n'est pas le cas, la destination est assignée à la console (sys.stderr) avec un format par défaut pour le message affiché, avant d'être déléguée au logger racine, qui sort le message.

Le format par défaut des messages est défini par basicConfig() comme suit :

severity:logger name:message

Vous pouvez modifier ce comportement en passant une chaîne de formatage à basicConfig() par l'argument nommé format. Voyez Formatter Objects pour toutes les options de construction de cette chaîne de formatage.

Flux du processus de journalisation

Le flux des informations associées à un évènement dans les loggers et les handlers est illustré dans le diagramme suivant.

../_images/logging_flow.png

Loggers

Les objets de classe Logger ont un rôle triple. Premièrement, ils exposent plusieurs méthodes au code de l'application, de manière à ce qu'elle puisse enregistrer des messages en cours d'exécution. Deuxièmement, les objets logger déterminent sur quel message agir selon leur sévérité (à partir des filtres par défaut) ou selon les objets filter associés. Troisièmement, les objets logger transmettent les messages pertinents à tous les handlers concernés.

Les méthodes des objets logger les plus utilisées appartiennent à deux catégories : la configuration et l'envoi de messages.

Voici les méthodes de configuration les plus communes :

  • Logger.setLevel() spécifie le plus bas niveau de sévérité qu'un logger traitera. Ainsi, debug est le niveau de sévérité défini par défaut le plus bas, et critical le plus haut. Par exemple, si le niveau de sévérité est INFO, le logger ne traite que les message de niveau INFO, WARNING, ERROR et CRITICAL, et ignore les messages de niveau DEBUG.
  • Logger.addHandler() et Logger.removeHandler() ajoutent ou enlèvent des objets handlers au logger. Les objets handlers sont expliqués plus en détail dans Handlers.
  • Logger.addFilter() et Logger.removeFilter() ajoutent ou enlèvent des objets filter au logger. Les objets filters sont expliqués plus en détail dans Filter Objects.

Comme nous l'expliquons aux deux derniers paragraphes de cette section, vous n'avez pas besoin de faire appel à ces méthodes à chaque fois que vous créez un logger.

Une fois que l'objet logger est correctement configuré, les méthodes suivantes permettent de créer un message :

  • Les méthodes Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), et Logger.critical`créent des entrées de journal avec un message et un niveau correspondant à leur nom. Le message est en fait une chaîne de caractère qui peut contenir la syntaxe  standard de substitution de chaînes de caractères : ``%s`(), %d, %f, etc. L'argument suivant est une liste des objets correspondant aux champs à substituer dans le message. En ce qui concerne **kwargs, les méthodes de logging ne tiennent compte que du mot clef exc_info et l'utilisent pour déterminer s'il faut enregistrer les informations associées à une exception.
  • Logger.exception() crée un message similaire à Logger.error(). La différence est que Logger.exception() ajoute la trace de la pile d'exécution au message. On ne peut appeler cette méthode qu'à l'intérieur d'un bloc de gestion d'exception.
  • Logger.log() prend le niveau de sévérité comme argument explicite. C'est un peu plus verbeux pour enregistrer des messages que d'utiliser les méthodes plus pratiques décrites si dessus, mais c'est ce qui permet d'enregistrer des messages pour des niveaux de sévérité définis par l'utilisateur.

getLogger() renvoie une référence à un objet logger du nom spécifié si celui-ci est donné en argument. Dans le cas contraire, se sera le logger root. Ces noms sont des structures hiérarchiques séparées par des points. Des appels répétés à getLogger() avec le même nom renvoient une référence au même objet logger. Les loggers qui sont plus bas dans cette liste hiérarchique sont des enfants des loggers plus haut dans la liste. Par exemple, si un logger a le nom foo, les loggers avec les noms foo.bar, foo.bar.baz, et foo.bam sont tous des descendants de foo.

On associe aux loggers un concept de niveau effectif. Si aucun niveau n'est explicitement défini pour un logger, c'est le niveau du parent qui est utilisé comme niveau effectif. Si le parent n'a pas de niveau défini, c'est celui de son parent qui est considéré, et ainsi de suite ; on examine tous les ancêtres jusqu'à ce qu'un niveau explicite soit trouvé. Le logger root a toujours un niveau explicite (WARNING par défaut). Quand le logger traite un événement, c'est ce niveau effectif qui est utilisé pour déterminer si cet événement est transmis à ses handlers.

Les loggers fils font remonter leurs messages aux handlers associés à leurs loggers ancêtres. De ce fait, il n'est pas nécessaire de définir et configurer des handlers pour tous les loggers employés par une application. Il suffit de configurer les handlers pour un logger de haut niveau et de créer des loggers fils quand c'est nécessaire (on peut cependant empêcher la propagation aux ancêtres des messages en donnant la valeur False à l'attribut propagate d'un logger).

Handlers

Les objets de type Handler sont responsables de la distribution des messages (selon leur niveau de sévérité) vers les destinations spécifiées pour ce handler. Les objets Logger peuvent ajouter des objets handler à eux-mêmes en appelant addHandler(). Pour donner un exemple, une application peut envoyer tous les messages dans un fichier journal, tous les messages de niveau error ou supérieur vers la sortie standard, et tous les messages de niveau critical vers une adresse de courriel. Dans ce scenario, nous avons besoin de trois handlers, responsable chacun d'envoyer des messages d'une sévérité donnée vers une destination donnée.

La bibliothèque standard inclut déjà un bon nombre de types d'handlers (voir Useful Handlers) ; le tutoriel utilise surtout StreamHandler et FileHandler dans ses exemples.

Peu de méthodes des objets handlers sont intéressantes pour les développeurs. Les seules méthodes intéressantes lorsqu'on utilise les objets handlers natifs (c'est à dire si l'on ne crée pas d'handler personnalisé) sont les méthodes de configuration suivantes :

  • La méthode setLevel(), comme celle des objets logger permet de spécifier le plus bas niveau de sévérité qui sera distribué à la destination appropriée. Pourquoi y a-t-il deux méthodes setLevel() ? Le niveau défini dans le logger détermine quelle sévérité doit avoir un message pour être transmis à ses handlers. Le niveau mis pour chaque handler détermine quels messages seront envoyés aux destinations.
  • setFormatter() sélectionne l'objet Formatter utilisé par cet handler.
  • addFilter() et removeFilter() configurent et respectivement dé-configurent des objets filter sur les handlers.

Le code d'une application ne devrait ni instancier, ni utiliser d'instances de la classe Handler. La classe Handler est plutôt d'une classe de base qui défini l'interface que tous les handlers doivent avoir, et établi les comportements par défaut que les classes filles peuvent employer (ou redéfinir).

Formatters

Les objets formatter configurent l'ordre final, la structure et le contenu du message. Contrairement à la classe de base logging.Handler, le code d'une application peut instancier un objet de classe formatter, même si vous pouvez toujours sous-classer formatter si vous avez besoin d'un comportement spécial dans votre application. Le constructeur a trois arguments optionnels : une chaîne de formatage du message, un chaîne de formatage de la date et un indicateur de style.

logging.Formatter.__init__(fmt=None, datefmt=None, style='%')

S'il n'y a pas de chaîne de formatage, la chaîne brute est utilisée par défaut. S'il n'y a pas de chaîne de formatage de date, le format de date par défaut est :

%Y-%m-%d %H:%M:%S

avec les millisecondes en suffixe. Le style est %, { ou $. Si aucun n'est spécifié, % sera utilisé.

Si l'argument style est %, la chaîne de formatage utilise %(<clef de dictionnaire>)s comme style de substitution de chaîne de caractères ; les clefs possibles sont documentées dans LogRecord attributes. Si le style est {, le message de la chaîne de formatage est compatible avec str.format() (en employant des arguments à mots clefs). Enfin si le style est $ alors la chaîne de formatage du message doit être conforme à ce qui est attendu de string.Template.substitute().

Modifié dans la version 3.2: Ajout du paramètre style.

Le chaîne de formatage de message suivante enregistrera le temps dans un format lisible par les humains, la sévérité du message et son contenu, dans cet ordre :

'%(asctime)s - %(levelname)s - %(message)s'

Les formatters emploient une fonction configurable par l'utilisateur pour convertir le temps de création d'une entrée de journal en un tuple. Par défaut, time.localtime() est employé ; pour changer cela pour une instance particulière de formatter, assignez une fonction avec la même signature que time.localtime() ou time.gmtime() à l'attribut converter de cette instance. Pour changer cela pour tous les formatters, par exemple si vous voulez que tous votre horodatage soit affiché en GMT, changez l'attribut converter de la classe Formatter en time.gmtime.

Configuration de Logging

On peut configurer logging de trois façons :

  1. Créer des loggers, handlers et formatters explicitement en utilisant du code Python qui appelle les méthodes de configuration listées ci-dessus.
  2. Créer un ficher de configuration de logging et le lire en employant la fonction fileConfig().
  3. Créer un dictionnaire d'informations de configuration et le passer à la fonction dictConfig().

Pour la documentation de référence de ces deux dernières options, voyez Configuration functions. L'exemple suivant configure un logger très simple, un handler employant la console, et un formatter simple en utilisant du code Python :

import logging

# create logger
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)

# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# add formatter to ch
ch.setFormatter(formatter)

# add ch to logger
logger.addHandler(ch)

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')

L'exécution de ce module via la ligne de commande produit la sortie suivante :

$ python simple_logging_module.py
2005-03-19 15:10:26,618 - simple_example - DEBUG - debug message
2005-03-19 15:10:26,620 - simple_example - INFO - info message
2005-03-19 15:10:26,695 - simple_example - WARNING - warn message
2005-03-19 15:10:26,697 - simple_example - ERROR - error message
2005-03-19 15:10:26,773 - simple_example - CRITICAL - critical message

Le module Python suivant crée un logger, un handler et un formatter identiques à ceux de l'exemple détaillé au dessus, au nom des objets près :

import logging
import logging.config

logging.config.fileConfig('logging.conf')

# create logger
logger = logging.getLogger('simpleExample')

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')

Voici le fichier logging.conf :

[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=

La sortie est presque identique à celle de l'exemple qui n'est pas basé sur un fichier de configuration :

$ python simple_logging_config.py
2005-03-19 15:38:55,977 - simpleExample - DEBUG - debug message
2005-03-19 15:38:55,979 - simpleExample - INFO - info message
2005-03-19 15:38:56,054 - simpleExample - WARNING - warn message
2005-03-19 15:38:56,055 - simpleExample - ERROR - error message
2005-03-19 15:38:56,130 - simpleExample - CRITICAL - critical message

Vous pouvez constatez les avantages de l'approche par fichier de configuration par rapport à celle du code Python, principalement la séparation de la configuration et du code, et la possibilité pour une personne qui ne code pas de modifier facilement les propriétés de logging.

Avertissement

La fonction fileConfig() accepte un paramètre par défaut disable_existing_loggers, qui vaut True par défaut pour des raisons de compatibilité ascendante. Ce n'est pas forcément ce que vous souhaitez : en effet, tout les loggers créés avant l'appel à fileConfig() seront désactivés sauf si eux-mêmes (ou l'un de leurs ancêtres) sont explicitement nommés dans le fichier de configuration. Veuillez vous rapporter à la documentation pour plus de détails, et donner la valeur False à ce paramètre si vous le souhaitez.

Le dictionnaire passé à dictConfig() peut aussi spécifier une valeur Booléenne pour la clef disable_existing_loggers. Si cette valeur n'est pas donnée, elle est interprétée comme vraie par défaut. Cela conduit au comportement de désactivation des loggers décrit ci-dessus, qui n'est pas forcément celui que vous souhaitez ; dans ce cas, donnez explicitement la valeur False à cette clef.

Notez que les noms de classe référencés dans le fichier de configuration doivent être relatifs aux module logging, ou des valeurs absolues qui peuvent être résolues à travers les mécanismes d'importation habituels. Ainsi, on peut soit utiliser WatchedFileHandler (relativement au module logging) ou mypackage.mymodule.MyHandler (pour une classe définie dans le paquet mypackage et le module mymodule, si mypackage est disponible dans les chemins d'importation de Python).

In Python 3.2, a new means of configuring logging has been introduced, using dictionaries to hold configuration information. This provides a superset of the functionality of the config-file-based approach outlined above, and is the recommended configuration method for new applications and deployments. Because a Python dictionary is used to hold configuration information, and since you can populate that dictionary using different means, you have more options for configuration. For example, you can use a configuration file in JSON format, or, if you have access to YAML processing functionality, a file in YAML format, to populate the configuration dictionary. Or, of course, you can construct the dictionary in Python code, receive it in pickled form over a socket, or use whatever approach makes sense for your application.

Here's an example of the same configuration as above, in YAML format for the new dictionary-based approach:

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    level: DEBUG
    formatter: simple
    stream: ext://sys.stdout
loggers:
  simpleExample:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]

For more information about logging using a dictionary, see Configuration functions.

What happens if no configuration is provided

If no logging configuration is provided, it is possible to have a situation where a logging event needs to be output, but no handlers can be found to output the event. The behaviour of the logging package in these circumstances is dependent on the Python version.

For versions of Python prior to 3.2, the behaviour is as follows:

  • If logging.raiseExceptions is False (production mode), the event is silently dropped.
  • If logging.raiseExceptions is True (development mode), a message 'No handlers could be found for logger X.Y.Z' is printed once.

In Python 3.2 and later, the behaviour is as follows:

  • The event is output using a 'handler of last resort', stored in logging.lastResort. This internal handler is not associated with any logger, and acts like a StreamHandler which writes the event description message to the current value of sys.stderr (therefore respecting any redirections which may be in effect). No formatting is done on the message - just the bare event description message is printed. The handler's level is set to WARNING, so all events at this and greater severities will be output.

To obtain the pre-3.2 behaviour, logging.lastResort can be set to None.

Configuring Logging for a Library

When developing a library which uses logging, you should take care to document how the library uses logging - for example, the names of loggers used. Some consideration also needs to be given to its logging configuration. If the using application does not use logging, and library code makes logging calls, then (as described in the previous section) events of severity WARNING and greater will be printed to sys.stderr. This is regarded as the best default behaviour.

If for some reason you don't want these messages printed in the absence of any logging configuration, you can attach a do-nothing handler to the top-level logger for your library. This avoids the message being printed, since a handler will be always be found for the library's events: it just doesn't produce any output. If the library user configures logging for application use, presumably that configuration will add some handlers, and if levels are suitably configured then logging calls made in library code will send output to those handlers, as normal.

A do-nothing handler is included in the logging package: NullHandler (since Python 3.1). An instance of this handler could be added to the top-level logger of the logging namespace used by the library (if you want to prevent your library's logged events being output to sys.stderr in the absence of logging configuration). If all logging by a library foo is done using loggers with names matching 'foo.x', 'foo.x.y', etc. then the code:

import logging
logging.getLogger('foo').addHandler(logging.NullHandler())

should have the desired effect. If an organisation produces a number of libraries, then the logger name specified can be 'orgname.foo' rather than just 'foo'.

Note

It is strongly advised that you do not add any handlers other than NullHandler to your library's loggers. This is because the configuration of handlers is the prerogative of the application developer who uses your library. The application developer knows their target audience and what handlers are most appropriate for their application: if you add handlers 'under the hood', you might well interfere with their ability to carry out unit tests and deliver logs which suit their requirements.

Logging Levels

The numeric values of logging levels are given in the following table. These are primarily of interest if you want to define your own levels, and need them to have specific values relative to the predefined levels. If you define a level with the same numeric value, it overwrites the predefined value; the predefined name is lost.

Niveau Numeric value
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0

Levels can also be associated with loggers, being set either by the developer or through loading a saved logging configuration. When a logging method is called on a logger, the logger compares its own level with the level associated with the method call. If the logger's level is higher than the method call's, no logging message is actually generated. This is the basic mechanism controlling the verbosity of logging output.

Logging messages are encoded as instances of the LogRecord class. When a logger decides to actually log an event, a LogRecord instance is created from the logging message.

Logging messages are subjected to a dispatch mechanism through the use of handlers, which are instances of subclasses of the Handler class. Handlers are responsible for ensuring that a logged message (in the form of a LogRecord) ends up in a particular location (or set of locations) which is useful for the target audience for that message (such as end users, support desk staff, system administrators, developers). Handlers are passed LogRecord instances intended for particular destinations. Each logger can have zero, one or more handlers associated with it (via the addHandler() method of Logger). In addition to any handlers directly associated with a logger, all handlers associated with all ancestors of the logger are called to dispatch the message (unless the propagate flag for a logger is set to a false value, at which point the passing to ancestor handlers stops).

Just as for loggers, handlers can have levels associated with them. A handler's level acts as a filter in the same way as a logger's level does. If a handler decides to actually dispatch an event, the emit() method is used to send the message to its destination. Most user-defined subclasses of Handler will need to override this emit().

Custom Levels

Defining your own levels is possible, but should not be necessary, as the existing levels have been chosen on the basis of practical experience. However, if you are convinced that you need custom levels, great care should be exercised when doing this, and it is possibly a very bad idea to define custom levels if you are developing a library. That's because if multiple library authors all define their own custom levels, there is a chance that the logging output from such multiple libraries used together will be difficult for the using developer to control and/or interpret, because a given numeric value might mean different things for different libraries.

Useful Handlers

In addition to the base Handler class, many useful subclasses are provided:

  1. StreamHandler instances send messages to streams (file-like objects).
  2. FileHandler instances send messages to disk files.
  3. BaseRotatingHandler is the base class for handlers that rotate log files at a certain point. It is not meant to be instantiated directly. Instead, use RotatingFileHandler or TimedRotatingFileHandler.
  4. RotatingFileHandler instances send messages to disk files, with support for maximum log file sizes and log file rotation.
  5. TimedRotatingFileHandler instances send messages to disk files, rotating the log file at certain timed intervals.
  6. SocketHandler instances send messages to TCP/IP sockets. Since 3.4, Unix domain sockets are also supported.
  7. DatagramHandler instances send messages to UDP sockets. Since 3.4, Unix domain sockets are also supported.
  8. SMTPHandler instances send messages to a designated email address.
  9. SysLogHandler instances send messages to a Unix syslog daemon, possibly on a remote machine.
  10. NTEventLogHandler instances send messages to a Windows NT/2000/XP event log.
  11. MemoryHandler instances send messages to a buffer in memory, which is flushed whenever specific criteria are met.
  12. HTTPHandler instances send messages to an HTTP server using either GET or POST semantics.
  13. WatchedFileHandler instances watch the file they are logging to. If the file changes, it is closed and reopened using the file name. This handler is only useful on Unix-like systems; Windows does not support the underlying mechanism used.
  14. QueueHandler instances send messages to a queue, such as those implemented in the queue or multiprocessing modules.
  15. NullHandler instances do nothing with error messages. They are used by library developers who want to use logging, but want to avoid the 'No handlers could be found for logger XXX' message which can be displayed if the library user has not configured logging. See Configuring Logging for a Library for more information.

Nouveau dans la version 3.1: The NullHandler class.

Nouveau dans la version 3.2: The QueueHandler class.

The NullHandler, StreamHandler and FileHandler classes are defined in the core logging package. The other handlers are defined in a sub-module, logging.handlers. (There is also another sub-module, logging.config, for configuration functionality.)

Logged messages are formatted for presentation through instances of the Formatter class. They are initialized with a format string suitable for use with the % operator and a dictionary.

For formatting multiple messages in a batch, instances of BufferingFormatter can be used. In addition to the format string (which is applied to each message in the batch), there is provision for header and trailer format strings.

When filtering based on logger level and/or handler level is not enough, instances of Filter can be added to both Logger and Handler instances (through their addFilter() method). Before deciding to process a message further, both loggers and handlers consult all their filters for permission. If any filter returns a false value, the message is not processed further.

The basic Filter functionality allows filtering by specific logger name. If this feature is used, messages sent to the named logger and its children are allowed through the filter, and all others dropped.

Exceptions raised during logging

The logging package is designed to swallow exceptions which occur while logging in production. This is so that errors which occur while handling logging events - such as logging misconfiguration, network or other similar errors - do not cause the application using logging to terminate prematurely.

SystemExit and KeyboardInterrupt exceptions are never swallowed. Other exceptions which occur during the emit() method of a Handler subclass are passed to its handleError() method.

The default implementation of handleError() in Handler checks to see if a module-level variable, raiseExceptions, is set. If set, a traceback is printed to sys.stderr. If not set, the exception is swallowed.

Note

The default value of raiseExceptions is True. This is because during development, you typically want to be notified of any exceptions that occur. It's advised that you set raiseExceptions to False for production usage.

Using arbitrary objects as messages

In the preceding sections and examples, it has been assumed that the message passed when logging the event is a string. However, this is not the only possibility. You can pass an arbitrary object as a message, and its __str__() method will be called when the logging system needs to convert it to a string representation. In fact, if you want to, you can avoid computing a string representation altogether - for example, the SocketHandler emits an event by pickling it and sending it over the wire.

Optimization

Formatting of message arguments is deferred until it cannot be avoided. However, computing the arguments passed to the logging method can also be expensive, and you may want to avoid doing it if the logger will just throw away your event. To decide what to do, you can call the isEnabledFor() method which takes a level argument and returns true if the event would be created by the Logger for that level of call. You can write code like this:

if logger.isEnabledFor(logging.DEBUG):
    logger.debug('Message with %s, %s', expensive_func1(),
                                        expensive_func2())

so that if the logger's threshold is set above DEBUG, the calls to expensive_func1() and expensive_func2() are never made.

Note

In some cases, isEnabledFor() can itself be more expensive than you'd like (e.g. for deeply nested loggers where an explicit level is only set high up in the logger hierarchy). In such cases (or if you want to avoid calling a method in tight loops), you can cache the result of a call to isEnabledFor() in a local or instance variable, and use that instead of calling the method each time. Such a cached value would only need to be recomputed when the logging configuration changes dynamically while the application is running (which is not all that common).

There are other optimizations which can be made for specific applications which need more precise control over what logging information is collected. Here's a list of things you can do to avoid processing during logging which you don't need:

What you don't want to collect How to avoid collecting it
Information about where calls were made from. Set logging._srcfile to None. This avoids calling sys._getframe(), which may help to speed up your code in environments like PyPy (which can't speed up code that uses sys._getframe()), if and when PyPy supports Python 3.x.
Threading information. Set logging.logThreads to 0.
Process information. Set logging.logProcesses to 0.

Also note that the core logging module only includes the basic handlers. If you don't import logging.handlers and logging.config, they won't take up any memory.

Voir aussi

Module logging
API reference for the logging module.
Module logging.config
Configuration API for the logging module.
Module logging.handlers
Useful handlers included with the logging module.

A logging cookbook