Une promenade dans le code source – Real Python

By | mai 22, 2019

trouver un expert Python

Le python enregistrement package est un package léger mais extensible permettant de garder une meilleure trace de ce que fait votre propre code. Son utilisation vous donne beaucoup plus de souplesse que de simplement surcharger votre code. impression() appels.

Cependant, Python enregistrement le paquet peut être compliqué à certains endroits. Gestionnaires, enregistreurs, niveaux, espaces de noms, filtres: il n’est pas facile de garder une trace de tous ces éléments et de leur interaction.

Une façon de régler le problème dans votre compréhension de enregistrement est à regarder sous le capot de son code source CPython. Le code Python derrière enregistrement est concis et modulaire, et sa lecture peut vous aider à comprendre aha moment.

Cet article est destiné à compléter le document HOWTO sur la journalisation, ainsi que la journalisation en Python, qui décrit comment utiliser le paquet.

À la fin de cet article, vous serez familiarisé avec les éléments suivants::

  • enregistrement niveaux et comment ils fonctionnent
  • La sécurité des fils par rapport à la sécurité des processus dans enregistrement
  • La conception de enregistrement du point de vue de la POO
  • Connexion dans les bibliothèques vs applications
  • Meilleures pratiques et modèles de conception pour l'utilisation enregistrement

Pour la plupart, nous allons ligne par ligne dans le module principal de Python. enregistrement package afin de construire une image de la façon dont il est aménagé.

Comment suivre

Parce que le enregistrement Le code source étant au cœur de cet article, vous pouvez supposer que tout bloc de code ou lien est basé sur un commit spécifique du référentiel Python 3.7 CPython, à savoir commit d730719. Vous pouvez trouver le enregistrement emballer dans le Lib / répertoire dans la source CPython.

Dans le enregistrement paquet, la plus grande partie du travail lourd se produit dans journalisation / __ init__.py, le fichier dans lequel vous passerez le plus de temps ici:

cpython /
│
├── Lib /
├── enregistrement /
│ ├── __init__.py
│ ├── config.py
│ └── handlers.py
├── ...
├── Modules /
├── Inclure /
...
... [truncated]

Sur ce, entrons.

Préliminaires

Avant d’arriver aux catégories des poids lourds, les cent premières lignes de la __init__.py Présentez quelques concepts subtils mais importants.

Préliminaire n ° 1: un niveau n'est qu'un int!

Des objets comme journalisation.INFO ou journalisation.DEBUG peut sembler un peu opaque. Quelles sont ces variables en interne et comment sont-elles définies?

En fait, les constantes majuscules de Python enregistrement ne sont que des entiers, formant une collection de niveaux numériques ressemblant à des énumérations:

CRITIQUE = 50
FATAL = CRITIQUE
ERREUR = 40
ATTENTION = 30
PRÉVENIR = ATTENTION
INFO = 20
DÉBOGUER = dix
PAS ENCORE DÉFINI = 0

Pourquoi ne pas simplement utiliser les chaînes "INFO" ou "DÉBOGUER"? Les niveaux sont int constantes pour permettre la comparaison simple et non ambiguë d’un niveau à un autre. On leur donne également des noms pour leur donner un sens sémantique. Dire qu'un message a une gravité de 50 peut ne pas être clair immédiatement, mais dire qu'il a un niveau de CRITIQUE vous permet de savoir que vous avez un feu rouge clignotant quelque part dans votre programme.

Maintenant, techniquement, vous pouvez passer juste la str forme d'un niveau dans certains endroits, tels que logger.setLevel ("DEBUG"). En interne, cela appellera _checkLevel (), qui fait finalement un dict rechercher le correspondant int:

_nameToLevel = 
    'CRITIQUE': CRITIQUE,
    'FATAL': FATAL,
    'ERREUR': ERREUR,
    'PRÉVENIR': ATTENTION,
    'ATTENTION': ATTENTION,
    'INFO': INFO,
    'DÉBOGUER': DÉBOGUER,
    'PAS ENCORE DÉFINI': PAS ENCORE DÉFINI,


