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


116

我想为我的应用程序使用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的公认答案,即IMHO,这是基于Eric S的非常好的建议的出色解决方案。

Answers:


171

@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 2.7中的class Loggerin 代码logging.__init__.py,这就是所有标准日志功能(.critical,.debug等)的功能。

我显然不能发表因缺乏声誉而对其他人的回答的回复...希望埃里克(Eric)会在看到这一点后更新其帖子。=)


7
这是更好的答案,因为它可以正确检查日志级别。
Panic Panic

2
当然比当前答案提供更多信息。
疯狂物理学家

4
@pfa如何添加,logging.DEBUG_LEVEL_NUM = 9以便在将记录器导入代码中的任何地方都可以访问该调试级别?
edgarstack '16

4
当然DEBUG_LEVEL_NUM = 9应该定义logging.DEBUG_LEVEL_NUM = 9。这样,您将可以使用log_instance.setLevel(logging.DEBUG_LEVEL_NUM)与使用正确的知识相同的方法logging.DEBUGlogging.INFO
maQ

这个答案非常有帮助。谢谢pfa和EricS。我想建议的完整性包含两个声明:logging.DEBUGV = DEBUG_LEVELV_NUMlogging.__all__ += ['DEBUGV'] 第二个是不是非常重要,但首先是必要的,如果你有动态调整日志记录级别的任何代码,你希望能够像做if verbose: logger.setLevel(logging.DEBUGV)`
基思汉兰

63

我回答了“避免看到lambda”,不得不修改在添加log_at_my_log_level的位置。我也看到了Paul所做的问题“我认为这不起作用。您是否需要logger作为log_at_my_log_level中的第一个参数?” 这对我有用

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。一种优雅的方法,而且效果很好。重要说明:您只需在一个模块中执行一次,即可在所有模块中使用。您甚至不必导入“设置”模块。因此,把它__init__.py
扔进

4
@Eric S.您应该看一下以下答案:stackoverflow.com/a/13638084/600110
Sam Mussmann 2012年

1
我同意@SamMussmann。我错过了那个答案,因为这是投票率最高的答案。
Panic Panic

@Eric S.为什么需要不带*的args?如果我这样做,我可以理解,TypeError: not all arguments converted during string formatting但是可以和*一起使用。(Python 3.4.3)。是python版本问题,还是我缺少的东西?
彼得

这个答案对我不起作用。尝试执行“ logging.debugv”会产生错误AttributeError: module 'logging' has no attribute 'debugv'
Alex

51

将所有现有答案与大量使用经验相结合,我想我已经列出了确保完全无缝使用新级别所需要做的所有事情的清单。下面的步骤假定您要添加一个TRACE具有value 的新级别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()改用。

下面的方法说明了所有步骤:

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)

将答案按排序Oldest,您将欣赏到这是所有答案中的最佳答案!
Serge Stroobandt,2016年

谢谢。我已经做了很多工作,将类似的内容整理在一起,并且此质量检查非常有帮助,所以我尝试添加一些内容。
疯狂物理学家,2013年

1
@PeterDolan。让我知道您是否对此有疑问。在我的个人工具箱中,我有一个扩展版本,可让您配置如何处理冲突的级别定义。这一次对我来说是因为我喜欢添加TRACE级别,而狮身人面像的组件之一也是如此。
疯狂物理学家

1
是缺乏前面的星号argslogForLevel故意实施/需要?
克里斯·巴恩斯

1
@突尼斯。这不是故意的。谢谢你的收获。
疯狂物理学家,

40

这个问题比较老,但是我只是处理相同的主题,并发现了一种与已经提到的类似的方法,对我来说似乎更干净。这已经在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)

1
恕我直言,这是最好的答案,因为它避免了猴子打补丁。什么get以及setLoggerClass究竟做和为什么需要他们?
Marco Sulla 2015年

3
@MarcoSulla它们被记录为Python日志记录模块的一部分。我假设使用动态子类,以防有人在使用此库时想要自己的llogger。然后,将MyLogger合并为我的类的子类。
CrackerJack9

关于是否TRACE向默认日志记录库中添加级别,本讨论中介绍的解决方案非常相似。+1
IMP1

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

18
需要使用_log()而不是log()以避免在调用堆栈中引入额外的级别。如果使用log(),则额外堆栈帧的引入将导致多个LogRecord属性(funcName,lineno,filename,pathname等)指向调试函数,而不是实际的调用者。这可能不是期望的结果。
rivy 2014年

5
从什么时候开始不允许调用类的内部方法?仅仅因为函数是在类之外定义的,并不意味着它是一个外部方法。
OozeMeister

3
此方法不仅会不必要地更改堆栈跟踪,而且不会检查是否记录了正确的级别。
疯狂物理学家,2016年

我觉得,@ schlamar的发言是正确的,但反原因获得的票数相同。那该怎么用呢?
Sumit Murari

1
为什么不使用内部方法?
Gringo Suave

9

我发现为通过log()函数的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')

应该能按预期工作。


与子类化相比,这对性能没有影响吗?使用这种方法,每次有人要求记录器时,他们都必须进行setattr调用。您可能将它们包装在一个自定义类中,但是尽管如此,必须在创建的每个记录器上调用setattr,对吗?
马修·隆德

下面的@Zbigniew表示此操作无效,我认为这是因为您的记录器需要致电_log,而不是log
2012年

9

虽然我们已经有了很多正确的答案,但我认为以下内容更像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以禁止添加属性的警告。


1
看起来不错,但最后一行令人困惑。不是logging.trace = partial(logging.log, logging.TRACE) # type: ignore吗?
Sergey Nudnov

@SergeyNudnov感谢您指出,我已修复它。从我这边是一个错误,我只是从代码中复制了代码,显然弄乱了清理工作。
DerWeh

8

我认为您必须对该Logger类进行子类化,并添加一个称为的方法trace,该方法基本上Logger.log以低于的级别进行调用DEBUG。我还没有尝试过,但这就是文档所指示的


3
而且,您可能需要替换logging.getLogger以返回子类而不是内置类。
S.Lott

4
@ S.Lott-实际上(至少使用当前版本的Python,也许在2010年不是这种情况),您必须使用setLoggerClass(MyClass)然后getLogger()按正常方式调用...
mac

IMO,这是迄今为止最好的(也是最Python化的)答案,如果我可以给它多个+1,那我就可以了。它执行起来很简单,但是示例代码会很好。:-D
Doug R.

@ DougR.Thanks,但就像我说的,我还没有尝试过。:)
Noufal Ibrahim

6

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

  1. 不要使用_log,使用log(您不必检查isEnabledFor
  2. 日志记录模块应该是自定义记录器的一个创建实例,因为它在中getLogger起到了一些神奇作用,因此您需要通过以下方式设置类setLoggerClass
  3. __init__如果您不存储任何内容,则无需为记录器定义类
# 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)handler与对log自身实际SE这低水平跟踪


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问题已通过@marqueed指出的logger._log解决。我认为使用lambda看起来更干净一些,但是缺点是它不能接受关键字参数。我自己从来没有用过,所以没什么大不了的。

  注意设置:暑假就要放学了!花花公子
  致命设置:找不到文件。

2

以我的经验,这是操作程序问题的完整解决方案...为了避免看到“ 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)。


我认为这行不通。您是否不需要logger作为第一个参数log_at_my_log_level
Paul

是的,我想您可能会。此答案改编自解决了稍有不同的问题的代码。
2011年

2

除了“疯狂物理学家”示例外,还可以使文件名和行号正确无误:

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

1

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

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

配置可能像这样:

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

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

0

我很困惑; 至少在python 3.5中,它可以正常工作:

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

输出:

调试:root:y1

跟踪:root:y2


1
这并不能让您做logger.trace('hi')我认为是主要目标的事情
Ultimation

-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

1
评估内部执行程序。哇。
疯狂物理学家

2
.....不知道是什么让我这么做的..在几个月之后,我很乐意将这个声明与一个setattr代替...
Vasilis Lemonidis
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.