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

178

我希望我的应用程序可以具有 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.的非常好的提议,我认为这是一个很好的解决方案。

19个回答

228

给2022年及以后的读者:你应该查看当前排名次高的答案:https://dev59.com/0XI95IYBdhLWcg3wtwb4#35804945

下面是我的原始回答。

--

@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中logging.__init__.py文件中的class Logger代码,这就是所有标准日志函数(.critical、.debug等)的作用。显然我由于声誉不足无法回复他人的答案……希望Eric看到后更新他的帖子。=)

8
这是更好的答案,因为它正确检查日志级别。 - Colonel Panic
6
@pfa 如果在代码中加入 logging.DEBUG_LEVEL_NUM = 9 ,那么你就可以在导入日志记录器时随时使用该调试级别,你觉得怎么样? - edgarstack
8
你应该定义 logging.DEBUG_LEVEL_NUM = 9 而不是 DEBUG_LEVEL_NUM = 9。这样,你就可以像使用 logging.DEBUGlogging.INFO 一样使用 log_instance.setLevel(logging.DEBUG_LEVEL_NUM) - maQ
2
这个答案非常有帮助。谢谢pfa和EricS。我想建议为了完整性,再加入两个语句: logging.DEBUGV = DEBUG_LEVELV_NUMlogging.__all__ += ['DEBUGV']第二个不是非常重要,但第一个是必要的,如果你有任何动态调整日志级别的代码,并且你想做类似于 if verbose: logger.setLevel(logging.DEBUGV) 这样的事情。 - Keith Hanlan
2
由于这是在现有类中添加的运行时定义,您知道是否有办法使Intellij / Pycharm发现并允许自动完成吗? - bodziec
1
如果您使用日志配置文件来设置默认处理程序、格式和任何日志级别,那么那里设置的日志级别是无法被覆盖和扩展的,例如在配置中设置为DEBUG,然后添加TRACE。如果使用logging.config.fileConfig(logConfigFile),则不要设置任何级别,或者使用level=NOTSET作为占位符,因为NOTSET=0。然后在其他地方设置您的级别,logging.getLogger().setLevel(logging.DEBUG)。 - chars

157

结合所有现有的答案以及一些使用经验,我认为我已经列出了确保完全无缝使用新级别所需做的所有事情的清单。以下步骤假设您正在添加一个名为TRACE,值为logging.DEBUG - 5 == 5的新级别:

  1. 需要调用logging.addLevelName(logging.DEBUG - 5, 'TRACE')来注册新级别,使其可以通过名称引用。
  2. 为了保持一致性,需要将新级别作为属性添加到logging本身中:logging.TRACE = logging.DEBUG - 5
  3. 需要在logging模块中添加一个名为trace的方法。它应该像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
    # https://dev59.com/0XI95IYBdhLWcg3wtwb4, especially
    # https://dev59.com/0XI95IYBdhLWcg3wtwb4#13638084
    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)

你可以在我维护的实用程序库haggis中找到更详细的实现。函数haggis.logs.add_logging_level是这个答案的一个更适合生产环境的实现。


1
@PeterDolan。如果您有困难,请告诉我。在我的个人工具箱中,我有一个扩展版本,可以让您配置如何处理冲突的级别定义。这对我来说曾经出现过一次,因为我喜欢添加TRACE级别,sphinx的某个组件也一样。 - Mad Physicist
1
logForLevel 实现中,args 前面缺少星号是有意为之还是必须的? - Chris L. Barnes
1
@突尼斯。这是无意的。感谢你的发现。 - Mad Physicist
1
@MattConway。哈哈,确实是自动驾驶,事实上已经过去3年了。感谢你的指出。 :) 我已经恢复到正确的版本了。 - Mad Physicist
1
@TimSweet。此时,可以使用kwargs.setdefault('stacklevel', 3),这样可以尊重用户的显式选择,或者使用kwargs['stacklevel'] = kwargs.get('stacklevel', 1) + 2。无论哪种方式,您都可以得到一个易读的一行代码。 - Mad Physicist
显示剩余13条评论

72

我采用了避免使用“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

