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


116

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

J'ai un mylogger.pyavec 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")

Maintenant j'aimerais 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 qui est, à mon humble avis , une excellente solution basée sur la très bonne proposition d'Eric S.

Réponses:


171

@ Eric S.

La réponse d'Eric S. est excellente, mais j'ai appris par expérimentation que cela entraînera toujours l'impression des messages enregistrés au nouveau niveau de débogage - quel que soit le niveau de journalisation. Donc, si vous créez un nouveau numéro de niveau 9, si vous appelez setLevel(50), les messages de niveau inférieur seront imprimés par erreur.

Pour éviter que cela ne se produise, vous avez besoin d'une autre ligne dans 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 regardez le code pour class Loggerdanslogging.__init__.py for Python 2.7, c'est ce que font toutes les fonctions de journalisation standard (.critical, .debug, etc.).

Je ne peux apparemment pas poster de réponses aux réponses des autres par manque de réputation ... j'espère qu'Eric mettra à jour son message s'il voit cela. =)


7
C'est la meilleure réponse car elle vérifie correctement le niveau de journalisation.
Colonel Panic

2
Certainement beaucoup plus informatif que la réponse actuelle.
Mad Physicist

4
@pfa Que diriez-vous d'ajouter logging.DEBUG_LEVEL_NUM = 9pour pouvoir accéder à ce niveau de débogage partout où vous importez le logger dans votre code?
edgarstack

4
Définitivement à la place, DEBUG_LEVEL_NUM = 9vous devriez définir logging.DEBUG_LEVEL_NUM = 9. De cette façon, vous pourrez utiliser log_instance.setLevel(logging.DEBUG_LEVEL_NUM)la même manière que vous en utilisant right know logging.DEBUGoulogging.INFO
maQ

Cette réponse a été très utile. Merci pfa et EricS. Je voudrais suggérer que, pour être complet, deux autres déclarations soient incluses: logging.DEBUGV = DEBUG_LEVELV_NUMet logging.__all__ += ['DEBUGV'] La seconde n'est pas très importante mais la première est nécessaire si vous avez un code qui ajuste dynamiquement le niveau de journalisation et que vous voulez pouvoir faire quelque chose comme if verbose: logger.setLevel(logging.DEBUGV)``
Keith Hanlan

63

J'ai pris la réponse "éviter de voir lambda" et j'ai dû modifier l'endroit où le log_at_my_log_level était ajouté. J'ai aussi vu le problème que Paul a fait "Je ne pense pas que cela fonctionne. N'avez-vous pas besoin de logger comme premier argument dans 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

7
+1 aussi. Une approche élégante, et cela a parfaitement fonctionné. Remarque importante: vous ne devez le faire qu'une seule fois, dans un seul module, et cela fonctionnera pour tous les modules . Vous n'avez même pas besoin d'importer le module "setup". Alors jetez ça dans un paquet __init__.pyet soyez heureux: D
MestreLion

4
@Eric S.Vous devriez jeter un oeil à cette réponse: stackoverflow.com/a/13638084/600110
Sam Mussmann

1
Je suis d'accord avec @SamMussmann. J'ai manqué cette réponse parce que c'était la réponse la plus votée.
Colonel Panic

@Eric S. Pourquoi avez-vous besoin d'arguments sans *? Si je fais ça, j'obtiens TypeError: not all arguments converted during string formattingmais ça marche bien avec *. (Python 3.4.3). Est-ce un problème de version de Python ou quelque chose qui me manque?
Peter

Cette réponse ne fonctionne pas pour moi. Essayer de faire un 'logging.debugv' donne une erreurAttributeError: module 'logging' has no attribute 'debugv'
Alex

51

En combinant toutes les réponses existantes avec un tas d'expérience d'utilisation, je pense avoir dressé une liste de toutes les choses à faire pour assurer une utilisation totalement transparente du nouveau niveau. Les étapes ci-dessous supposent que vous ajoutez un nouveau niveau TRACEavec une valeur logging.DEBUG - 5 == 5:

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

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)

Triez les réponses par Oldest, et vous comprendrez que c'est la meilleure de toutes!
Serge Stroobandt

Merci. J'ai fait pas mal de travail pour bricoler quelque chose comme ça ensemble et ce contrôle qualité a été très utile, alors j'ai essayé d'ajouter quelque chose.
Mad Physicist

1
@PeterDolan. Faites-moi savoir si vous avez des problèmes avec cela. Dans ma boîte à outils personnelle, j'ai une version étendue qui vous permet de configurer comment gérer les définitions de niveau en conflit. Cela m'est venu une fois parce que j'aime ajouter un niveau TRACE, tout comme l'un des composants du sphinx.
Mad Physicist

