如何向 Python 的日志功能添加自定义日志级别
- 2025-03-18 08:55:00
- admin 原创
- 44
问题描述:
我希望我的应用程序具有日志级别 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 的非常好的建议的绝佳解决方案。
解决方案 1:
对于 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 的代码class Logger
,logging.__init__.py
这就是所有标准日志函数所做的(.critical、.debug 等)。
由于缺乏声誉,我显然无法对其他人的答案发表回复...希望 Eric 看到这个后能更新他的帖子。=)
解决方案 2:
结合所有现有答案和大量使用经验,我认为我已经列出了需要做的所有事情的清单,以确保完全无缝地使用新级别。以下步骤假设您正在添加一个TRACE
具有值的新级别logging.DEBUG - 5 == 5
:
logging.addLevelName(logging.DEBUG - 5, 'TRACE')
需要调用来内部注册新的级别,以便可以通过名称引用它。logging
为了保持一致性,需要将新级别作为其自身的属性添加:logging.TRACE = logging.DEBUG - 5
。trace
需要向模块添加一个名为 的方法logging
。它的行为应与debug
、info
等类似。需要将名为 的方法
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中找到更详细的实现。 该函数haggis.logs.add_logging_level
是此答案的更适合生产的实现。
解决方案 3:
我选择了避免看到“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
解决方案 4:
这个问题相当老了,但我刚刚处理了同样的主题,找到了一种类似于前面提到的方法,对我来说似乎更简洁一些。这是在 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)
解决方案 5:
虽然我们已经有很多正确答案,但我认为下面的答案更符合 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
抑制警告属性。
解决方案 6:
谁开始了使用内部方法 () 的不良做法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')
解决方案 7:
我认为您必须将该Logger
类子类化并添加一个名为 的方法,trace
该方法基本上Logger.log
以低于 的级别进行调用DEBUG
。我还没有尝试过,但这是文档所指出的。
解决方案 8:
我发现为传递 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')
应该可以按预期工作。
解决方案 9:
创建自定义记录器的提示:
不要使用
_log
,使用log
(您不必检查isEnabledFor
)日志记录模块应该是创建自定义记录器实例的模块,因为它有一些神奇的功能
getLogger
,因此您需要通过以下方式设置类setLoggerClass
__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")
您需要在和本身上setFormatter
进行才能真正看到这个低级跟踪setHandler
`setLevel(TRACE)handler
log`
解决方案 10:
这对我有用:
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/funcName 问题已通过 logger._log 修复。我认为使用 lambda 看起来更简洁一些,但缺点是它不能接受关键字参数。我自己从未使用过,所以没什么大不了的。
注意设置:学校放暑假了!哥们
严重设置:未找到文件。
解决方案 11:
作为向 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')
解决方案 12:
根据我的经验,这是 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)。
解决方案 13:
除了 Mad Physicists 示例外,还可以获取正确的文件名和行号:
def logToRoot(message, *args, **kwargs):
if logging.root.isEnabledFor(levelNum):
logging.root._log(levelNum, message, args, **kwargs)
解决方案 14:
跟进 Eric S. 和 Mad Physicist 给出的热门答案:
检查日志记录级别是否启用的固定示例:
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
此代码片段
添加新的日志级别“DEBUGV”并分配数字 9
定义一个
debugv
方法,它记录级别为“DEBUGV”的消息,除非日志级别设置为高于 9 的值(例如日志级别“DEBUG”)对类进行monkey-patches
logging.Logger
处理,以便可以调用logger.debugv
建议的实施方案对我来说效果很好,但是
代码完成无法识别
logger.debugv
-methodpyright 是我们 CI 管道的一部分,但它失败了,因为它无法跟踪-class
debugv
的 -member 方法Logger
(有关动态添加成员方法的信息,请参阅https://github.com/microsoft/pylance-release/issues/2335 )
我最终使用了继承,正如 Noufal Ibrahim 在这个主题中的回答所建议的那样:
我认为您必须将 Logger 类子类化并添加一个名为 trace 的方法,该方法基本上会以低于 DEBUG 的级别调用 Logger.log。我还没有尝试过,但文档中是这样指出的。
实施 Noufal Ibrahim 的建议很有效,pyright 很高兴:
import logging
# add log-level DEBUGV
DEBUGV = 9 # slightly lower than DEBUG (10)
logging.addLevelName(DEBUGV, "DEBUGV")
class MyLogger(logging.Logger):
"""Inherit from standard Logger and add level DEBUGV."""
def debugv(self, msg, *args, **kwargs):
"""Log 'msg % args' with severity 'DEBUGV'."""
if self.isEnabledFor(DEBUGV):
self._log(DEBUGV, msg, args, **kwargs)
logging.setLoggerClass(MyLogger)
接下来,您可以使用记录器管理器初始化扩展记录器的实例:
logger = logging.getLogger("whatever_logger_name")
编辑:为了让 pyright 识别debugv
-method,您可能需要转换 返回的记录器logging.getLogger
,如下所示:
import logging
from typing import cast
logger = cast(MyLogger, logging.getLogger("whatever_logger_name"))
解决方案 15:
根据固定答案,我编写了一个自动创建新日志级别的小方法
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,
}
解决方案 16:
有人可能想做一个根级别自定义日志记录;并避免使用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")
解决方案 17:
我很困惑;至少在 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
解决方案 18:
我重新组合了该主题中所有最好的答案(特别是https://stackoverflow.com/a/35804945和https://stackoverflow.com/a/55276759 ),并得到了 IMO 通用且最具 Python 风格的方式:
import logging
from functools import partial, partialmethod
def add_logging_level(level_name, level_num, method_name=None):
"""
Comprehensively adds a new logging level to the `logging` module and the
currently configured logging class.
`level_name` becomes an attribute of the `logging` module with the value
`level_num`.
`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
-------
>>> add_logging_level('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 method_name:
method_name = level_name.lower()
if hasattr(logging, level_name):
raise AttributeError(f'{level_name} already defined in logging module')
if hasattr(logging, method_name):
raise AttributeError(
f'{method_name} already defined in logging module'
)
if hasattr(logging.getLoggerClass(), method_name):
raise AttributeError(f'{method_name} already defined in logger class')
# This method was inspired by the answers to Stack Overflow post
# http://stackoverflow.com/q/2183233/2988730, especially
# https://stackoverflow.com/a/35804945
# https://stackoverflow.com/a/55276759
logging.addLevelName(level_num, level_name)
setattr(logging, level_name, level_num)
setattr(
logging.getLoggerClass(), method_name,
partialmethod(logging.getLoggerClass().log, level_num)
)
setattr(logging, method_name, partial(logging.log, level_num))
解决方案 19:
以下是适用于 Python3.10.12 的操作:
# your-loggings.py
import logging
TRACE = logging.DEBUG + 5
logging.addLevelName(TRACE, "TRACE")
def trace(instance: logging.Logger, msg, *args, **kwargs):
if instance.isEnabledFor(TRACE):
instance._log(TRACE, msg, args, *kwargs)
logging.Logger.trace = trace
用法:
# your-module.py
logger = logging.getLogger()
logger.trace("Something")
解决方案 20:
如果有人想要一种自动化的方式动态地向日志模块(或其副本)添加新的日志级别,我已经创建了这个函数,扩展了@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
扫码咨询,免费领取项目管理大礼包!