def _checkLevel(niveau):
    si isinstance(niveau, int):
        VR = niveau
    elif str(niveau) == niveau:
        si niveau ne pas dans _nameToLevel:
            élever ValueError("Niveau inconnu: % r" % niveau)
        VR = _nameToLevel[[[[niveau]
    autre:
        élever Erreur-type("Niveau pas un entier ou une chaîne valide: % r" % niveau)
    revenir VR

Lequel devriez-vous préférer? Je n’ai pas trop d’opinion à ce sujet, mais il est à noter que le enregistrement docs utilise systématiquement le formulaire journalisation.DEBUG plutôt que "DÉBOGUER" ou dix. En outre, en passant le str formulaire n'est pas une option dans Python 2, et certains enregistrement des méthodes telles que logger.isEnabledFor () acceptera seulement un int, pas sa str cousin.

Préliminaire n ° 2: la journalisation est sécurisée pour les threads, mais non pour le processus

Quelques lignes plus bas, vous trouverez le bloc de code court suivant, essentiel pour l’ensemble du package:

importation filetage

_fermer à clé = filetage.RLock()

def _acquireLock():
     si _fermer à clé:
        _fermer à clé.acquérir()

def _releaseLock():
    si _fermer à clé:
        _fermer à clé.Libération()

le _fermer à clé objet est un verrou réentrant qui se trouve dans l'espace de noms global du journalisation / __ init__.py module. Il fait à peu près tous les objets et opérations de l'ensemble enregistrement package thread-safe, permettant aux threads d'effectuer des opérations de lecture et d'écriture sans menace de situation de concurrence critique. Vous pouvez voir dans le code source du module que _acquireLock () et _releaseLock () sont omniprésents au module et à ses classes.

Il ya cependant quelque chose qui n’a pas été pris en compte ici: qu’en est-il de la sécurité des processus? La réponse courte est que le enregistrement le module est ne pas processus sécurisé. Ce n’est pas intrinsèquement une faute de enregistrementEn règle générale, deux processus ne peuvent pas écrire dans le même fichier sans beaucoup d’efforts proactifs de la part du programmeur.

Cela signifie que vous devrez faire attention avant d’utiliser des classes telles que logging.FileHandler avec multitraitement impliqué. Si deux processus veulent lire et écrire simultanément sur le même fichier sous-jacent, vous pouvez rencontrer un bogue méchant à mi-chemin d'une routine longue.

Si vous souhaitez contourner cette limitation, vous trouverez une recette complète dans le livre de recettes officiel Logging. Dans la mesure où cela implique une configuration correcte, une alternative consiste à enregistrer chaque journal de processus dans un fichier séparé en fonction de son ID de processus, que vous pouvez récupérer avec os.getpid ().

Architecture de package: MRO de la journalisation

Maintenant que nous avons couvert quelques codes de configuration préliminaires, examinons de plus près comment enregistrement est aménagé. le enregistrement package utilise une bonne dose de POO et d’héritage. Voici un aperçu de l’ordre de résolution de la méthode (MRO) de certaines des classes les plus importantes du package:

objet
│
├── LogRecord
├── Filtreur
├── ├── enregistreur
│ └── RootLogger
└── └── Gestionnaire
├── StreamHandler
└── └── NullHandler
├── Filtrer
└── Responsable

L’arborescence ci-dessus ne couvre pas toutes les classes du module, mais uniquement celles qui méritent d’être soulignées.

Cette litanie de classes est généralement une source de confusion, car il se passe beaucoup de choses et que tout est dans le jargon. Filtre contre Filtreur? Enregistreur contre Gestionnaire? Il peut être difficile de garder une trace de tout, et encore moins de visualiser comment cela s’agence. Une image vaut mille mots. Voici donc le schéma d’un scénario dans lequel un enregistreur auquel sont associés deux gestionnaires écrit un message de consignation de niveau. journalisation.INFO:

Flux du package de journalisation
Flux d'objets de journalisation (Image: Real Python)

En code Python, tout ce qui précède ressemble à ceci:

importation enregistrement
importation sys

enregistreur = enregistrement.getLogger("Pylog")
enregistreur.setLevel(enregistrement.DÉBOGUER)
h1 = enregistrement.FileHandler(nom de fichier="/tmp/records.log")
h1.setLevel(enregistrement.INFO)
h2 = enregistrement.StreamHandler(sys.stderr)
h2.setLevel(enregistrement.ERREUR)
enregistreur.addHandler(h1)
enregistreur.addHandler(h2)
enregistreur.Info("essai %ré.. %ré.. %ré.. ", 1, 2, 3)

Il existe une carte plus détaillée de ce flux dans le logging HOWTO. Ce qui est montré ci-dessus est un scénario simplifié.

Votre code n'en définit qu'un Enregistreur exemple, enregistreur, avec deux Gestionnaire les instances, h1 et h2.

Quand vous appelez logger.info ("test% d ..% d ..% d ..", 1, 2, 3), la enregistreur objet sert de filtre car il a aussi un niveau associé avec. Ce n’est que si le niveau du message est suffisamment grave que l’enregistreur fera quoi que ce soit avec le message. Parce que l'enregistreur a le niveau DÉBOGUER, et le message porte un plus haut INFO niveau, il obtient le feu vert pour aller de l'avant.

Intérieurement, enregistreur appels logger.makeRecord () mettre la chaîne de message "tester% d ..% d ..% d .." et ses arguments (1, 2, 3) dans une instance de classe de bonne foi d'un LogRecord, qui est juste un conteneur pour le message et ses métadonnées.

le enregistreur objet regarde autour de lui pour ses gestionnaires (instances de Gestionnaire), qui peuvent être liés directement à enregistreur lui-même ou à ses parents (un concept que nous aborderons plus tard). Dans cet exemple, il trouve deux gestionnaires:

  1. Une avec niveau INFO qui vide les données du journal dans un fichier à /tmp/records.log
  2. Celui qui écrit à sys.stderr mais seulement si le message entrant est au niveau ERREUR ou plus

À ce stade, il y a une autre série de tests qui démarre. Parce que le LogRecord et son message ne portent que le niveau INFO, l’enregistrement est écrit dans le gestionnaire 1 (flèche verte), mais pas dans le gestionnaire 2. stderr flux (flèche rouge). Pour les gestionnaires, écrire le LogRecord à leur flux s'appelle émettant il, qui est capturé dans leur .émettre().

Ensuite, disséquons tout d’en haut.

le LogRecord Classe

Qu'est-ce qu'un LogRecord? Lorsque vous enregistrez un message, une instance du LogRecord classe est l'objet que vous envoyez pour être connecté. Il a été créé pour vous par un Enregistreur instance et encapsule toutes les informations pertinentes sur cet événement. En interne, c’est un peu plus qu’une enveloppe autour d’un dict qui contient des attributs pour l'enregistrement. UNE Enregistreur l'instance envoie un LogRecord exemple à zéro ou plus Gestionnaire les instances.

le LogRecord contient des métadonnées, telles que:

  1. Un nom
  2. L'heure de création sous forme d'horodatage Unix
  3. Le message lui-même
  4. Informations sur la fonction utilisée pour l'appel de journalisation

Voici un aperçu des métadonnées qu’elles contiennent, que vous pouvez introspecter en parcourant un logging.error () appel à la pdb module:

>>>

>>> importation enregistrement
>>> importation pdb

>>> def F(X):
...     enregistrement.Erreur("mauvaises vibrations")
...     revenir X / 0
... 
>>> pdb.courir("f (1)")

Après avoir parcouru certaines fonctions de niveau supérieur, vous vous retrouvez à la ligne 1517:

(Pdb) l
1514 exc_info = (type (exc_info), exc_info, exc_info .__ traceback__)
1515 elif not isstance (exc_info, tuple):
1516 exc_info = sys.exc_info ()
1517 enregistrement = self.makeRecord (self.name, level, fn, lno, msg, args,
1518 exc_info, func, extra, sinfo)
1519 -> self.handle (record)
1520
1521 def poignée (self, record):
1522 "" "
1523 Appelez les gestionnaires pour l'enregistrement spécifié.
1524
(Pdb) de pprint import pprint
(Pdb) pprint (vars (record))
'args': (),
    'créé': 1550671851.660067,
    'exc_info': aucun,
    'exc_text': Aucun,
    'nom de fichier': '',
    'funcName': 'f',
    'levelname': 'ERROR',
    'levelno': 40,
    'linéno': 2,
    'module': '',
    'msecs': 660.067081451416,
    'msg': 'mauvaises vibrations',
    'nom': 'racine',
    'chemin': '',
    'process': 2360,
    'processName': 'MainProcess',
    'relativeCreated': 295145.5490589142,
    'stack_info': aucun,
    'thread': 4372293056,
    'threadName': 'MainThread'

UNE LogRecord, en interne, contient une mine de métadonnées utilisées d’une manière ou d’une autre.

Vous aurez rarement besoin de traiter avec un LogRecord directement, depuis le Enregistreur et Gestionnaire fais ça pour toi. Il est toujours utile de savoir quelles informations sont contenues dans un LogRecord, car c’est de là que viennent toutes ces informations utiles, telles que l’horodatage, lorsque vous voyez les messages du journal d’enregistrement.

le Enregistreur et Gestionnaire Des classes

le Enregistreur et Gestionnaire les classes sont à la fois au cœur de la enregistrement fonctionne, et ils interagissent fréquemment les uns avec les autres. UNE Enregistreur, une Gestionnaire, Et LogRecord ont chacun une .niveau associé à eux.

le Enregistreur prend le LogRecord et le passe à la Gestionnaire, mais seulement si le niveau effectif du LogRecord est égal ou supérieur à celui de la Enregistreur. La même chose vaut pour le LogRecord contre Gestionnaire tester. C'est appelé filtrage par niveau, lequel Enregistreur et Gestionnaire mettre en œuvre de manière légèrement différente.

En d'autres termes, un test (au moins) en deux étapes est appliqué avant que le message que vous vous connectez puisse aller n'importe où. Afin d’être entièrement passé d’un enregistreur à un gestionnaire, puis connecté au flux final (qui peut être sys.stdout, un fichier ou un email via SMTP), un LogRecord doit avoir un niveau au moins aussi élevé que tous les deux l'enregistreur et le gestionnaire.

PEP 282 décrit comment cela fonctionne:

Chaque Enregistreur object garde la trace d'un niveau de journalisation (ou seuil) qui l'intéresse, et élimine les requêtes de journalisation inférieures à ce niveau. (La source)

Alors, où se situe ce filtrage en fonction du niveau pour les deux Enregistreur et Gestionnaire?

Pour le Enregistreur classe, c’est une première hypothèse raisonnable que l’enregistreur compare ses .niveau attribuer au niveau de la LogRecordet se faire là-bas. Cependant, c’est un peu plus compliqué que cela.

Le filtrage par niveau pour les enregistreurs a lieu dans .isEnabledFor (), qui appelle à son tour .getEffectiveLevel (). Toujours utilisation logger.getEffectiveLevel () plutôt que de consulter logger.level. La raison a à voir avec l'organisation de Enregistreur objets dans un espace de noms hiérarchique. (Vous en verrez plus tard.)

Par défaut, un Enregistreur par exemple a un niveau de 0 (PAS ENCORE DÉFINI). Cependant, les bûcherons ont aussi enregistreurs parents, dont l’un est l’enregistreur racine, qui sert de parent à tous les autres enregistreurs. UNE Enregistreur va monter dans sa hiérarchie et obtenir son niveau effectif vis-à-vis de sa société mère (qui peut finalement être racine si aucun autre parent n'est trouvé).

C’est là que cela se passe dans le Enregistreur classe:

classe Enregistreur(Filtreur):
    # ...
    def getEffectiveLevel(soi):
        enregistreur = soi
        tandis que enregistreur:
            si enregistreur.niveau:
                revenir enregistreur.niveau
            enregistreur = enregistreur.parent
        revenir PAS ENCORE DÉFINI

    def isEnabledFor(soi, niveau):
        essayer:
            revenir soi._cache[[[[niveau]
        sauf KeyError:
            _acquireLock()
            si soi.directeur.désactiver > = niveau:
                est autorisé = soi._cache[[[[niveau] = Faux
            autre:
                est autorisé = soi._cache[[[[niveau] = niveau > = soi.getEffectiveLevel()
            _releaseLock()
        revenir est autorisé

En conséquence, voici un exemple qui appelle le code source que vous voyez ci-dessus:

>>>

>>> importation enregistrement
>>> enregistreur = enregistrement.getLogger("App")
>>> enregistreur.niveau  # Non!
0
>>> enregistreur.getEffectiveLevel()
30
>>> enregistreur.parent

>>> enregistreur.parent.niveau
30

Voici ce qu’il faut emporter: ne comptez pas sur .niveau. Si vous ne définissez pas explicitement un niveau sur votre enregistreur objet, et vous dépendez de .niveau pour une raison quelconque, votre configuration de journalisation se comportera probablement différemment que prévu.

Qu'en est-il de Gestionnaire? Pour les gestionnaires, la comparaison de niveau est plus simple, bien qu’elle se produise réellement dans .callHandlers () du Enregistreur classe:

classe Enregistreur(Filtreur):
    # ...
    def callHandlers(soi, record):
        c = soi
        a trouvé = 0
        tandis que c:
            pour hdlr dans c.manutentionnaires:
                a trouvé = a trouvé + 1
                si record.niveauno > = hdlr.niveau:
                    hdlr.manipuler(record)

Pour une donnée LogRecord par exemple (du nom record ci-dessus), un enregistreur vérifie auprès de chacun de ses gestionnaires enregistrés et effectue une vérification rapide de la .niveau attribut de ce Gestionnaire exemple. Si la .levelno du LogRecord est supérieur ou égal à celui du gestionnaire, c'est seulement à ce moment-là que l'enregistrement est transmis. Un docstring dans enregistrement appelle cela «émettre sous condition[ting] l'enregistrement de journalisation spécifié. ”

L'attribut le plus important pour un Gestionnaire instance de sous-classe est son .courant attribut. Ceci est la destination finale où les journaux sont écrits et peut être à peu près tout objet de type fichier. Voici un exemple avec io.StringIO, qui est un flux en mémoire (tampon) pour les E / S de texte.

Tout d'abord, mettre en place un Enregistreur exemple avec un niveau de DÉBOGUER. Vous verrez que, par défaut, il n’a pas de gestionnaire direct:

>>>

>>> importation io
>>> importation enregistrement
>>> enregistreur = enregistrement.getLogger("abc")
>>> enregistreur.setLevel(enregistrement.DÉBOGUER)
>>> impression(enregistreur.manutentionnaires)
[]

Ensuite, vous pouvez sous-classe logging.StreamHandler pour faire le .affleurer() appeler un no-op. Nous voudrions faire partir sys.stderr ou sys.stdout, mais pas le tampon en mémoire dans ce cas:

classe IOHandler(enregistrement.StreamHandler):
    def affleurer(soi):
        passer  # No-op

Maintenant, déclarez l’objet tampon lui-même et attachez-le en tant que .courant pour votre gestionnaire personnalisé avec un niveau de INFO, puis attachez ce gestionnaire à l'enregistreur:

>>>

>>> courant = io.StringIO()
>>> h = IOHandler(courant)
>>> h.setLevel(enregistrement.INFO)
>>> enregistreur.addHandler(h)

>>> enregistreur.déboguer("informations superflues")
>>> enregistreur.Attention("tu as été prévenu")
>>> enregistreur.critique("SOS")

>>> essayer:
...     impression(courant.getvalue())
... enfin:
...     courant.Fermer()
... 
tu as été prévenu
SOS

Ce dernier morceau est une autre illustration du filtrage par niveau.

Trois messages avec des niveaux DÉBOGUER, ATTENTION, et CRITIQUE sont passés à travers la chaîne. Au début, on dirait qu’ils ne vont nulle part, mais deux le font. Tous les trois sortent des portes de enregistreur (qui a le niveau DÉBOGUER).

Cependant, seulement deux d’entre eux sont émis par le gestionnaire car il a un niveau de INFO, qui dépasse DÉBOGUER. Enfin, vous obtenez le contenu entier du tampon en tant que str et fermez le tampon pour libérer explicitement les ressources système.

le Filtre et Filtreur Des classes

Ci-dessus, nous avons posé la question suivante: «Où se produit le filtrage par niveau?». En répondant à cette question, il est facile de se laisser distraire par la Filtre et Filtreur Des classes. Paradoxalement, le filtrage par niveau pour Enregistreur et Gestionnaire cas se produit sans l'aide de l'un des Filtre ou Filtreur Des classes.

Filtre et Filtreur sont conçus pour vous permettre d'ajouter des filtres basés sur des fonctions supplémentaires en plus du filtrage par niveau effectué par défaut. J'aime penser à ça à la carte filtration.

Filtreur est la classe de base pour Enregistreur et Gestionnaire parce que ces deux classes peuvent recevoir des filtres personnalisés supplémentaires que vous spécifiez. Vous ajoutez des instances de Filtre à eux avec logger.addFilter () ou handler.addFilter (), lequel est quoi auto-filtres fait référence à la méthode suivante:

classe Filtreur(objet):
    # ...
    def filtre(soi, record):
        VR = Vrai
        pour F dans soi.filtres:
            si hasattr(F, 'filtre'):
                résultat = F.filtre(record)
            autre:
                résultat = F(record)
            si ne pas résultat:
                VR = Faux
                Pause
        revenir VR

Donné un record (Qui est un LogRecord exemple), .filtre() résultats Vrai ou Faux selon que cet enregistrement obtient l’accord des filtres de cette classe.

Voici .manipuler() à son tour, pour le Enregistreur et Gestionnaire Des classes:

classe Enregistreur(Filtreur):
    # ...
    def manipuler(soi, record):
        si (ne pas soi.désactivée) et soi.filtre(record):
            soi.callHandlers(record)

# ...

classe Gestionnaire(Filterer):
    # ...
    def manipuler(soi, record):
        rv = soi.filtre(record)
        si VR:
            soi.acquérir()
            essayer:
                soi.émettre(record)
            enfin:
                soi.Libération()
        revenir rv

Ni enregistreur ni maître avec des filtres supplémentaires par défaut, mais voici un exemple rapide de la façon dont vous pouvez en ajouter un:

>>>

>>> importation enregistrement

>>> enregistreur = enregistrement.getLogger("rp")
>>> enregistreur.setLevel(enregistrement.INFO)
>>> enregistreur.addHandler(enregistrement.StreamHandler())
>>> enregistreur.filtres  # Initialement vide
[]
>>> classe ShortMsgFilter(enregistrement.Filtre):
...     "" "Autoriser uniquement les enregistrements contenant des messages longs (> 25 caractères)." ""
...     def filtre(soi, record):
...         msg = record.msg
...         si isinstance(msg, str):
...             revenir len(msg) > 25
...         revenir Faux
... 
>>> enregistreur.addFilter(ShortMsgFilter())
>>> enregistreur.filtres
[[[[<__main__.ShortMsgFilter object at 0x10c28b208>]
>>> enregistreur.Info("Reeeeaaaaallllllly long message")  # Longueur: 31
Reeeeaaaaallllllly long message
>>> enregistreur.Info("Terminé")  # Longueur: <25, pas de sortie

Ci-dessus, vous définissez une classe ShortMsgFilter et remplacer sa .filtre(). Dans .addHandler (), vous pouvez aussi simplement passer un appelable, tel qu'une fonction ou un lambda ou une classe qui définit .__appel__().

le Directeur Classe

Il y a un autre acteur dans les coulisses de enregistrement qui mérite d’être abordé: le Directeur classe. Ce qui compte le plus n’est pas le Directeur classe, mais une seule instance de celle-ci qui agit en tant que conteneur pour la hiérarchie croissante des enregistreurs définis entre les packages. Vous verrez dans la section suivante qu’une seule instance de cette classe est essentielle pour coller le module ensemble et permettre à ses parties de communiquer entre elles.

Le très important enregistreur racine

Quand cela vient à Enregistreur cas, on se démarque. Il s’appelle le root logger:

classe RootLogger(Enregistreur):
    def __init__(soi, niveau):
        enregistreur.__init__(soi, "racine", niveau)

# ...

racine = RootLogger(ATTENTION)
enregistreur.racine = racine
Enregistreur.directeur = Directeur(Enregistreur.racine)

Les trois dernières lignes de ce bloc de code sont l’une des astuces ingénieuses employées par le enregistrement paquet. Voici quelques points:

  • L'enregistreur racine est juste un objet Python sans fioritures avec l'identifiant racine. Il a un niveau de logging.AVERTISSEMENT et un .prénom de "racine". Aussi loin que la classe RootLogger est concerné, ce nom unique est tout ce qui fait sa particularité.

  • le racine l'objet à son tour devient un attribut class pour le enregistreur classe. Cela signifie que toutes les instances de enregistreur, et le enregistreur classe elle-même, tous ont un .racine attribut qui est le logger racine. C’est un autre exemple de modèle semblable à un singleton appliqué dans le enregistrement paquet.

  • UNE Directeur l'instance est définie comme .directeur attribut de classe pour enregistreur. Cela finit par jouer dans logging.getLogger ("nom"). le .directeur fait toute la facilitation de la recherche d'enregistreurs existants avec le nom "prénom" et les créer s'ils n'existent pas.

La hiérarchie de l'enregistreur

Tout est un enfant de racine dans l'espace de noms de l'enregistreur, et je veux dire tout. Cela inclut les enregistreurs que vous spécifiez vous-même ainsi que ceux des bibliothèques tierces que vous importez.

Rappelez-vous plus tôt comment le .getEffectiveLevel () pour notre enregistreur cas était 30 (ATTENTION) même si nous ne l’avions pas explicitement définie? C’est parce que le consignateur racine se situe au sommet de la hiérarchie et que son niveau est un repli si un consignateur imbriqué a un niveau nul. PAS ENCORE DÉFINI:

>>>

>>> racine = enregistrement.getLogger()  # Ou getLogger ("")
>>> racine

>>> racine.parent est Aucun
Vrai
>>> racine.racine est racine  # Auto-référentiel
Vrai
>>> racine est enregistrement.racine
Vrai
>>> racine.getEffectiveLevel()
30

La même logique s’applique à la recherche des gestionnaires d’un enregistreur. La recherche est en réalité une recherche inversée dans l’arbre des parents d’un enregistreur.

Une conception multi-handler

La hiérarchie des enregistreurs peut sembler nette en théorie, mais à quel point est-elle bénéfique en pratique?

Faisons une pause pour explorer la enregistrement codez et tentez d’écrire notre propre mini-application – une application qui tire parti de la hiérarchie des enregistreurs de manière à réduire le code standard et à garder les éléments évolutifs si la base de code du projet se développe.

Voici la structure du projet:

projet/
│
└── projet /
    ├── __init__.py
    ├── utils.py
    └── base.py

Ne vous inquiétez pas des fonctions principales de l’application dans utils.py et base.py. Ce à quoi nous prêtons plus d’attention, c’est l’interaction dans enregistrement objets entre les modules dans projet/.

Dans ce cas, supposons que vous souhaitiez concevoir une configuration de journalisation à plusieurs volets:

  • Chaque module reçoit un enregistreur avec plusieurs gestionnaires.

  • Certains des gestionnaires sont partagés entre différents enregistreur instances dans différents modules. Ces gestionnaires se soucient uniquement du filtrage par niveau, pas du module d'où provient l'enregistrement du journal. Il y a un gestionnaire pour DÉBOGUER messages, un pour INFO, un pour ATTENTION, etc.

  • Chaque enregistreur est également lié à un autre gestionnaire supplémentaire qui ne reçoit que LogRecord cas de cette seule enregistreur. Vous pouvez appeler cela un gestionnaire de fichiers basé sur un module.

Visuellement, notre objectif est de ressembler à ceci:

Configuration de la journalisation à plusieurs volets
Une conception de journalisation à plusieurs volets (Image: Real Python)

Les deux objets turquoise sont des exemples de Enregistreur, établi avec logging.getLogger (__ name__) pour chaque module dans un package. Tout le reste est un Gestionnaire exemple.

L’idée qui sous-tend cette conception est qu’elle est parfaitement compartimentée. Vous pouvez regarder facilement à des messages provenant d'un seul enregistreur, ou regarder les messages d'un certain niveau et au-dessus provenant de tout enregistreur ou d'un module.

Les propriétés de la hiérarchie de l'enregistreur rendent approprié pour la mise en place de cette disposition enregistreur-gestionnaire pluridimensionnelle. Qu'est-ce que ça veut dire? Voici une explication concise de la documentation de Django:

Pourquoi la hiérarchie est-elle importante? Eh bien, parce que les enregistreurs peuvent être configurés pour propager leurs appels de journalisation à leurs parents. De cette façon, vous pouvez définir un ensemble unique de gestionnaires à la racine d'un arbre enregistreur, et capturer tous les appels de l'exploitation forestière dans la sous-arborescence des enregistreurs. Un gestionnaire de journalisation défini dans le projet L’espace de noms intercepte tous les messages de journalisation émis sur le projet.intéressant et projet.intéressant.truff bûcherons. (La source)

Le terme propager se réfère à la façon dont un bûcheron continue à marcher dans sa chaîne de parents à la recherche de gestionnaires. le .propager attribut est Vrai pour un Enregistreur exemple par défaut:

>>>

>>> enregistreur = enregistrement.getLogger(__prénom__)
>>> enregistreur.propager
Vrai

Dans .callHandlers (), si propager est Vrai, chaque parent successif est réaffecté à la variable locale c jusqu'à épuisement de la hiérarchie:

classe Enregistreur(Filtreur):
    # ...
    def callHandlers(soi, record):
        c = soi
        a trouvé = 0
        tandis que c:
            pour hdlr dans c.manutentionnaires:
                a trouvé = a trouvé + 1
                si record.niveauno > = hdlr.niveau:
                    hdlr.manipuler(record)
            si ne pas c.propager:
                c = Aucun
            autre:
                c = c.parent

Voici ce que cela signifie: parce que le __prénom__ dunder variable dans un paquet __init__.py module est simplement le nom du paquet, un enregistreur, il devient parent à tous les enregistreurs présents dans d'autres modules dans le même emballage.

Voici le résultat .prénom attributs d'attribution à enregistreur avec logging.getLogger (__ name__):

Module .prénom Attribut
projet / __ init__.py 'projet'
project / utils.py 'project.utils'
projet / base.py 'project.base'

Parce que le 'project.utils' et 'project.base' les bûcherons sont des enfants de 'projet', ils verrouillent non seulement leurs propres gestionnaires directs, mais également les gestionnaires auxquels ils sont attachés. 'projet'.

Construisons les modules. Vient en premier __init__.py:

# __init__.py
importation enregistrement

enregistreur = enregistrement.getLogger(__prénom__)
enregistreur.setLevel(enregistrement.DÉBOGUER)

niveaux = ("DÉBOGUER", "INFO", "ATTENTION", "ERREUR", "CRITIQUE")
pour niveau dans niveaux:
    gestionnaire = enregistrement.FileHandler(F"/Tmp/level-level.lower().log")
    gestionnaire.setLevel(getattr(enregistrement, niveau))
    enregistreur.addHandler(gestionnaire)

def add_module_handler(enregistreur, niveau=enregistrement.DÉBOGUER):
    gestionnaire = enregistrement.FileHandler(
        F"/ Tmp / module- logger.name.replace (, '' '-') log."
    )
    gestionnaire.setLevel(niveau)
    enregistreur.addHandler(gestionnaire)

Ce module est importé lorsque le projet le paquet est importé. Vous ajoutez un gestionnaire pour chaque niveau de DÉBOGUER à travers CRITIQUE, puis attachez-le à un seul enregistreur situé en haut de la hiérarchie.

Vous définissez également une fonction utilitaire qui en ajoute une de plus. FileHandler à un bûcheron, où le nom de fichier du gestionnaire correspond au nom du module où le consignateur est défini. (Cela suppose que l’enregistreur est défini avec __prénom__.)

Vous pouvez ensuite ajouter une configuration minimale d’enregistreur standard dans base.py et utils.py. Notez que vous devez uniquement ajouter un gestionnaire supplémentaire avec add_module_handler () de __init__.py. Vous ne devez pas vous inquiéter au sujet des gestionnaires orientés niveau parce qu'ils sont déjà ajoutés à leur enregistreur de parent nommé 'projet':

# base.py
importation enregistrement

de projet importation add_module_handler

enregistreur = enregistrement.getLogger(__prénom__)
add_module_handler(enregistreur)

def func1():
    enregistreur.déboguer("debug appelé depuis base.func1 ()")
    enregistreur.critique("critique appelé depuis base.func1 ()")

Voici utils.py:

# utils.py
importation enregistrement

de projet importation add_module_handler

enregistreur = enregistrement.getLogger(__prénom__)
add_module_handler(enregistreur)

def func2():
    enregistreur.déboguer("debug appelé depuis utils.func2 ()")
    enregistreur.critique("critique appelé depuis utils.func2 ()")

Voyons comment tout cela fonctionne à partir d’une nouvelle session Python:

>>>

>>> de empreinte importation empreinte
>>> importation projet
>>> de projet importation base, utils

>>> projet.enregistreur

>>> base.enregistreur, utils.enregistreur
(, )
>>> base.enregistreur.manutentionnaires
[[[[]
>>> empreinte(base.enregistreur.parent.manutentionnaires)
[[[[,
 ,
 ,
 ,
 ]
>>> base.func1()
>>> utils.func2()

Vous verrez dans les fichiers journaux résultants que notre système de filtration fonctionne comme prévu. gestionnaires orientés module directement un enregistreur à un fichier spécifique, alors que handlers orienté niveau loggers multiples directs à un autre fichier:

$ cat /tmp/level-debug.log 
débogage appelé depuis base.func1 ()
critique appelé depuis base.func1 ()
débogage appelé depuis utils.func2 ()
critique appelé depuis utils.func2 ()

$ cat /tmp/level-critical.log 
critique appelé depuis base.func1 ()
critique appelé depuis utils.func2 ()

$ cat /tmp/module-project-base.log
débogage appelé depuis base.func1 ()
critique appelé depuis base.func1 ()

$ cat /tmp/module-project-utils.log 
débogage appelé depuis utils.func2 ()
critique appelé depuis utils.func2 ()

Un inconvénient à noter est que cette conception introduit beaucoup de redondance. Un LogRecord instance peut aller à pas moins de six fichiers. C'est également une quantité non négligeable de fichier E / S qui peut ajouter dans une application critique la performance.

Maintenant que vous avez vu un exemple pratique, nous allons changer de vitesse et plonger dans une source possible de confusion dans enregistrement.

Le dilemme «Pourquoi mon message de journal n’est-il pas allé n'importe où?

Il y a deux situations courantes avec enregistrement quand il est facile de se faire prendre:

  1. Vous avez enregistré un message qui, apparemment, n’est allé nulle part, et vous ne savez pas pourquoi.
  2. Au lieu d'être supprimé, un message de journal est apparu à un endroit inattendu.

Chacun de ceux-ci a une raison ou deux généralement associée à cela.

Vous avez enregistré un message qui, apparemment, n’est allé nulle part, et vous ne savez pas pourquoi.

N’oubliez pas que le efficace niveau d’un enregistreur pour lequel vous ne définissez pas autrement un niveau personnalisé est ATTENTION, parce qu’un enregistreur gravit la hiérarchie jusqu’à ce qu’il trouve l’enregistreur racine avec son propre ATTENTION niveau:

>>>

>>> importation enregistrement
>>> enregistreur = enregistrement.getLogger("xyz")
>>> enregistreur.déboguer("info engourdissant ici")
>>> enregistreur.critique("l'orage arrive")
l'orage arrive

En raison de ce défaut, le .déboguer() L'appel ne va nulle part.

Au lieu d'être supprimé, un message de journal est apparu à un endroit inattendu.

Quand vous avez défini votre enregistreur ci-dessus, vous n’y avez ajouté aucun gestionnaire. Alors, pourquoi écrit-il sur la console?

La raison en est que enregistrement utilise sournoisement un gestionnaire appelé dernier recours qui écrit à sys.stderr si aucun autre gestionnaire n'est trouvé:

classe _StderrHandler(StreamHandler):
    # ...
    @propriété
    def courant(soi):
        revenir sys.stderr

_defaultLastResort = _StderrHandler(ATTENTION)
dernier recours = _defaultLastResort

Cela commence quand un enregistreur va trouver ses gestionnaires:

classe Enregistreur(Filtreur):
    # ...
    def callHandlers(soi, record):
        c = soi
        a trouvé = 0
        tandis que c:
            pour hdlr dans c.manutentionnaires:
                a trouvé = a trouvé + 1
                si record.niveauno > = hdlr.niveau:
                    hdlr.manipuler(record)
            si ne pas c.propager:
                c = Aucun
            autre:
                c = c.parent
        si (a trouvé == 0):
            si dernier recours:
                si record.niveauno > = dernier recours.niveau:
                     dernier recours.manipuler(record)

Si l'enregistreur donne sur la recherche de gestionnaires (ses deux propres gestionnaires directs et les attributs des enregistreurs de parents), il prend la dernier recours gestionnaire et utilise cela.

Il y a un autre détail subtil à connaître. Cette section a largement parlé des méthodes d'instance (méthodes qu'une classe définit) plutôt que les fonctions au niveau du module de enregistrement paquet portant le même nom.

Si vous utilisez les fonctions, telles que logging.info () plutôt que logger.info (), alors quelque chose de légèrement différent se produit en interne. Les appels de fonction logging.basicConfig (), qui ajoute un StreamHandler qui écrit à sys.stderr. Au final, le comportement est pratiquement le même:

>>>

>>> importation enregistrement
>>> racine = enregistrement.getLogger("")
>>> racine.manutentionnaires
[]
>>> racine.hasHandlers()
Faux
>>> enregistrement.basicConfig()
>>> racine.manutentionnaires
[<StreamHandler[<StreamHandler[ (NOTSET)>]
>>> racine.hasHandlers()
Vrai

Profitant du formatage paresseux

Il est temps de changer de vitesse et regarder de plus près comment les messages se sont joints à leurs données. Bien qu’il ait été remplacé par str.format () et f-cordes, vous avez probablement utilisé la mise en forme pour cent de style de Python pour faire quelque chose comme ceci:

>>>

>>> impression("Itérer est % s, récidiver % s" % ("Humain", "Divin"))
Itérer est humain, réciter divin

En conséquence, vous pourriez être tenté de faire la même chose dans un enregistrement appel:

>>>

>>> # Mal! Découvrez une alternative plus efficace ci-dessous.
>>> enregistrement.Attention("Itérer est % s, récidiver % s" % ("Humain", "Divin"))
ATTENTION: root: Itérer est humain, récidiver divin

Ceci utilise la chaîne de format entière et ses arguments comme msg argument à logging.warning ().

Voici l'alternative recommandée, directement de la enregistrement docs:

>>>

>>> # Mieux: le formatage ne se produit pas avant que cela soit vraiment nécessaire.
>>> enregistrement.Attention("Itérer est % s, récidiver % s", "Humain", "Divin")
ATTENTION: root: Itérer est humain, récidiver divin

Ça fait un peu bizarre, non? Cela semble défier les conventions de la façon dont la mise en forme de chaîne de style pour cent fonctionne, mais il est un appel de fonction plus efficace, car la chaîne de format sera formaté paresseusement plutôt que avidement. Voici ce que cela signifie.

La signature de méthode pour Logger.warning () ressemble à ça:

def Attention(soi, msg, *args, **Kwargs)

Il en va de même pour les autres méthodes, telles que .déboguer(). Quand vous appelez warning("To iterate is %s, to recurse %s", "human", "divine"), both "human" et "divine" get caught as *args and, within the scope of the method’s body, args est égal à ("human", "divine").

Contrast this to the first call above:

enregistrement.Attention("To iterate is %s, to recurse %s" % ("human", "divine"))

In this form, everything in the parentheses gets immediately merged together into "To iterate is human, to recurse divine" and passed as msg, while args is an empty tuple.

Why does this matter? Repeated logging calls can degrade runtime performance slightly, but the enregistrement package does its very best to control that and keep it in check. By not merging the format string with its arguments right away, enregistrement is delaying the string formatting until the LogRecord is requested by a Handler.

This happens in LogRecord.getMessage(), so only after enregistrement deems that the LogRecord will actually be passed to a handler does it become its fully merged self.

All that is to say that the enregistrement package makes some very fine-tuned performance optimizations in the right places. This may seem like minutia, but if you’re making the same logging.debug() call a million times inside a loop, and the args are function calls, then the lazy nature of how enregistrement does string formatting can make a difference.

Before doing any merging of msg et args, une Logger instance will check its .isEnabledFor() to see if that merging should be done in the first place.

Functions vs Methods

Towards the bottom of logging/__init__.py sit the module-level functions that are advertised up front in the public API of enregistrement. You already saw the Logger methods such as .debug(), .info(), et .warning(). The top-level functions are wrappers around the corresponding methods of the same name, but they have two important features:

  1. They always call their corresponding method from the root logger, racine.

  2. Before calling the root logger methods, they call logging.basicConfig() with no arguments if racine doesn’t have any handlers. As you saw earlier, it is this call that sets a sys.stdout handler for the root logger.

For illustration, here’s logging.error():

def Erreur(msg, *args, **kwargs):
    si len(racine.manutentionnaires) == 0:
        basicConfig()
    racine.Erreur(msg, *args, **kwargs)

You’ll find the same pattern for logging.debug(), logging.info(), and the others as well. Tracing the chain of commands is interesting. Eventually, you’ll end up at the same place, which is where the internal Logger._log() is called.

The calls to debug(), info(), warning(), and the other level-based functions all route to here. _log() primarily has two purposes:

  1. Appel self.makeRecord(): Make a LogRecord instance from the msg and other arguments you pass to it.

  2. Appel self.handle(): This determines what actually gets done with the record. Where does it get sent? Does it make it there or get filtered out?

Here’s that entire process in one diagram:

Logging function call stack
Internals of a logging call (Image: Real Python)

You can also trace the call stack with pdb.

>>>

>>> importation enregistrement
>>> importation pdb
>>> pdb.courir('logging.warning("%s-%s", "uh", "oh")')
> (1)()
(Pdb) s
--Call--
> lib/python3.7/logging/__init__.py(1971)warning()
-> def warning(msg, *args, **kwargs):
(Pdb) s
> lib/python3.7/logging/__init__.py(1977)warning()
-> if len(root.handlers) == 0:
(Pdb) unt
> lib/python3.7/logging/__init__.py(1978)warning()
-> basicConfig()
(Pdb) unt
> lib/python3.7/logging/__init__.py(1979)warning()
-> root.warning(msg, *args, **kwargs)
(Pdb) s
--Call--
> lib/python3.7/logging/__init__.py(1385)warning()
-> def warning(self, msg, *args, **kwargs):
(Pdb) l
1380             logger.info("Houston, we have a %s", "interesting problem", exc_info=1)
1381             """
1382             if self.isEnabledFor(INFO):
1383                 self._log(INFO, msg, args, **kwargs)
1384     
1385 ->        def warning(self, msg, *args, **kwargs):
1386             """
1387             Log 'msg % args' with severity 'WARNING'.
1388     
1389             To pass exception information, use the keyword argument exc_info with
1390             a true value, e.g.
(Pdb) s
> lib/python3.7/logging/__init__.py(1394)warning()
-> if self.isEnabledFor(WARNING):
(Pdb) unt
> lib/python3.7/logging/__init__.py(1395)warning()
-> self._log(WARNING, msg, args, **kwargs)
(Pdb) s
--Call--
> lib/python3.7/logging/__init__.py(1496)_log()
-> def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False):
(Pdb) s
> lib/python3.7/logging/__init__.py(1501)_log()
-> sinfo = None
(Pdb) unt 1517
> lib/python3.7/logging/__init__.py(1517)_log()
-> record = self.makeRecord(self.name, level, fn, lno, msg, args,
(Pdb) s
> lib/python3.7/logging/__init__.py(1518)_log()
-> exc_info, func, extra, sinfo)
(Pdb) s
--Call--
> lib/python3.7/logging/__init__.py(1481)makeRecord()
-> def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
(Pdb) p name
'root'
(Pdb) p level
30
(Pdb) p msg
'%s-%s'
(Pdb) p args
('uh', 'oh')
(Pdb) up
> lib/python3.7/logging/__init__.py(1518)_log()
-> exc_info, func, extra, sinfo)
(Pdb) unt
> lib/python3.7/logging/__init__.py(1519)_log()
-> self.handle(record)
(Pdb) n
WARNING:root:uh-oh

What Does getLogger() Really Do?

Also hiding in this section of the source code is the top-level getLogger(), which wraps Logger.manager.getLogger():

def getLogger(prénom=Aucun):
    si prénom:
        revenir Logger.directeur.getLogger(prénom)
    autre:
        revenir racine

This is the entry point for enforcing the singleton logger design:

  • If you specify a prénom, then the underlying .getLogger() fait un dict lookup on the string prénom. What this comes down to is a lookup in the loggerDict de logging.Manager. This is a dictionary of all registered loggers, including the intermediate PlaceHolder instances that are generated when you reference a logger far down in the hierarchy before referencing its parents.

  • Autrement, racine is returned. Il n'y a qu'un seul racine—the instance of RootLogger discussed above.

This feature is what lies behind a trick that can let you peek into all of the registered loggers:

>>>

>>> importation enregistrement
>>> enregistrement.Logger.directeur.loggerDict


>>> de pprint importation pprint
>>> importation asyncio
>>> pprint(enregistrement.Logger.directeur.loggerDict)
'asyncio': ,
    'concurrent': ,
    'concurrent.futures': 

Whoa, hold on a minute. What’s happening here? It looks like something changed internally to the enregistrement package as a result of an import of another library, and that’s exactly what happened.

Firstly, recall that Logger.manager is a class attribute, where an instance of Directeur is tacked onto the Logger classe. le directeur is designed to track and manage all of the singleton instances of Logger. These are housed in .loggerDict.

Now, when you initially import enregistrement, this dictionary is empty. But after you import asyncio, the same dictionary gets populated with three loggers. This is an example of one module setting the attributes of another module in-place. Sure enough, inside of asyncio/log.py, you’ll find the following:

importation enregistrement

enregistreur = enregistrement.getLogger(__package__)  # "asyncio"

The key-value pair is set in Logger.getLogger() de sorte que la directeur can oversee the entire namespace of loggers. This means that the object asyncio.log.logger gets registered in the logger dictionary that belongs to the enregistrement paquet. Something similar happens in the concurrent.futures package as well, which is imported by asyncio.

You can see the power of the singleton design in an equivalence test:

>>>

>>> obj1 = enregistrement.getLogger("asyncio")
>>> obj2 = enregistrement.Logger.directeur.loggerDict[[[["asyncio"]
>>> obj1 est obj2
Vrai

This comparison illustrates (glossing over a few details) what getLogger() ultimately does.

Library vs Application Logging: What Is NullHandler?

That brings us to the final hundred or so lines in the logging/__init__.py source, where NullHandler is defined. Here’s the definition in all its glory:

classe NullHandler(Handler):
    def manipuler(soi, record):
        passer

    def émettre(soi, record):
        passer

    def createLock(soi):
        soi.fermer à clé = Aucun

le NullHandler is all about the distinctions between logging in a library versus an application. Let’s see what that means.

UNE bibliothèque is an extensible, generalizable Python package that is intended for other users to install and set up. It is built by a developer with the express purpose of being distributed to users. Examples include popular open-source projects like NumPy, dateutil, et cryptographie.

Un application (or app, or program) is designed for a more specific purpose and a much smaller set of users (possibly just one user). It’s a program or set of programs highly tailored by the user to do a limited set of things. An example of an application is a Django app that sits behind a web page. Applications commonly use (importation) libraries and the tools they contain.

When it comes to logging, there are different best practices in a library versus an app.

That’s where NullHandler fits in. It’s basically a do-nothing stub class.

If you’re writing a Python library, you really need to do this one minimalist piece of setup in your package’s __init__.py:

# Place this in your library's uppermost `__init__.py`
# Nothing else!

importation enregistrement

enregistrement.getLogger(__name__).addHandler(NullHandler())

This serves two critical purposes.

Firstly, a library logger that is declared with logger = logging.getLogger(__name__) (without any further configuration) will log to sys.stderr by default, even if that’s not what the end user wants. This could be described as an opt-out approach, where the end user of the library has to go in and disable logging to their console if they don’t want it.

Common wisdom says to use an opt-in approach instead: don’t emit any log messages by default, and let the end users of the library determine if they want to further configure the library’s loggers and add handlers to them. Here’s that philosophy worded more bluntly by the author of the enregistrement package, Vinay Sajip:

A third party library which uses enregistrement should not spew logging output by default which may not be wanted by a developer/user of an application which uses it. (Source)

This leaves it up to the library user, not library developer, to incrementally call methods such as logger.addHandler() ou logger.setLevel().

The second reason that NullHandler exists is more archaic. In Python 2.7 and earlier, trying to log a LogRecord from a logger that has no handler set would raise a warning. Adding the no-op class NullHandler will avert this.

Here’s what specifically happens in the line logging.getLogger(__name__).addHandler(NullHandler()) from above:

  1. Python gets (creates) the Logger instance with the same name as your package. If you’re designing the calcul package, within __init__.py, puis __name__ will be equal to 'calculus'.

  2. UNE NullHandler instance gets attached to this logger. That means that Python will not default to using the lastResort handler.

Keep in mind that any logger created in any of the other .py modules of the package will be children of this logger in the logger hierarchy and that, because this handler also belongs to them, they won’t need to use the lastResort handler and won’t default to logging to standard error (stderr).

As a quick example, let’s say your library has the following structure:

calculus/
│
├── __init__.py
└── integration.py

Dans integration.py, as the library developer you are free to do the following:

# calculus/integration.py
importation enregistrement

enregistreur = enregistrement.getLogger(__name__)

def func(X):
    enregistreur.Attention("Look!")
    # Do stuff
    revenir Aucun

Now, a user comes along and installs your library from PyPI via pip install calculus. They use from calculus.integration import func in some application code. This user is free to manipulate and configure the enregistreur object from the library like any other Python object, to their heart’s content.

What Logging Does With Exceptions

One thing that you may be wary of is the danger of exceptions that stem from your calls to enregistrement. If you have a logging.error() call that is designed to give you some more verbose debugging information, but that call itself for some reason raises an exception, that would be the height of irony, right?

Cleverly, if the enregistrement package encounters an exception that has to do with logging itself, then it will print the traceback, but not raise the exception itself.

Here’s an example that deals with a common typo: passing two arguments to a format string that is only expecting one argument. The important distinction is that what you see below is ne pas an exception being raised, but rather a prettified printed traceback of the internal exception, which itself was suppressed:

>>>

>>> enregistrement.critique("This %s    has too many arguments", "msg", "other")
--- Logging error ---
Traceback (dernier appel le plus récent):
  Fichier "lib/python3.7/logging/__init__.py", line 1034, in émettre
    msg = soi.format(record)
  Fichier "lib/python3.7/logging/__init__.py", line 880, in format
    revenir fmt.format(record)
  Fichier "lib/python3.7/logging/__init__.py", line 619, in format
    record.message = record.getMessage()
  Fichier "lib/python3.7/logging/__init__.py", line 380, in getMessage
    msg = msg % soi.args
TypeError: tous les arguments ne sont pas convertis lors du formatage de chaîne
Call stack:
        File "", line 1, in 
Message: 'This %s has too many arguments'
Arguments: ('msg', 'other')

This lets your program gracefully carry on with its actual program flow. The rationale is that you wouldn’t want an uncaught exception to come from a enregistrement call itself and stop a program dead in its tracks.

Tracebacks can be messy, but this one is informative and relatively straightforward. What enables the suppression of exceptions related to enregistrement est Handler.handleError(). When the handler calls .emit(), which is the method where it attempts to log the record, it falls back to .handleError() if something goes awry. Here’s the implementation of .emit() pour le StreamHandler class:

def émettre(soi, record):
    essayer:
        msg = soi.format(record)
        courant = soi.courant
        courant.écrire(msg + soi.terminator)
        soi.affleurer()
    sauf Exception:
        soi.handleError(record)

Any exception related to the formatting and writing gets caught rather than being raised, and handleError gracefully writes the traceback to sys.stderr.

Logging Python Tracebacks

Speaking of exceptions and their tracebacks, what about cases where your program encounters them but should log the exception and keep chugging along in its execution?

Let’s walk through a couple of ways to do this.

Here’s a contrived example of a lottery simulator using code that isn’t Pythonic on purpose. You’re developing an online lottery game where users can wager on their lucky number:

importation au hasard

classe Lottery(objet):
    def __init__(soi, n):
        soi.n = n

    def make_tickets(soi):
        pour je dans intervalle(soi.n):
            rendement je

    def dessiner(soi):
        bassin = soi.make_tickets()
        au hasard.mélanger(bassin)
        revenir suivant(bassin)

Behind the frontend application sits the critical code below. You want to make sure that you keep track of any errors caused by the site that may make a user lose their money. The first (suboptimal) way is to use logging.error() and log the str form of the exception instance itself:

essayer:
    lucky_number = int(contribution("Enter your ticket number: "))
    tiré = Lottery(n=20).dessiner()
    si lucky_number == tiré:
        impression("Winner chicken dinner!")
sauf Exception comme e:
    # NOTE: See below for a better way to do this.
    enregistrement.Erreur("Could not draw ticket: %s", e)

This will only get you the actual exception message, rather than the traceback. You check the logs on your website’s server and find this cryptic message:

ERROR:root:Could not draw ticket: object of type 'generator' has no len()

Hmm. As the application developer, you’ve got a serious problem, and a user got ripped off as a result. But maybe this exception message itself isn’t very informative. Wouldn’t it be nice to see the lineage of the traceback that led to this exception?

The proper solution is to use logging.exception(), which logs a message with level ERREUR and also displays the exception traceback. Replace the two final lines above with these:

sauf Exception:
    enregistrement.exception("Could not draw ticket")

Now you get a better indication of what’s going on:

>>>

ERROR:root:Could not draw ticket
Traceback (dernier appel le plus récent):
  Fichier "", line 3, in 
  
  
  
  Fichier "", line 9, in dessiner
  Fichier "lib/python3.7/random.py", line 275, in mélanger
    pour je dans renversé(intervalle(1, len(X))):
TypeError: object of type 'generator' has no len()

En utilisant exception() saves you from having to reference the exception yourself because enregistrement pulls it in with sys.exc_info().

This makes things clearer that the problem stems from random.shuffle(), which needs to know the length of the object it is shuffling. Because our Lottery class passes a generator to shuffle(), it gets held up and raises before the pool can be shuffled, much less generate a winning ticket.

In large, full-blown applications, you’ll find logging.exception() to be even more useful when deep, multi-library tracebacks are involved, and you can’t step into them with a live debugger like pdb.

The code for logging.Logger.exception(), and hence logging.exception(), is just a single line:

def exception(soi, msg, *args, exc_info=Vrai, **kwargs):
    soi.Erreur(msg, *args, exc_info=exc_info, **kwargs)

That is, logging.exception() just calls logging.error() avec exc_info=True, which is otherwise Faux par défaut. If you want to log an exception traceback but at a level different than logging.ERROR, just call that function or method with exc_info=True.

Keep in mind that exception() should only be called in the context of an exception handler, inside of an sauf block:

pour je dans Les données:
    essayer:
        résultat = my_longwinded_nested_function(je)
    sauf ValueError:
        # We are in the context of exception handler now.
        # If it's unclear exactly *why* we couldn't process
        # `i`, then log the traceback and move on rather than
        # ditching completely.
        enregistreur.exception("Could not process %s", je)
        continuer

Use this pattern sparingly rather than as a means to suppress any exception. It can be most helpful when you’re debugging a long function call stack where you’re otherwise seeing an ambiguous, unclear, and hard-to-track error.

Conclusion

Pat yourself on the back, because you’ve just walked through almost 2,000 lines of dense source code. You’re now better equipped to deal with the enregistrement package!

Keep in mind that this tutorial has been far from exhaustive in covering all of the classes found in the enregistrement paquet. There’s even more machinery that glues everything together. If you’d like to learn more, then you can look into the Formateur classes and the separate modules logging/config.py et logging/handlers.py.