1
L'absence d'astérisque devant argsdans la logForLevelmise en œuvre est-elle intentionnelle / requise?
Chris L. Barnes

1
@Tunisie. Ce n'est pas intentionnel. Merci pour la capture.
Mad Physicist

40

Cette question est assez ancienne, mais je viens de traiter le même sujet et j'ai trouvé une voie similaire à celles déjà évoquées qui me paraît un peu plus propre. Cela a été testé sur 3.4, donc je ne suis pas sûr que les méthodes utilisées existent dans les anciennes versions:

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)

1
C'est à mon humble avis la meilleure réponse, car elle évite le patching de singe. Que getet setLoggerClassfaire exactement et pourquoi ils sont nécessaires?
Marco Sulla

3
@MarcoSulla Ils sont documentés dans le cadre du module de journalisation de Python. Le sous-classement dynamique, je suppose, est utilisé au cas où quelqu'un voudrait son propre enregistreur tout en utilisant cette bibliothèque. Ce MyLogger deviendrait alors une sous-classe de ma classe, combinant les deux.
CrackerJack9

Ceci est très similaire à la solution présentée dans cette discussion pour savoir si ajouter un TRACEniveau à la bibliothèque de journalisation par défaut. +1
IMP1

18

Qui a commencé la mauvaise pratique d'utiliser des méthodes internes ( self._log) et pourquoi chaque réponse est-elle basée sur cela?! La solution pythonique serait d'utiliser à la self.logplace pour ne pas avoir à jouer avec des trucs 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')

18
L'utilisation de _log () au lieu de log () est nécessaire pour éviter d'introduire un niveau supplémentaire dans la pile d'appels. Si log () est utilisé, l'introduction du frame de pile supplémentaire fait pointer plusieurs attributs LogRecord (funcName, lineno, filename, pathname, ...) vers la fonction de débogage au lieu de l'appelant réel. Ce n'est probablement pas le résultat souhaité.
rivy

5
Depuis quand est-il interdit d'appeler les méthodes internes d'une classe? Ce n'est pas parce que la fonction est définie en dehors de la classe que c'est une méthode externe.
OozeMeister

3
Cette méthode modifie non seulement la trace de la pile inutilement, mais ne vérifie pas non plus que le niveau correct est enregistré.
Mad Physicist

Je pense que ce que @schlamar dit est juste, mais la contre-raison a obtenu le même nombre de votes. Alors quoi utiliser?
Sumit Murari

1
Pourquoi une méthode n'utiliserait-elle pas une méthode interne?
Gringo Suave

9

Je trouve plus facile de créer un nouvel attribut pour l'objet enregistreur qui passe la fonction log (). Je pense que le module logger 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

maintenant

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

devrait fonctionner comme prévu.


Cela n'aurait-il pas un petit impact sur les performances par rapport au sous-classement? Avec cette approche, chaque fois que certains demandent un enregistreur, ils devront faire l'appel setattr. Vous les regrouperiez probablement dans une classe personnalisée, mais néanmoins, ce setattr doit être appelé sur chaque enregistreur créé, non?
Matthew Lund

@Zbigniew ci-dessous a indiqué que cela n'a pas fonctionné, ce qui, je pense, est dû au fait que votre enregistreur doit faire son appel _log, non log.
marqué

9

Bien que nous ayons déjà beaucoup de réponses correctes, ce qui suit est à mon avis 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 mypysur votre code, il est recommandé d'ajouter # type: ignorepour supprimer les avertissements d'ajout d'attribut.


1
Cela a l'air génial, mais la dernière ligne est déroutante. Ne devrait-il pas l'être logging.trace = partial(logging.log, logging.TRACE) # type: ignore?
Sergey Nudnov

@SergeyNudnov merci de l'avoir signalé, je l'ai corrigé. C'était une erreur de mon côté, je viens de copier mon code et apparemment gâché le nettoyage.
DerWeh

8

Je pense que vous devrez sous- Loggerclasser la classe et ajouter une méthode appelée tracequi appelle essentiellement Logger.logavec un niveau inférieur à DEBUG. Je n'ai pas essayé cela, mais c'est ce que la documentation indique .


3
Et vous voudrez probablement remplacer logging.getLoggerpour renvoyer votre sous-classe au lieu de la classe intégrée.
S.Lott

4
@ S.Lott - En fait (au moins avec la version actuelle de Python, ce n'était peut-être pas le cas en 2010), vous devez utiliser setLoggerClass(MyClass)puis appeler getLogger()comme d'habitude ...
mac

OMI, c'est de loin la meilleure réponse (et la plus pythonique), et si j'avais pu lui donner plusieurs + 1, je l'aurais fait. C'est simple à exécuter, mais un exemple de code aurait été bien. :-D
Doug R.

@ DougR.Merci mais comme je l'ai dit, je ne l'ai pas essayé. :)
Noufal Ibrahim

