创建一个日志记录器,可以在日志消息前添加前缀

9
假设我有一个客户端和服务器类:
```html

假设我有一个客户端和服务器类:

```
import logging

class Client:
    def __init__(self, name):
        self.logger = logging.getLogger(self.__class__.__name__)
        self.name = name

    def foo(self):
        self.logger.warn('[%s] foo', self.name)

class Server:
    def __init__(self):
        self.logger = logging.getLogger(self.__class__.__name__)

    def bar(self):
        self.logger.warn('bar')

我该如何为客户端制作一个日志记录器,使其自动处理[self.name]前缀?我唯一的想法是设置一个包括%(client)前缀的全局格式,并为Client使用自定义过滤器。这似乎过于复杂和全局化。我感觉应该有一个简单的方法,只是我没有看到。

7个回答

17

可以使用自定义的LoggerAdapter来完成此操作,无需任何其他依赖项:

import logging

class LoggerAdapter(logging.LoggerAdapter):
    def __init__(self, prefix, logger):
        super(LoggerAdapter, self).__init__(logger, {})
        self.prefix = prefix

    def process(self, msg, kwargs):
        return '[%s] %s' % (self.prefix, msg), kwargs

class Client:
    def __init__(self, name):
        logger = logging.getLogger(self.__class__.__name__) 
        self.logger = LoggerAdapter(name, logger)
        self.name = name

    def foo(self):
        self.logger.warning('foo: %s', 'bar')

client = Client('client1')
logging.basicConfig(format='%(message)s')
client.foo()

应该打印出

[client1] foo: bar

7

使用LoggerAdapter:

logger = PrefixLoggerAdapter(logger, dict(prefix='username'))
logger.info('hey')  
# INFO [username] hey

这是该类的定义:

class PrefixLoggerAdapter(logging.LoggerAdapter):
    """ A logger adapter that adds a prefix to every message """
    def process(self, msg: str, kwargs: dict) -> (str, dict):
        return (f'[{self.extra["nickname"]}] ' + msg, kwargs)

4
这可以使用logaugment库来完成(免责声明:我制作了这个库以使其更容易,但也可以使用标准库完成)。
在记录器的Formatter中填写客户名称,并将消息添加到前缀中,以将日志记录格式化为字符串。
创建一个记录器:
import logging

logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = logging.Formatter("%(client)s: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)

为此记录器指定客户端名称(即,在上述格式化日志字符串中填写%(client)s的值):

logaugment.set(logger, client='Client')

从现在开始,您可以像平常一样调用日志记录方法,并且客户端的名称将自动填写:

logger.warn("My message")

我需要在客户端记录器中本地指定处理程序才能应用格式化程序吗?这似乎会阻止我全局设置处理程序(例如,应用程序决定日志是否进入文件或标准输出)。 - Zulan
@Zulan:如果记录器已经有处理程序,则可以更改该处理程序的格式化程序。因此,您确实可以为所有记录器全局创建处理程序,然后稍后修改格式化程序。 - Simeon Visser

3
另一种解决方法是使用“extra”字典来指定前缀。
首先,像平常一样创建您的记录器,例如:
logger = logging.getLogger(__name__)
# Add any custom handlers, formatters for this logger
myHandler = logging.StreamHandler()
myFormatter = logging.Formatter('%(myApp)s - %(message)s') # Note the myApp key here
myHandler.setFormatter(myFormatter)
logger.addHandler(myHandler)
logger.setLevel(logging.INFO)

请看我们如何按照格式添加了一个名为myApp的新键。接着,在客户端,我们创建了一个LoggerAdapter,并指定了包含自定义值的extra字典用于myApp:
logger = logging.LoggerAdapter(logger, extra={'myApp': 'Client'})
logger.info('All your base are belong to us')

这将会打印出:

Client - All your base are belong to us

同样地,从服务器上您可以按如下方式进行登录:

logger = logging.LoggerAdapter(logger, extra={'myApp': 'Server'})
logger.info('All your base are belong to us')

这将打印出以下内容:
Server - All your base are belong to us

0

我想在C#世界中使用类似于PrefixedLog的东西,包装日志实例,但在Python世界中,日志配置不同(而不是传递一个实例,似乎通常调用getLogger并依赖于为您的库定义的处理程序)。

Vitaly的答案很好,但如果您尝试链接适配器,则会失败,因为logging.LoggerAdapter未继承自logging.Logger。我的解决方案是:

import logging

class PrefixedLogger(logging.LoggerAdapter):
    def __init__(self, prefix, logger):
        prefix = "[{}]".format(prefix)

        #hack because LoggerAdapter in not inherited from Logger
        if isinstance(logger, PrefixedLogger):
            inner_prefix = logger.prefix
            inner_logger = logger.logger
            prefix = "{} {}".format(inner_prefix, prefix)
            logger = inner_logger

        super(PrefixedLogger, self).__init__(logger, {})
        self.logger = logger
        self.prefix = prefix

    def process(self, msg, kwargs):
        return '%s %s' % (self.prefix, msg), kwargs

0
另一种方法是更新当前处理程序以具有新的格式。 这将在输出中添加一个pytest函数名称:
@pytest.fixture(scope="function", autouse=True)
def setLoggerPrefix(request):
    ''' add the test name to the logging messages '''

    # Prepare the test name.
    testName = request.function.__name__[len("test_"):]

    logger = logging.getLogger()
    myFormatter = logging.Formatter('%(levelname)s:[' + testName + '] - %(message)s')

    # Set each logger to have this new format
    myHandler = logging.StreamHandler()
    myHandler.setFormatter(myFormatter)
    for handler in logger.handlers:
        handler.setFormatter(myHandler)

现在的输出看起来像:

INFO:[validate_page] - <some message here>
INFO:[validate_page] - <some other message here>
PASSED                                                                   [ 14%]
------------------------------ live log teardown ------------------------------
INFO:[validate_all_pages] - <some teardown message here>

tests/application/test_users/test_create_users.py::test_create_users
------------------------------- live log setup --------------------------------
INFO:[create_users] - <some message here>
INFO:[create_users] - <some other message here>
..

0
LoggerAdapter仅适用于直接发送给记录器的消息,而不适用于子记录器发送的消息。
如果您希望将前缀应用于来自外部模块的所有日志消息,可以执行以下操作:
import logging
class PrefixFormatter(logging.Formatter):
    def __init__(self, prefix, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.prefix = prefix
    def format(self, record):
        if record.name == "externallib" or record.name.startswith("externallib."):
            record.msg = f"{self.prefix}{record.msg}"
        return super().format(record)
    
handler = logging.StreamHandler()
handler.setFormatter(PrefixFormatter("externallibprefix:"))
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.addHandler(handler)

# in the "externallib" library
logging.getLogger("externallib").info("from externallib")
logging.getLogger("externallib.sub").info("from externallib.sub")

# in the main script
root_logger.info("from main")

这不会改变外部库本身注册的任何处理程序。

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