8
+1。一个优雅的方法,而且完美运行。一个重要的提示:你只需要在一个模块中执行一次,它就会为所有模块工作。你甚至不需要导入 "setup" 模块。所以把它放在包的 __init__.py 中,并感到高兴 :D - MestreLion
5
@Eric S. 你应该看看这个答案:https://dev59.com/0XI95IYBdhLWcg3wtwb4#13638084 - Sam Mussmann
1
我同意@SamMussmann的观点。我错过了那个答案,因为它是最受欢迎的答案。 - Colonel Panic
@Eric S. 为什么你不使用 * 就能得到 args?如果我这样做,我会得到 TypeError: not all arguments converted during string formatting 的错误,但是如果使用 * 就可以正常工作。(Python 3.4.3)。这是 Python 版本的问题还是我漏掉了什么? - Peter
3
这个答案对我没用。尝试使用“logging.debugv”会出现错误:“AttributeError: module 'logging' has no attribute 'debugv'”。 - Alex

50

这个问题比较老,但我最近也遇到了同样的话题,并找到了一种类似于已经提到的方法,但对我来说更加清晰的方式。这在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
这是我个人认为最好的答案,因为它避免了猴子补丁。getsetLoggerClass究竟是做什么的,为什么需要它们? - Marco Sulla
3
它们被记录在Python的日志模块中。我认为,动态子类化是为了让使用这个库的人可以自定义自己的记录器。这样,MyLogger就成为我的类的子类,将两者结合起来。 - CrackerJack9
这与此讨论中提出的解决方案非常相似,即是否向默认日志记录库添加TRACE级别。+1 - IMP1

31

虽然我们已经有了许多正确的答案,但以下是我认为更符合Python风格的答案:

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来抑制添加属性时的警告。


2
看起来很好,但最后一行有点令人困惑。难道不应该是 logging.trace = partial(logging.log, logging.TRACE) # type: ignore 吗? - Sergey Nudnov
1
@SergeyNudnov 感谢您指出,我已经修复了它。这是我的错误,我只是从我的代码中复制,显然弄乱了清理工作。 - DerWeh

19

谁开始使用内部方法(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')

24
为了避免在调用栈中引入额外的级别,需要使用 _log() 替代 log()。如果使用 log(),引入额外的栈帧会导致几个 LogRecord 属性(funcName、lineno、filename、pathname 等)指向调试函数而不是实际的调用者。这可能不是期望的结果。 - rivy
7
调用类的内部方法从何时开始不允许了?仅仅因为函数定义在类外面并不意味着它是一个外部方法。请问需要翻译其他内容吗? - OozeMeister
5
这种方法不仅会不必要地改变调用堆栈的跟踪信息,而且也没有检查记录正确的级别。 - Mad Physicist
我觉得@schlamar所说的是对的,但反方意见也得到了同样数量的投票。那么该用什么呢? - Sumit Murari
1
为什么一个方法不使用内部方法? - Gringo Suave

9

我认为您需要子类化Logger类,并添加一个名为trace的方法,该方法基本上调用Logger.log并将级别设置低于DEBUG。我没有尝试过这个方法,但这是文档所示的方法。


3
你可能需要替换 logging.getLogger 以返回你的子类而不是内置类。 - S.Lott
6
实际上(至少在当前版本的Python中是这样,也许在2010年不是这样),你需要使用setLoggerClass(MyClass)然后像平常一样调用getLogger() - mac
在我看来,这绝对是最好的(并且最符合 Python 风格的)答案,如果我可以给它多个 +1 的话,我一定会这么做的。它执行起来很简单,不过如果附上示例代码就更好了。 :-D - Deacon
@DougR 谢谢,但是像我说的,我还没有尝试过。 :) - Noufal Ibrahim

8

我认为为日志记录器对象创建一个新属性并传递log()函数会更容易。我认为日志记录器模块提供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方法,对吧? - Matthew Lund
@Zbigniew指出这个方法不起作用,我认为是因为你的记录器需要调用_log而不是log - marqueed

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

您需要在 handlerlog 上设置 setFormattersetHandlersetLevel(TRACE),才能实际看到这个低级别跟踪。

4
这对我有用:
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看起来更加简洁,但缺点是它不能接受关键字参数。我自己从未使用过这个功能,所以没什么大不了的。
注意:设置:放假了!伙计们 严重:设置:文件未找到。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接