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

我希望我的应用程序有日志级 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 的的公认答案,恕我直言,这是一个基于 Eric S 非常好的建议的优秀解决方案。

79970 次浏览

我认为您必须继承 Logger类并添加一个名为 trace的方法,该方法基本上调用低于 DEBUG级别的 Logger.log。我还没有试过这个,但这是什么 医生说

我发现为传递 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')

应该会如预期般奏效。

根据我的经验,这是操作问题的完整解决方案... ... 为了避免将“ 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

我从未尝试使用独立的 logger 类,但我认为基本思想是相同的(use _ log)。

我采用了 避免看到“ lambda”的答案,并且必须修改 log_at_my_log_level被添加的位置。我也看到了保罗所做的问题-我不认为这个工程。你不需要记录器作为第一个参数在 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

这对我很有效:

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

正如@marqueed 所指出的那样,lambda/funName 问题通过 logger._ log 得到了修复。我认为使用 lambda 看起来更简洁,但缺点是它不能接受关键字参数。我自己从来没用过,所以没什么大不了的。

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

对于2022年及以后的读者: 你可能应该在这里查看当前排名第二的答案: https://stackoverflow.com/a/35804945/1691778

我的原始答案在下面。

--

@ 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__.pyclass Logger的代码,就会发现所有标准日志函数都是这样做的(。至关重要。调试等)。

我显然不能回复其他人的回答缺乏声誉... 希望埃里克会更新他的帖子,如果他看到这一点。=)

谁开始使用内部方法(self._log)的坏习惯,为什么每个答案都是基于这个? !Python 的解决方案是使用 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')

作为向 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')

这个问题是相当老,但我只是处理了同样的主题,并找到了一个类似的方法,已经提到的那些似乎对我来说更干净一点。这是在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)

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

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

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

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

  1. 不要使用 _log,使用 log(您不必检查 isEnabledFor)
  2. 日志记录模块应该是创建自定义日志记录器的实例,因为它在 getLogger中起了一些作用,所以您需要通过 setLoggerClass设置类
  3. 您不需要为日志记录器定义 __init__,如果不存储任何内容,则可以使用 class
# 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)来实际执行这个低级跟踪

增加疯狂物理学家的例子,以获得正确的文件名和行号:

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

如果有人需要一种自动化的方法来动态地向日志模块添加一个新的日志级别(或者它的一个副本) ,我已经创建了这个函数,扩展了@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

我很困惑,至少对于 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")
    

产出:

根: y1

追踪: 根: y2

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

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,
}

虽然我们已经有很多正确答案,但我认为下面的答案更简洁:

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以禁止添加属性的警告。

有些人可能希望这样做,一个根级别的自定义日志记录; 并避免使用 logging.get _ logger (”) :

import logging
from datetime import datetime
c_now=datetime.now()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] :: %(message)s",
handlers=[
logging.StreamHandler(),
logging.FileHandler("../logs/log_file_{}-{}-{}-{}.log".format(c_now.year,c_now.month,c_now.day,c_now.hour))
]
)
DEBUG_LEVELV_NUM = 99
logging.addLevelName(DEBUG_LEVELV_NUM, "CUSTOM")
def custom_level(message, *args, **kws):
logging.Logger._log(logging.root,DEBUG_LEVELV_NUM, message, args, **kws)
logging.custom_level = custom_level
# --- --- --- ---
logging.custom_level("Waka")