如何扩展 logging.Logger 类?

19
我想从一个基本的日志记录类开始,该类继承自Python的logging.Logger类。但是,我不确定如何构建我的类,以便可以建立定制继承记录器所需的基础。
以下是我在logger.py文件中目前的内容:
import sys
import logging
from logging import DEBUG, INFO, ERROR

class MyLogger(object):
    def __init__(self, name, format="%(asctime)s | %(levelname)s | %(message)s", level=INFO):
        # Initial construct.
        self.format = format
        self.level = level
        self.name = name

        # Logger configuration.
        self.console_formatter = logging.Formatter(self.format)
        self.console_logger = logging.StreamHandler(sys.stdout)
        self.console_logger.setFormatter(self.console_formatter)

        # Complete logging config.
        self.logger = logging.getLogger("myApp")
        self.logger.setLevel(self.level)
        self.logger.addHandler(self.console_logger)

    def info(self, msg, extra=None):
        self.logger.info(msg, extra=extra)

    def error(self, msg, extra=None):
        self.logger.error(msg, extra=extra)

    def debug(self, msg, extra=None):
        self.logger.debug(msg, extra=extra)

    def warn(self, msg, extra=None):
        self.logger.warn(msg, extra=extra)

这是主要的myApp.py文件:
import entity
from core import MyLogger

my_logger = MyLogger("myApp")

def cmd():
    my_logger.info("Hello from %s!" % ("__CMD"))

entity.third_party()
entity.another_function()
cmd()

以下是 entity.py 模块的内容:

# Local modules
from core import MyLogger

# Global modules
import logging
from logging import DEBUG, INFO, ERROR, CRITICAL

my_logger = MyLogger("myApp.entity", level=DEBUG)

def third_party():
    my_logger.info("Initial message from: %s!" % ("__THIRD_PARTY"))

def another_function():
    my_logger.warn("Message from: %s" % ("__ANOTHER_FUNCTION"))

当我运行主应用程序时,我会得到以下内容:
2016-09-14 12:40:50,445 | INFO | Initial message from: __THIRD_PARTY!
2016-09-14 12:40:50,445 | INFO | Initial message from: __THIRD_PARTY!
2016-09-14 12:40:50,445 | WARNING | Message from: __ANOTHER_FUNCTION
2016-09-14 12:40:50,445 | WARNING | Message from: __ANOTHER_FUNCTION
2016-09-14 12:40:50,445 | INFO | Hello from __CMD!
2016-09-14 12:40:50,445 | INFO | Hello from __CMD!

所有内容都被打印了两次,可能是我未能正确设置记录器类。

更新1

让我澄清我的目标。

(1) 我希望将主要的日志功能封装在一个单一的位置中,这样我就可以这样做:

 from mylogger import MyLogger
 my_logger = MyLogger("myApp")
 my_logger.info("Hello from %s!" % ("__CMD"))

(2) 我计划使用CustomFormatterCustomAdapter类。这一部分不需要自定义日志记录类,可以直接插入。

(3) 我可能不需要在底层日志记录器类(记录等)方面进行深度定制,拦截logger.infologgin.debug等就足够了。

所以参考一下这个Python代码片段在这些论坛上已经广泛传播:

我试图找到一个甜点,既有Logger Class,又能够使用内置函数,如分配FormattersAdapters等。因此,所有内容都与logging模块兼容。

class OurLogger(logging.getLoggerClass()):
    def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
        # Don't pass all makeRecord args to OurLogRecord bc it doesn't expect "extra"
        rv = OurLogRecord(name, level, fn, lno, msg, args, exc_info, func)
        # Handle the new extra parameter.
        # This if block was copied from Logger.makeRecord
        if extra:
            for key in extra:
                if (key in ["message", "asctime"]) or (key in rv.__dict__):
                    raise KeyError("Attempt to overwrite %r in LogRecord" % key)
                rv.__dict__[key] = extra[key]
        return rv

更新2

我创建了一个存储库,其中包含一个简单的Python应用程序,演示了一种可能的解决方案。然而,我很想改进这个应用程序。

xlog_example

此示例有效地演示了通过继承覆盖logging.Logger类和logging.LogRecord类的技术。

两个外部项目被混合到日志流中:funcnameusername,而不使用任何FormattersAdapters


@JonasWielicki 我想基于现有的设施封装日志记录机制,并且我想使用自定义的FormattersHandlersAdapters等进行工作。这个想法是(作为一种练习)覆盖日志记录所需的部分。话虽如此,我知道这在社区中一直引起了一定程度的争议。我仍然相信有一种方法可以通过自定义类等来实现。我知道还有其他官方的方法来做到这一点。 - mbilyanov
我已经创建了一个具有可工作基础版本的repo。更多信息请阅读“更新(02)”。 - mbilyanov
在一天结束时,logging 模块在自定义方面提供了许多选项。但是,如果我们想要向 logging.debug()logging.info() 等函数添加更复杂的自定义逻辑,该怎么办呢?例如,在现有的级别逻辑之上,假设“如果今天是星期一,则不显示任何消息”。如果不劫持 loggin.info() 方法并注入我们的逻辑,那么如何实现呢? - mbilyanov
@JonasWielicki请注意,日志模块的创建者本人鼓励Logger继承:“... findCaller应该在Logger子类中被覆盖,以用于这些用例 - 与gonvaled不同,我认为这不是一个常见的用例。面向对象编程的一点是,您可以通过子类化和覆盖来扩展现有功能以适应特殊情况。请注意,可能会有其他方法(例如使用Filter)来提供附加处理,而不是使用包装器。” - mbilyanov
以上引用摘自Vinay Sajip在此处发表的评论。 - Piotr Dobrogost
2个回答

