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é.
Bonus gratuit: 5 réflexions sur la maîtrise Python, un cours gratuit pour les développeurs Python qui vous montre la feuille de route et l'état d'esprit dont vous aurez besoin pour améliorer vos compétences en Python.
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 enregistrement
En 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.
Remarque: Vous pouvez utiliser l'attribut dunder logging.StreamHandler .__ mro__
pour voir la chaîne de l'héritage. Un guide définitif sur le MRO se trouve dans la documentation Python 2, bien qu’il soit également applicable à Python 3.
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
:
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:
- Une avec niveau
INFO
qui vide les données du journal dans un fichier à/tmp/records.log
- Celui qui écrit à
sys.stderr
mais seulement si le message entrant est au niveauERREUR
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:
- Un nom
- L'heure de création sous forme d'horodatage Unix
- Le message lui-même
- 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.
Remarque: Sous le LogRecord
classe, vous trouverez également le setLogRecordFactory ()
, getLogRecordFactory ()
, et makeLogRecord ()
fonctions d'usine. Vous n’en aurez pas besoin à moins d’utiliser une classe personnalisée au lieu de LogRecord
pour encapsuler les messages de journal et leurs métadonnées.
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 LogRecord
et 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 delogging.AVERTISSEMENT
et un.prénom
de"racine"
. Aussi loin que la classeRootLogger
est concerné, ce nom unique est tout ce qui fait sa particularité. -
le
racine
l'objet à son tour devient un attribut class pour leenregistreur
classe. Cela signifie que toutes les instances deenregistreur
, et leenregistreur
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 leenregistrement
paquet. -
UNE
Directeur
l'instance est définie comme.directeur
attribut de classe pourenregistreur
. Cela finit par jouer danslogging.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 pourDÉBOGUER
messages, un pourINFO
, un pourATTENTION
, etc. -
Chaque
enregistreur
est également lié à un autre gestionnaire supplémentaire qui ne reçoit queLogRecord
cas de cette seuleenregistreur
. Vous pouvez appeler cela un gestionnaire de fichiers basé sur un module.
Visuellement, notre objectif est de ressembler à ceci:
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 leprojet.intéressant
etprojet.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:
- Vous avez enregistré un message qui, apparemment, n’est allé nulle part, et vous ne savez pas pourquoi.
- 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:
-
They always call their corresponding method from the root logger,
racine
. -
Before calling the root logger methods, they call
logging.basicConfig()
with no arguments ifracine
doesn’t have any handlers. As you saw earlier, it is this call that sets asys.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:
-
Appel
self.makeRecord()
: Make aLogRecord
instance from themsg
and other arguments you pass to it. -
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:
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 undict
lookup on the stringprénom
. What this comes down to is a lookup in theloggerDict
delogging.Manager
. This is a dictionary of all registered loggers, including the intermediatePlaceHolder
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 seulracine
—the instance ofRootLogger
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:
-
Python gets (creates) the
Logger
instance with the same name as your package. If you’re designing thecalcul
package, within__init__.py
, puis__name__
will be equal to'calculus'
. -
UNE
NullHandler
instance gets attached to this logger. That means that Python will not default to using thelastResort
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
.
[ad_2]