Comment ajouter un loglevel personnalisé au service de notation de Python
-
24-09-2019 - |
Question
Je voudrais avoir loglevel TRACE (5) pour mon application, je ne pense pas que debug()
est suffisante. De plus log(5, msg)
n'est pas ce que je veux. Comment puis-je ajouter un loglevel personnalisé à un enregistreur de 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'utiliser 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, je voudrais appeler self.log.trace("foo bar")
Merci d'avance pour votre aide.
Modifier (8 décembre 2016): J'ai changé la réponse acceptée à pfa qui est , à mon humble avis, une excellente solution basée sur la très bonne proposition de Eric S.
La solution
@Eric S.
La réponse de Eric S. est excellent, mais je l'ai appris par l'expérience que cela provoquera toujours des messages enregistrés au niveau de débogage à imprimer - quel que soit le niveau de journalisation est réglé sur. Donc, si vous faites un nouveau numéro de niveau de 9
, si vous appelez setLevel(50)
, niveau inférieur messages seront imprimés par erreur.
Pour éviter que cela se produise, vous avez besoin une autre ligne dans la fonction « debugv » pour vérifier si le niveau d'enregistrement en question est effectivement activée.
exemple fixe qui vérifie si le niveau d'enregistrement 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 Logger
en logging.__init__.py
pour Python 2.7, c'est ce que toutes les fonctions du journal standard ne (.critical, .debug, etc.).
Je ne peux apparemment pas envoyer des réponses aux réponses des autres par manque de réputation ... espérons-Eric mettra à jour son poste s'il voit. =)
Autres conseils
Je pris la « éviter de voir lambda » réponse et a dû modifier où le log_at_my_log_level a été ajouté. Moi aussi vu le problème que Paul ne « Je ne pense pas que cela fonctionne. Ne pas vous avez besoin enregistreur comme le premier arg 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
La combinaison de toutes les réponses existantes avec un tas d'expérience d'utilisation, je pense que je suis venu avec une liste de toutes les choses qui doivent être fait pour garantir une utilisation complètement transparente du nouveau niveau. Les étapes ci-dessous supposent que vous ajoutez un nouveau niveau TRACE
avec la valeur logging.DEBUG - 5 == 5
:
-
logging.addLevelName(logging.DEBUG - 5, 'TRACE')
doit être invoqué pour obtenir le nouveau niveau enregistré en interne afin qu'il puisse être référencé par son nom. - Le nouveau niveau doit être ajouté comme attribut à lui-même
logging
pour la cohérence.logging.TRACE = logging.DEBUG - 5
- Une méthode appelée
trace
doit être ajouté au modulelogging
. Il doit se comporter commedebug
,info
, etc. - Une méthode appelée
trace
doit être ajouté à la classe de l'enregistreur actuellement configuré. Étant donné que ce n'est pas 100% garanti àlogging.Logger
, utilisezlogging.getLoggerClass()
à la place.
Toutes les étapes sont illustrées dans le procédé 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)
Cette question est assez vieux, mais je ne me traite le même sujet et a trouvé une façon similaire à ceux déjà mentionnés qui semble 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)
Qui a commencé la mauvaise pratique d'utiliser des méthodes internes (self._log
) et pourquoi chaque réponse basée sur cette ?! La solution pythonique serait d'utiliser self.log
à la place que vous ne devez pas salir avec une substance interne:
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')
Je trouve plus facile de créer un nouvel attribut de l'objet enregistreur qui passe la fonction log (). Je pense que le module enregistreur fournit la addLevelName () et le journal () pour cette raison. Par conséquent, aucune sous-classes ou nouvelle méthode 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
mylogger.trace('This is a trace message')
devrait fonctionner comme prévu.
Je pense que vous aurez à sous-classe la classe Logger
et ajouter une méthode appelée trace
qui appelle essentiellement Logger.log
à un niveau inférieur à DEBUG
. Je ne l'ai pas essayé, mais c'est ce que les docs indiquent .
Conseils pour la création d'un enregistreur personnalisé:
- Ne pas utiliser
_log
, utilisezlog
(vous ne devez pas vérifierisEnabledFor
) - le module d'enregistrement doit être une instance de créer l'enregistreur personnalisé car il fait un peu de magie dans
getLogger
, vous aurez donc besoin de définir la classe viasetLoggerClass
- Vous n'avez pas besoin de définir
__init__
pour l'enregistreur, classe si vous n'êtes pas tout stocker
# 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 cette utilisation de l'enregistreur setLoggerClass(MyLogger)
pour faire de ce l'enregistreur par défaut de getLogger
logging.setLoggerClass(MyLogger)
log = logging.getLogger(__name__)
# ...
log.trace("something specific")
Vous devrez setFormatter
, setHandler
et setLevel(TRACE)
sur le handler
et sur le log
lui-même Sé fait cette trace de bas niveau
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.')
La question lambda / funcName est fixé par logger._log comme @marqueed souligné. Je pense que l'aide lambda ressemble un peu plus propre, mais l'inconvénient est qu'il ne peut pas prendre des arguments clés. Je ne l'ai jamais utilisé moi-même, donc pas trop grave.
NOTE setup: school's out for summer! dude FATAL setup: file not found.
Dans mon expérience, c'est la solution complète du problème de l'op ... pour éviter de voir « lambda » comme la fonction dans laquelle le message est émis, aller 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 ne l'ai jamais essayé de travailler avec une classe d'enregistreur autonome, mais je pense que l'idée de base est la même (utilisation _log).
Ajout d'exemple pour obtenir Mad Physiciens nom du fichier et le numéro de ligne correct:
def logToRoot(message, *args, **kwargs):
if logging.root.isEnabledFor(levelNum):
logging.root._log(levelNum, message, args, **kwargs)
Alors que nous avons déjà beaucoup de réponses, 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 voulez utiliser mypy
sur votre code, il est recommandé d'ajouter # type: ignore
pour supprimer les avertissements de l'ajout d'attribut.
Comme alternative à l'ajout d'une méthode supplémentaire pour la classe Logger Je 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')
Je suis confus; avec Python 3.5, au moins, il 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: racine: y1
TRACE: racine: y2
fonction de la réponse épinglé, je l'ai écrit une petite méthode qui crée de nouveaux niveaux de automaticaly exploitation forestière
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 SMTH comme ça:
new_log_levels = {
# level_num is in logging.INFO section, that's why it 21, 22, etc..
"FOO": 21,
"BAR": 22,
}
Dans le cas où quelqu'un veut un moyen automatisé d'ajouter un nouveau niveau de journalisation au module d'enregistrement (ou une copie de celui-ci) de manière dynamique, j'ai créé cette fonction, en expansion @ 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