如何在Python logging中使用不同的记录器而无需传递记录器变量?

6
我有多个模块使用相同的utils.py包。如何使utils.py中的记录器不同,而无需从调用者(即ClassA或ClassB)传递记录器变量?
以下是非常简单的示例代码。实际上,在utils.py中我有很多函数和类,这就是为什么我不想将logger变量传递到utils.py中的原因。
~/test-two-loggers$ tree .

├── main.py
├── configs.py
├── ClassA.py
├── ClassB.py
└── utils.py

0 directories, 5 files

main.py

import ClassA
import ClassB

ClassA.func()
ClassB.func()

ClassA.py

import utils
import configs
import logging

def func():
    logger = logging.getLogger("classA")
    logger.info("in ClassA")
    utils.common_func(logger)  # I want to change this line!!!!

ClassB.py

import utils
import configs
import logging

def func():
    logger = logging.getLogger("classB")
    logger.info("in ClassB")
    utils.common_func(logger)  # I want to change this line!!!!

utils.py

def common_func(logger):  # I want to change this line!!!!
    # do a lot of things ClassA and ClassB both need to do
    logger.info("in utils - step one finished")
    # do a lot of things ClassA and ClassB both need to do
    logger.info("in utils - step two finished")
    # do a lot of things ClassA and ClassB both need to do
    logger.info("in utils - step three finished")

configs.py

import logging.config

logging_config = {
        "version": 1, 
        "formatters": {
            "formatter_a": {
                "format": u"[A][%(levelname)s] %(module)s.%(lineno)d: %(message)s"
            },
            "formatter_b": {
                "format": u"[B][%(levelname)s] %(module)s.%(lineno)d: %(message)s"
            },
        },
        "handlers": {
            "console_a": {
                "class": "logging.StreamHandler",
                "level": "DEBUG",
                "formatter": "formatter_a",
                "stream": "ext://sys.stdout"
            },
            "console_b": {
                "class": "logging.StreamHandler",
                "level": "DEBUG",
                "formatter": "formatter_b",
                "stream": "ext://sys.stdout"
            },
        },
        "loggers": {
            "classA": {
                "level": "DEBUG",
                "handlers": ["console_a"],
                "propagate": "no"
            },
            "classB": {
                "level": "DEBUG",
                "handlers": ["console_b"],
                "propagate": "no"
            },
        },
}

logging.config.dictConfig(logging_config)

Result I want:

~/test-two-loggers$ python main.py 
[A][INFO] ClassA.7: in ClassA
[A][INFO] utils.3: in utils - step one finished
[A][INFO] utils.5: in utils - step two finished
[A][INFO] utils.7: in utils - step three finished
[B][INFO] ClassB.7: in ClassB
[B][INFO] utils.3: in utils - step one finished
[B][INFO] utils.5: in utils - step two finished
[B][INFO] utils.7: in utils - step three finished

但是我希望有另一种解决方案。我不想把logger变量传递给utils


1
你的代码有点异味。如果你有一个实用模块,似乎应该从实用命名空间记录日志。如果你有一些理由想要使用堆栈跟踪,你可以创建自己的格式化程序来处理找出它到底是从哪里调用的。这里提供了示例。 - Wayne Werner
1
@WayneWerner 如果您有一个实用程序模块,似乎应该从实用程序命名空间记录日志。但是,如果没有上下文(无论是来自ClassA还是ClassB,如果在实用程序中发生错误,则是由哪个调用者引起的以及在实用程序被调用之前在ClassA或ClassB中执行的操作),则实用程序中的日志将不太有意义。但我想这也是我们可以尝试的一种哲学。也许我们可以使实用程序中的日志独立,并在ClassA / B中添加日志以显示实用程序中的结果摘要。这肯定是一个选项。 - AnnieFromTaiwan
那绝对是我的首选 - 重新设计你的应用程序来跳过奇怪的日志记录是很少一个好主意。您还可以在try/except中包装fn调用并使用logger.exception('problem with utility'),它甚至会友好地包含traceback信息。因此,这是一种处理异常情况的方便方法。 - Wayne Werner
好奇为什么您不想传递“logger”变量。 - starflyer
4个回答

1

看起来你正在寻找类似于隐式参数的东西。

这是Python所没有的(显式优于隐式)。

但是,像往常一样,有一种更或多或少优雅的方法来模拟它:

class LoggerWrapper:
    def __init__(self, logger_name):
        self.logger = logging.getLogger(logger_name)

    def common_func(self):
        pass # do stuff here

logger = LoggerWrapper('classA')
logger.common_func()

