如何将自定义日志级别添加到Python的日志记录工具中

时间:2010-02-02 10:16:04

标签: python logging

我想为我的应用程序提供loglevel TRACE(5),因为我不认为debug()就足够了。另外log(5, msg)不是我想要的。如何将自定义日志级别添加到Python记录器?

我有一个mylogger.py,内容如下:

import logging

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

在我的代码中,我以下列方式使用它:

class ExampleClass(object):
    from mylogger import log

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

现在我想致电self.log.trace("foo bar")

提前感谢您的帮助。

编辑(2016年12月8日):我将接受的答案更改为pfa's,即恕我直言,这是一个基于Eric S非常好的提案的出色解决方案。

16 个答案:

答案 0 :(得分:138)

@Eric S。

Eric S.的答案非常好,但我通过实验了解到,这将始终导致打印在新调试级别的消息 - 无论日志级别设置为什么。因此,如果您将新的级别数设置为9,如果调用setLevel(50),将错误地打印较低级别的消息。为防止这种情况发生,您需要在“debugv”函数中使用另一行来检查相关的日志记录级别是否实际启用。

检查是否启用了日志记录级别的固定示例:

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

如果您查看Python {的class Loggerlogging.__init__.py的代码,这就是所有标准日志函数的作用(.critical,.debug等)。

由于缺乏声誉,我显然无法回复别人的答案......希望如果他看到这个,Eric会更新他的帖子。 =)

答案 1 :(得分:59)

我接受了“避免看到lambda”的答案,并且必须修改添加log_at_my_log_level的位置。我也看到了保罗所做的问题“我觉得这不行。你不需要logger作为log_at_my_log_level中的第一个arg吗?”这对我有用

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

答案 2 :(得分:34)

这个问题相当陈旧,但我刚刚处理了相同的主题,并找到了一种类似于已经提到过的方式,这对我来说似乎有点清洁。这是在3.4上测试的,所以我不确定使用的方法是否存在于旧版本中:

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)

答案 3 :(得分:33)

将所有现有答案与大量使用经验相结合,我认为我已经提出了所有需要完成的事情的列表,以确保完全无缝地使用新级别。以下步骤假设您要添加值TRACE的新级别logging.DEBUG - 5 == 5

    需要调用
  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE')来获取内部注册的新级别,以便可以按名称引用它。
  2. 新级别需要作为属性添加到logging本身以保持一致性:logging.TRACE = logging.DEBUG - 5
  3. 需要将名为trace的方法添加到logging模块中。它应该表现得像debuginfo
  4. 需要将名为trace的方法添加到当前配置的记录器类中。由于这不是100%保证为logging.Logger,因此请改用logging.getLoggerClass()
  5. 以下方法说明了所有步骤:

    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)
    

答案 4 :(得分:18)

谁开始使用内部方法(self._log)的不良做法,为什么每个答案都基于此?! pythonic解决方案是使用self.log代替你,所以你不必乱用任何内部东西:

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

答案 5 :(得分:9)

我发现为传递log()函数的logger对象创建一个新属性更容易。我认为logger模块提供addLevelName()和log()就是出于这个原因。因此,不需要子类或新方法。

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

应该按预期工作。

答案 6 :(得分:8)

我认为你必须继承Logger类并添加一个名为trace的方法,该方法基本上调用Logger.log,其级别低于DEBUG。我没试过,但这就是docs indicate

答案 7 :(得分:5)

创建自定义记录器的提示:

  1. 请勿使用_log,请使用log(您不必查看isEnabledFor
  2. 日志记录模块应该是自定义记录器的一个创建实例,因为它在getLogger中有一些魔力,因此您需要通过setLoggerClass
  3. 设置类
  4. 如果您没有存储任何内容,则无需为记录器定义__init__
  5. # Lower than debug which is 10
    TRACE = 5
    class MyLogger(logging.Logger):
        def trace(self, msg, *args, **kwargs):
            self.log(TRACE, msg, *args, **kwargs)
    

    调用此记录器时,使用setLoggerClass(MyLogger)将其设为getLogger

    的默认记录器
    logging.setLoggerClass(MyLogger)
    log = logging.getLogger(__name__)
    # ...
    log.trace("something specific")
    

    setFormattersetHandler本身需要setLevel(TRACE)handlerlog来实际查看此低级别跟踪

答案 8 :(得分:3)

这对我有用:

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.')

lambda / funcName问题用@gerqueed指出的logger._log修复。我认为使用lambda看起来有点干净,但缺点是它不能接受关键字参数。我自己从未使用过,所以没什么大不了的。

  NOTE     setup: school's out for summer! dude
  FATAL    setup: file not found.

答案 9 :(得分:2)

根据我的经验,这是op的问题的完整解决方案......为了避免将“lambda”视为发出消息的函数,进一步深入:

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

我从未尝试过使用独立的记录器类,但我认为基本思路是一样的(使用_log)。

答案 10 :(得分:2)

添加Mad Physicists示例以获取正确的文件名和行号:

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

答案 11 :(得分:2)

虽然我们已经有了很多正确的答案,但我认为以下内容更像是pythonic:

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)

如果要在代码上使用mypy,建议添加# type: ignore以禁止警告添加属性。

答案 12 :(得分:0)

作为向Logger类添加额外方法的替代方法,我建议使用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')

答案 13 :(得分:0)

我很困惑;使用python 3.5,至少,它只是工作:

EmptyResult

输出:

  

DEBUG:root:y1

     

TRACE:根:Y2

答案 14 :(得分:0)

基于固定答案, 我写了一种自动创建新日志记录级别的小方法

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可能像这样:

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

答案 15 :(得分:-3)

如果有人想要一种自动方式动态地将新的日志记录级别添加到日志记录模块(或其副本),我已经创建了这个函数,扩展了@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