16

在这个阶段,我认为我已经做了足够的研究并提供了示例来解决问题,这已经足以作为我的问题的答案。一般来说,可以使用许多方法来包装日志记录功能。这个问题旨在专注于一种利用logging.Logger类继承的解决方案,以便可以更改内部机制,而保留其余功能因为它将由原始的logging.Logger类提供。

即便如此,应该非常谨慎地使用类继承技术。日志记录模块提供的大多数功能已经足以维护和运行稳定的日志记录工作流程。从logging.Logger类继承可能是好的选择,当目标是对日志数据处理和导出的方式进行根本性的更改时。

总结一下,我认为有两种包装日志记录功能的方法:

1)传统的日志记录方法:

这只是简单地使用提供的日志记录方法和函数,但将它们封装在一个模块中,以便某些通用的重复任务都集中在一个地方。这样,像日志文件、日志级别、管理自定义FiltersAdapters等就会变得更容易。

我不确定在这种情况下是否可以使用class方法(我指的不是超类方法,也就是第二个项目的主题),因为当日志记录调用被封装在一个类中时,事情似乎变得很复杂。我想听听有关这个问题的意见,并且我一定会准备一个探讨这方面的问题。

2)Logger继承:

这种方法基于从原始的logging.Logger类继承并添加现有方法或通过修改内部行为来完全劫持它们。其机制基于以下代码段:

# Register our logger.
logging.setLoggerClass(OurLogger)
my_logger = logging.getLogger("main")

从这里开始,我们依靠自己的日志记录器,但仍能从所有其他日志记录设施中受益:

# We still need a loggin handler.
ch = logging.StreamHandler()
my_logger.addHandler(ch)

# Confgure a formatter.
formatter = logging.Formatter('LOGGER:%(name)12s - %(levelname)7s - <%(filename)s:%(username)s:%(funcname)s> %(message)s')
ch.setFormatter(formatter)

# Example main message.
my_logger.setLevel(DEBUG)
my_logger.warn("Hi mom!")

这个示例非常重要,因为它演示了如何在不使用自定义适配器或格式化程序的情况下注入两个数据位usernamefuncname

有关此解决方案的更多信息,请参见xlog.py repo。这是我基于其他问题和其他来源的代码片段准备的示例。


3

这一行

self.logger = logging.getLogger("myApp")

始终检索到相同日志记录器的引用,因此每次实例化MyLogger时都会向其中添加另一个处理程序。由于您两次使用不同的参数调用MyLogger,因此以下内容将修复当前实例。
self.logger = logging.getLogger(name)

但请注意,如果您多次传递相同的 name 参数,则仍将出现相同的问题。

您的类需要做的是跟踪其已配置的记录器。

class MyLogger(object):
    loggers = set()
    def __init__(self, name, format="%(asctime)s | %(levelname)s | %(message)s", level=INFO):
        # Initial construct.
        self.format = format
        self.level = level
        self.name = name

        # Logger configuration.
        self.console_formatter = logging.Formatter(self.format)
        self.console_logger = logging.StreamHandler(sys.stdout)
        self.console_logger.setFormatter(self.console_formatter)

        # Complete logging config.
        self.logger = logging.getLogger(name)
        if name not in self.loggers:
            self.loggers.add(name)
            self.logger.setLevel(self.level)
            self.logger.addHandler(self.console_logger)

这并不能让你重新配置一个记录器,但我留下了一个练习来找出如何正确地做到这一点。

需要注意的关键事项是,不能有两个具有相同名称的单独配置的记录器。


当然,logging.getLogger 总是返回给定名称的同一对象的引用,这意味着你的类与 logging 模块存在冲突。只需在程序启动时配置您的记录器,然后使用 getLogger 在必要时获取引用即可。


为什么它不允许我配置日志记录?我的目的是创建一个日志记录类,它将像 logging.Logger 对象一样行事,但在内部封装了不同的自定义选项。我知道有很多项目和帖子解释这个问题。我已经研究了一段时间。不幸的是,这些信息散乱且难以理解。因此,我正在尝试提出最基本的解决方案,为该概念提供基础,并允许进一步进行定制工作。 - mbilyanov
在最基本的层面上,你无法重新配置记录器,因为我将配置调用放在了一个 if 语句中,该语句仅在您尝试配置特定记录器的第一次运行时才会运行。对于处理程序之类的东西,您不是设置 the 处理程序,而是 添加 新的处理程序,这些处理程序会累积。 - chepner

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