如何为自定义日志级别编写日志方法

7 投票
1 回答
5650 浏览
提问于 2025-04-16 11:34

你好,
我想在我的日志记录器(通过 logging.getLogger("rrcheck") 获取)上添加一些自己的方法,比如: def warnpfx(...):

我该怎么做比较好呢?

我最初的想法是有一个根日志记录器,可以把所有信息写入一个文件,同时还有一个命名的日志记录器("rrcheck"),可以把信息输出到标准输出(stdout),但后者还应该有一些其他的方法和级别。我需要它在输出到标准输出的某些信息前加上"! PFXWRN"的前缀(但只有这些信息),而其他信息则保持不变。我还希望能为根日志记录器和命名日志记录器单独设置日志级别。

这是我的代码:

class CheloExtendedLogger(logging.Logger):
    """
    Custom logger class with additional levels and methods
    """
    WARNPFX = logging.WARNING+1

    def __init__(self, name):
        logging.Logger.__init__(self, name, logging.DEBUG)                

        logging.addLevelName(self.WARNPFX, 'WARNING')

        console = logging.StreamHandler()
        console.setLevel(logging.DEBUG)
        # create formatter and add it to the handlers
        formatter = logging.Formatter("%(asctime)s [%(funcName)s: %(filename)s,%(lineno)d] %(message)s")
        console.setFormatter(formatter)

        # add the handlers to logger
        self.addHandler(console)

        return

    def warnpfx(self, msg, *args, **kw):
        self.log(self.WARNPFX, "! PFXWRN %s" % msg, *args, **kw)


logging.setLoggerClass(CheloExtendedLogger)    
rrclogger = logging.getLogger("rrcheck")
rrclogger.setLevel(logging.INFO)

def test():
    rrclogger.debug("DEBUG message")
    rrclogger.info("INFO message")
    rrclogger.warnpfx("warning with prefix")

test()

这是输出 - 函数和行号是错误的:warnpfx 应该是 test

2011-02-10 14:36:51,482 [test: log4.py,35] INFO message
2011-02-10 14:36:51,497 [warnpfx: log4.py,26] ! PFXWRN warning with prefix

也许我自己的日志记录器方法不是最好的选择?
你会建议我朝哪个方向去(自定义日志记录器、自定义处理器、自定义格式化器等等)?

如果我想再添加一个日志记录器,我该怎么做?
不幸的是,日志记录模块没有注册自定义日志记录器的功能,所以 getLogger(name) 会获取一个所需的日志记录器……

祝好,
Zbigniew

1 个回答

5

如果你查看一下Python的源代码,你会发现问题出在Logger.findCaller这个方法上。这个方法会遍历调用栈,寻找第一个不在logging.py文件中的行。因此,你在CheloExtendedLogger.warnpfx中调用self.log时,记录的行号就会出错。

不幸的是,logging.py中的代码结构不太灵活,所以修复起来比较麻烦:你需要在自己的子类中重新定义findCaller方法,这样它才能同时考虑logging.py文件和你自己的记录器所在的文件(注意,你的文件中不能有其他代码,否则结果还是会不准确)。这只需要在方法体中做一行小改动:

class CheloExtendedLogger(logging.Logger):

    [...]

    def findCaller(self):
        """
        Find the stack frame of the caller so that we can note the source
        file name, line number and function name.
        """
        f = logging.currentframe().f_back
        rv = "(unknown file)", 0, "(unknown function)"
        while hasattr(f, "f_code"):
            co = f.f_code
            filename = os.path.normcase(co.co_filename)
            if filename in (_srcfile, logging._srcfile): # This line is modified.
                f = f.f_back
                continue
            rv = (filename, f.f_lineno, co.co_name)
            break
        return rv

为了让这个方法正常工作,你需要在自己的文件中定义一个_srcfile变量。再次强调,logging.py没有使用函数,而是把所有代码放在模块级别,所以你需要再次复制粘贴:

if hasattr(sys, 'frozen'): #support for py2exe
    _srcfile = "logging%s__init__%s" % (os.sep, __file__[-4:])
elif string.lower(__file__[-4:]) in ['.pyc', '.pyo']:
    _srcfile = __file__[:-4] + '.py'
else:
    _srcfile = __file__
_srcfile = os.path.normcase(_srcfile)

好吧,如果你不在乎编译版本,最后的两行代码就足够了。

现在,你的代码可以按预期工作了:

2011-02-10 16:41:48,108 [test: lg.py,16] INFO message
2011-02-10 16:41:48,171 [test: lg.py,17] ! PFXWRN warning with prefix

至于多个记录器类,如果你不介意记录器名称和记录器类之间的依赖关系,你可以创建一个logging.Logger的子类,根据记录器的名称将调用委托给合适的记录器类。可能还有其他更优雅的解决方案,但我现在想不起来了。

撰写回答