6

Conseils pour créer un enregistreur personnalisé:

  1. Ne pas utiliser _log, utiliser log(vous n'avez pas à vérifier isEnabledFor)
  2. le module de journalisation doit être celui qui crée l'instance de l'enregistreur personnalisé car il fait de la magie dans getLogger , vous devrez donc définir la classe viasetLoggerClass
  3. Vous n'avez pas besoin de définir __init__pour l'enregistreur, la 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 degetLogger

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

Vous aurez besoin de setFormatter, setHandleret setLevel(TRACE)sur le handleret sur loglui - même pour voir réellement cette trace de bas niveau


3

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 lambda / funcName est résolu avec logger._log comme @marqueed l'a souligné. Je pense que l'utilisation de lambda semble un peu plus propre, mais l'inconvénient est qu'il ne peut pas prendre d'arguments de mots-clés. Je n'ai jamais utilisé ça moi-même, donc pas de problème.

  REMARQUE configuration: l'école est finie pour l'été! mec
  Configuration FATAL: fichier introuvable.

2

D'après mon expérience, c'est la solution complète au problème de l'op ... pour éviter de voir "lambda" comme la 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).


Je ne pense pas que cela fonctionne. N'avez-vous pas besoin loggercomme premier argument log_at_my_log_level?
Paul

Oui, je pense que vous le feriez probablement. Cette réponse a été adaptée d'un code qui résout un problème légèrement différent.
marqué

2

Exemple d'ajout à 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)

1

basé sur une réponse épinglée, j'ai écrit une petite méthode qui crée automatiquement de nouveaux niveaux de journalisation

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.__name__ = 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

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

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

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")
    

production:

DEBUG: racine: y1

TRACE: racine: y2


1
Cela ne vous permet pas de faire ce logger.trace('hi')que je crois être l'objectif principal
Ultimation

-3

Au cas où quelqu'un souhaiterait un moyen automatisé d'ajouter un nouveau niveau de journalisation au module de journalisation (ou à une copie de celui-ci) de manière dynamique, j'ai créé cette fonction, en développant la réponse de @ pfa:

def add_level(log_name,custom_log_module=None,log_num=None,
                log_call=None,
                   lower_than=None, higher_than=None, same_as=None,
              verbose=True):
    '''
    Function to dynamically add a new log level to a given custom logging module.
    <custom_log_module>: the logging module. If not provided, then a copy of
        <logging> module is used
    <log_name>: the logging level name
    <log_num>: the logging level num. If not provided, then function checks
        <lower_than>,<higher_than> and <same_as>, at the order mentioned.
        One of those three parameters must hold a string of an already existent
        logging level name.
    In case a level is overwritten and <verbose> is True, then a message in WARNING
        level of the custom logging module is established.
    '''
    if custom_log_module is None:
        import imp
        custom_log_module = imp.load_module('custom_log_module',
                                            *imp.find_module('logging'))
    log_name = log_name.upper()
    def cust_log(par, message, *args, **kws):
        # Yes, logger takes its '*args' as 'args'.
        if par.isEnabledFor(log_num):
            par._log(log_num, message, args, **kws)
    available_level_nums = [key for key in custom_log_module._levelNames
                            if isinstance(key,int)]

    available_levels = {key:custom_log_module._levelNames[key]
                             for key in custom_log_module._levelNames
                            if isinstance(key,str)}
    if log_num is None:
        try:
            if lower_than is not None:
                log_num = available_levels[lower_than]-1
            elif higher_than is not None:
                log_num = available_levels[higher_than]+1
            elif same_as is not None:
                log_num = available_levels[higher_than]
            else:
                raise Exception('Infomation about the '+
                                'log_num should be provided')
        except KeyError:
            raise Exception('Non existent logging level name')
    if log_num in available_level_nums and verbose:
        custom_log_module.warn('Changing ' +
                                  custom_log_module._levelNames[log_num] +
                                  ' to '+log_name)
    custom_log_module.addLevelName(log_num, log_name)

    if log_call is None:
        log_call = log_name.lower()

    setattr(custom_log_module.Logger, log_call, cust_log)
    return custom_log_module

1
Eval à l'intérieur de l'exécutif. Sensationnel.
Mad Physicist

2
..... je ne sais pas ce qui m'a fait faire cela .... après tant de mois, je serais heureux d'échanger cette déclaration avec un à la setattrplace ...
Vasilis Lemonidis
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.