web-dev-qa-db-fra.com

Comment ajouter un niveau de log personnalisé à la fonction de journalisation de Python

J'aimerais utiliser loglevel TRACE (5) pour mon application, car je ne pense pas que debug() soit suffisant. De plus, log(5, msg) n'est pas ce que je veux. Comment puis-je ajouter un niveau de journalisation personnalisé à un enregistreur Python?

J'ai un mylogger.py avec le contenu suivant:

import logging

@property
def log(obj):
    myLogger = logging.getLogger(obj.__class__.__name__)
    return myLogger

Dans mon code, je l'utilise de la manière suivante:

class ExampleClass(object):
    from mylogger import log

    def __init__(self):
        '''The constructor with the logger'''
        self.log.debug("Init runs")

J'aimerais maintenant appeler self.log.trace("foo bar")

Merci d'avance pour votre aide.

Edit (8 décembre 2016): J'ai changé la réponse acceptée en pfa's qui est, à mon humble avis, une excellente solution basée sur la très bonne proposition de Eric S.

84
tuergeist

@ Eric S.

La réponse d'Eric S. est excellente, mais j'ai appris expérimentalement que les messages enregistrés au nouveau niveau de débogage seront toujours imprimés, quel que soit le niveau défini pour le niveau de consignation. Ainsi, si vous créez un nouveau numéro de niveau de 9, si vous appelez setLevel (50), les messages de niveau inférieur seront imprimés par erreur. Pour éviter cela, vous avez besoin d'une autre ligne à l'intérieur de la fonction "debugv" pour vérifier si le niveau de journalisation en question est réellement activé.

Exemple fixe qui vérifie si le niveau de journalisation est activé:

import logging
DEBUG_LEVELV_NUM = 9 
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
    if self.isEnabledFor(DEBUG_LEVELV_NUM):
        # Yes, logger takes its '*args' as 'args'.
        self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
logging.Logger.debugv = debugv

Si vous examinez le code pour class Logger dans logging.__init__.py pour Python 2.7, toutes les fonctions de journalisation standard (.critical, .debug, etc.) remplissent cette fonction.

Je ne peux apparemment pas répondre aux réponses des autres par manque de réputation ... J'espère que Eric mettra à jour son message s'il le voit. =)

134
pfa

J'ai pris la réponse "éviter de voir lambda" et j'ai dû modifier l'emplacement où le log_at_my_log_level était ajouté. J'ai moi aussi vu le problème rencontré par Paul "Je ne pense pas que cela fonctionne. N'avez-vous pas besoin de logger en tant que premier argument de log_at_my_log_level?" Cela a fonctionné pour moi

import logging
DEBUG_LEVELV_NUM = 9 
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
    # Yes, logger takes its '*args' as 'args'.
    self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
logging.Logger.debugv = debugv
58
Eric S.

Cette question est assez ancienne, mais je viens de traiter du même sujet et de trouver un moyen similaire à ceux déjà mentionnés qui me parait un peu plus propre. Ceci a été testé sur 3.4, donc je ne suis pas sûr que les méthodes utilisées existent dans les versions précédentes:

from logging import getLoggerClass, addLevelName, setLoggerClass, NOTSET

VERBOSE = 5

class MyLogger(getLoggerClass()):
    def __init__(self, name, level=NOTSET):
        super().__init__(name, level)

        addLevelName(VERBOSE, "VERBOSE")

    def verbose(self, msg, *args, **kwargs):
        if self.isEnabledFor(VERBOSE):
            self._log(VERBOSE, msg, args, **kwargs)

setLoggerClass(MyLogger)
32
Wisperwind

En combinant toutes les réponses existantes avec de nombreuses expériences d'utilisation, je pense avoir dressé une liste de toutes les tâches à effectuer pour garantir une utilisation totalement transparente du nouveau niveau. Les étapes ci-dessous supposent que vous ajoutez un nouveau niveau TRACE avec la valeur logging.DEBUG - 5 == 5:

  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE') doit être appelé pour obtenir le nouveau niveau enregistré en interne afin qu'il puisse être référencé par nom.
  2. Le nouveau niveau doit être ajouté en tant qu'attribut à logging lui-même pour assurer la cohérence: logging.TRACE = logging.DEBUG - 5.
  3. Une méthode appelée trace doit être ajoutée au module logging. Il devrait se comporter exactement comme debug, info, etc.
  4. Une méthode appelée trace doit être ajoutée à la classe de journalisation actuellement configurée. Puisque ce n'est pas garanti à 100% d'être logging.Logger, utilisez plutôt logging.getLoggerClass().

Toutes les étapes sont illustrées dans la méthode ci-dessous:

def addLoggingLevel(levelName, levelNum, methodName=None):
    """
    Comprehensively adds a new logging level to the `logging` module and the
    currently configured logging class.

    `levelName` becomes an attribute of the `logging` module with the value
    `levelNum`. `methodName` becomes a convenience method for both `logging`
    itself and the class returned by `logging.getLoggerClass()` (usually just
    `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
    used.

    To avoid accidental clobberings of existing attributes, this method will
    raise an `AttributeError` if the level name is already an attribute of the
    `logging` module or if the method name is already present 

    Example
    -------
    >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
    >>> logging.getLogger(__name__).setLevel("TRACE")
    >>> logging.getLogger(__name__).trace('that worked')
    >>> logging.trace('so did this')
    >>> logging.TRACE
    5

    """
    if not methodName:
        methodName = levelName.lower()

    if hasattr(logging, levelName):
       raise AttributeError('{} already defined in logging module'.format(levelName))
    if hasattr(logging, methodName):
       raise AttributeError('{} already defined in logging module'.format(methodName))
    if hasattr(logging.getLoggerClass(), methodName):
       raise AttributeError('{} already defined in logger class'.format(methodName))

    # This method was inspired by the answers to Stack Overflow post
    # http://stackoverflow.com/q/2183233/2988730, especially
    # http://stackoverflow.com/a/13638084/2988730
    def logForLevel(self, message, *args, **kwargs):
        if self.isEnabledFor(levelNum):
            self._log(levelNum, message, args, **kwargs)
    def logToRoot(message, *args, **kwargs):
        logging.log(levelNum, message, *args, **kwargs)

    logging.addLevelName(levelNum, levelName)
    setattr(logging, levelName, levelNum)
    setattr(logging.getLoggerClass(), methodName, logForLevel)
    setattr(logging, methodName, logToRoot)
28
Mad Physicist

Qui a commencé la mauvaise pratique d'utiliser des méthodes internes (self._log) et pourquoi chaque réponse est-elle basée sur ça?! La solution Pythonic consisterait à utiliser self.log afin de ne pas avoir à vous soucier de choses internes:

import logging

SUBDEBUG = 5
logging.addLevelName(SUBDEBUG, 'SUBDEBUG')

def subdebug(self, message, *args, **kws):
    self.log(SUBDEBUG, message, *args, **kws) 
logging.Logger.subdebug = subdebug

logging.basicConfig()
l = logging.getLogger()
l.setLevel(SUBDEBUG)
l.subdebug('test')
l.setLevel(logging.DEBUG)
l.subdebug('test')
17
schlamar

Je trouve plus facile de créer un nouvel attribut pour l'objet logger qui transmet la fonction log (). Je pense que le module de journalisation fournit le addLevelName () et le log () pour cette raison même. Ainsi, aucune sous-classe ou nouvelle méthode n'est nécessaire. 

import logging

@property
def log(obj):
    logging.addLevelName(5, 'TRACE')
    myLogger = logging.getLogger(obj.__class__.__name__)
    setattr(myLogger, 'trace', lambda *args: myLogger.log(5, *args))
    return myLogger

à présent

mylogger.trace('This is a trace message')

devrait fonctionner comme prévu.

9
LtPinback

Je pense que vous devrez sous-classer la classe Logger et ajouter une méthode appelée trace qui appelle fondamentalement Logger.log avec un niveau inférieur à DEBUG. Je n’ai pas essayé cela, mais c’est ce que les docs indiquent

8
Noufal Ibrahim

Conseils pour créer un enregistreur personnalisé:

  1. N'utilisez pas _log, utilisez log (vous n'avez pas à vérifier isEnabledFor)
  2. le module de journalisation doit être l'instance créatrice de l'enregistreur personnalisé, car il effectue un peu de magie dans getLogger; vous devrez donc définir la classe via setLoggerClass
  3. Vous n'avez pas besoin de définir __init__ pour le consignateur, classe si vous ne stockez rien
# Lower than debug which is 10
TRACE = 5
class MyLogger(logging.Logger):
    def trace(self, msg, *args, **kwargs):
        self.log(TRACE, msg, *args, **kwargs)

Lorsque vous appelez cet enregistreur, utilisez setLoggerClass(MyLogger) pour en faire l’enregistreur par défaut à partir de getLogger.

logging.setLoggerClass(MyLogger)
log = logging.getLogger(__name__)
# ...
log.trace("something specific")

Vous aurez besoin de setFormatter, setHandler et setLevel(TRACE) sur handler et sur log pour que cette trace de bas niveau apparaisse réellement.

4
Bryce Guinta

Cela a fonctionné pour moi:

import logging
logging.basicConfig(
    format='  %(levelname)-8.8s %(funcName)s: %(message)s',
)
logging.NOTE = 32  # positive yet important
logging.addLevelName(logging.NOTE, 'NOTE')      # new level
logging.addLevelName(logging.CRITICAL, 'FATAL') # rename existing

log = logging.getLogger(__name__)
log.note = lambda msg, *args: log._log(logging.NOTE, msg, args)
log.note('school\'s out for summer! %s', 'dude')
log.fatal('file not found.')

Le problème de lambda/funcName est résolu avec logger._log comme l'a souligné @marqueed. Je pense que l'utilisation de lambda semble un peu plus propre, mais l'inconvénient est qu'elle ne peut pas prendre d'argument de mot clé. Je n'ai jamais utilisé ça moi-même, donc pas grave.

 NOTE setup: l'école est finie pour l'été! mec
 Configuration FATAL: fichier non trouvé .
3
Gringo Suave

D'après mon expérience, c'est la solution complète au problème de l'op ... Pour éviter de voir "lambda" comme fonction dans laquelle le message est émis, allez plus loin:

MY_LEVEL_NUM = 25
logging.addLevelName(MY_LEVEL_NUM, "MY_LEVEL_NAME")
def log_at_my_log_level(self, message, *args, **kws):
    # Yes, logger takes its '*args' as 'args'.
    self._log(MY_LEVEL_NUM, message, args, **kws)
logger.log_at_my_log_level = log_at_my_log_level

Je n'ai jamais essayé de travailler avec une classe de journalisation autonome, mais je pense que l'idée de base est la même (utilisez _log).

2
marqueed

Ajout à l’exemple de Mad Physicists pour obtenir le nom de fichier et le numéro de ligne corrects:

def logToRoot(message, *args, **kwargs):
    if logging.root.isEnabledFor(levelNum):
        logging.root._log(levelNum, message, args, **kwargs)
2
Frederik Holljen

Bien que nous ayons déjà plein de réponses correctes, ce qui suit est à mon avis un peu plus pythonique:

import logging

from functools import partial, partialmethod

logging.TRACE = 5
logging.addLevelName(logging.TRACE, 'TRACE')
logging.Logger.trace = partialmethod(logging.Logger.log, logging.TRACE)
logging.trace = partial(logging.log, logging.TRACE)

Si vous souhaitez utiliser mypy dans votre code, il est recommandé d'ajouter # type: ignore pour empêcher les avertissements d'ajouter un attribut.

2
DerWeh

sur la base d'une réponse épinglée, J'ai écrit une petite méthode qui crée automatiquement de nouveaux niveaux

def set_custom_logging_levels(config={}):
    """
        Assign custom levels for logging
            config: is a dict, like
            {
                'EVENT_NAME': EVENT_LEVEL_NUM,
            }
        EVENT_LEVEL_NUM can't be like already has logging module
        logging.DEBUG       = 10
        logging.INFO        = 20
        logging.WARNING     = 30
        logging.ERROR       = 40
        logging.CRITICAL    = 50
    """
    assert isinstance(config, dict), "Configuration must be a dict"

    def get_level_func(level_name, level_num):
        def _blank(self, message, *args, **kws):
            if self.isEnabledFor(level_num):
                # Yes, logger takes its '*args' as 'args'.
                self._log(level_num, message, args, **kws) 
        _blank.__= level_name.lower()
        return _blank

    for level_name, level_num in config.items():
        logging.addLevelName(level_num, level_name.upper())
        setattr(logging.Logger, level_name.lower(), get_level_func(level_name, level_num))

config peut ressembler à ça:

new_log_levels = {
    # level_num is in logging.INFO section, that's why it 21, 22, etc..
    "FOO":      21,
    "BAR":      22,
}
0
groshevpavel

Au lieu d’ajouter une méthode supplémentaire à la classe Logger, je vous recommande d’utiliser la méthode Logger.log(level, msg).

import logging

TRACE = 5
logging.addLevelName(TRACE, 'TRACE')
FORMAT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'


logging.basicConfig(format=FORMAT)
l = logging.getLogger()
l.setLevel(TRACE)
l.log(TRACE, 'trace message')
l.setLevel(logging.DEBUG)
l.log(TRACE, 'disabled trace message')
0
schlamar

Je suis confus; avec python 3.5, au moins, cela fonctionne:

import logging


TRACE = 5
"""more detail than debug"""

logging.basicConfig()
logging.addLevelName(TRACE,"TRACE")
logger = logging.getLogger('')
logger.debug("n")
logger.setLevel(logging.DEBUG)
logger.debug("y1")
logger.log(TRACE,"n")
logger.setLevel(TRACE)
logger.log(TRACE,"y2")

sortie:

DEBUG: root: y1 

TRACE: racine: y2

0
gerardw