嗨,这是我正在思考的方法!我认为不应该称其为 LoggerWrapper,而应该称其为 UtilsWithLogger。这确实是一个答案,但有两个缺点。(1)在 ClassAClassB 中都需要初始化 LoggerWrapper(比当前解决方案好,但不完美)(2)我实际上在 utils.py 中有多个类(命名空间目的)。如果我采用这种方法,我将不得不处理嵌套类,或者如果我不想处理嵌套类,我将不得不在 ClassAClassB 中初始化 utils 中的所有类。如果有其他方法我会等待。 - AnnieFromTaiwan
我不明白你的第二个问题... 你能换一种方式解释一下吗? - GingerPlusPlus
嗨,关于(2):在utils.py中,有多个类,例如class CrawlerUtils - staticmethods crawl_json(),crawl_url() / class DbUtils - staticmethods insert_with_md5(),my_insert_many() / class OtherUtils...。因此,如果我使用这个包装器方法,我要么(a)在这些原始类上方添加一个父类,然后我需要处理嵌套类中的self.logger;要么(b)在ClassA和ClassB内部执行这些初始化步骤,如crawler_utils=CrawlerUtilsWithLogger('classA'),db_utils=DbUtilsWithLoggers('classA'),another_utils=...etc - AnnieFromTaiwan

0
如果您需要在util.common_func内部使用记录器,无需传递它。您可以使用logging.getLoggerutils.common_func中检索您的记录器。只要传递相同的字符串(例如classA),logging.getLogger将返回相同的日志记录对象。
logger1 = logging.getLogger('classA')
def func():
   logger2 = logging.getLogger('classA')
   print logger1 is logger2 #True

来自文档

请注意,Logger 永远不会直接实例化,而是始终通过模块级别的函数 logging.getLogger(name) 进行调用。对具有相同名称的 getLogger() 的多次调用将始终返回对同一 Logger 对象的引用。


嗨,但是既有classA也有classB都调用了utils.common_func...... 因此问题就出在这里。 - AnnieFromTaiwan

0

最简单的解决方案,假设您没有线程化应用程序,就是直接monkeypatch模块属性:

util.py

import logging

logger = logging.getLogger(__name__)


def do_something():
    logger.info('Doing something')

whatever.py

import logging
from contextlib import contextmanager


@contextmanager
def patch_logger(module, logger):
    '''
    Patch the given ``module``'s logger with
    the provided ``logger``. Return the module's
    original logger when we're done.
    '''

    prev_logger = module.logger
    try:
        module.logger = logger
        yield
    finally:
        module.logger = prev_logger


class ClassA:
    def __init__(self):
        self.logger = logging.getLogger('A')

    def func(self):
        self.logger.info('Starting A')
        with patch_logger(util, self.logger):
            util.do_something()


class ClassB:
    def __init__(self):
        self.logger = logging.getLogger('B')

    def func(self):
        self.logger.info('Starting B')
        with patch_logger(util, self.logger):
            util.do_something()

猴子补丁是不被赞同的,有很好的理由。


-1

您可以更改格式化程序并使用extra关键字参数将额外的字典参数传递给日志记录消息。这使您能够传递任何您想要假装从中调用记录器的“模块”。

因此,将格式化程序更改为:

"[A][%(levelname)s] %(module)s.%(lineno)d: %(message)s"

to:

"[A][%(levelname)s] %(mymodule)s.%(lineno)d: %(message)s"

然后调用您的函数:

logger.info("in utils", extra={'mymodule':'somemodule'})

如果您想使用调用模块本身,将'somemodule'更改为__name__

我也考虑过可能只需覆盖module的值(这样您必须更改格式化程序),但是logging不允许这样做,因此似乎您必须更改格式化程序。


编辑:

为了让它更加清晰,你在 ClassA.py 中的 func() 应该改成:

def func():
    logger = logging.getLogger("classA")
    logger.info("in ClassA", extra={'mymodule':__name__)
    logger.info("in utils", extra={'mymodule':'utils')
    utils.common_func() #call the function without passing the logger

在你的logging_config字典中,你应该将logging_config['formatters']['formatter_a']['format']中的字符串module改为mymodule

对于ClassB也应该做同样的修改。显然,你还应该删除使用loggercommon_func行。


更多参考:


你好,我不是很理解你的方法。你能把需要修改的所有代码都粘贴一下吗?你确定这个修改后的代码可以生成与旧代码完全相同的输出结果吗? - AnnieFromTaiwan
嗨,我现在明白你的方法了,但它并不符合我的需求。我写的只是一个简单的示例。utils及其函数不应该被删除并在不同的模块中重复。实际上,在utils.py中有许多方法,这就是为什么这两个类共享utils的原因。删除utils.py并将utils.py中的函数放回ClassAClassB中会导致一个项目中出现许多重复代码。 - AnnieFromTaiwan
你将不再需要你的utils.common_func了,但我需要那个函数。 - AnnieFromTaiwan
嗨,我已经编辑了我的问题并在其中添加了一条评论。请查看 -> #做很多事情ClassA和ClassB都需要做。抱歉没有表达清楚。 - AnnieFromTaiwan
已编辑答案。 - Gustavo Bezerra
显示剩余3条评论

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