为每个异步函数调用创建一个新的记录器,好主意吗?

时间:2018-12-27 13:55:51

标签: python logging python-asyncio

在编写Python asyncio程序时,通常会有一个异步函数,该函数具有许多并发运行的调用。我想向此函数添加一些日志记录,但是来自不同调用的日志记录输出将交织在一起,因此很难遵循。我当前的解决方案是以某种方式为每次调用创建一个唯一的名称,并每次记录该名称,如下所示:

async def make_request(args):
    logger = logging.getLogger('myscript.request')
    log_name = unique_name()
    logger.debug('[%s] making request with args %r', log_name, args)
    response = await request(args)
    logger.debug('[%s] response: %r', log_name, response)

但是,必须在每个记录调用中放入log_name会很快使人感到疲劳。为了保存这些击键,我想出了一个不同的解决方案,创建一个新的记录器,为每次调用创建一个唯一的名称:

async def make_request(args):
    logger = logging.getLogger(f'myscript.request.{unique_name()}')
    logger.debug('making request with args %r', args)
    response = await request(args)
    logger.debug('response: %r', response)

这种方法有不利之处吗?我唯一能想到的就是创建一个新的记录器可能会很昂贵,但是实际上是这样吗?我没有看到任何陷阱吗?

3 个答案:

答案 0 :(得分:2)

  

[为每个协程创建一个新的记录器]有不利之处吗?

除了创建记录器的可能代价外,另一个缺点是,您创建的记录器永远与唯一名称关联,并且永远不会销毁,因此您实际上会发生内存泄漏。文档明确承诺:

  

对具有相同名称的getLogger()的多次调用将始终返回对同一Logger对象的引用。

我建议您只是硬着头皮,创建一个具有所需功能的助手。基于Brad Solomon的答案,包装器可能看起来像这样(未经测试):

import itertools, weakref, logging

logging.basicConfig(format='%(asctime)-15s %(task_name)s %(message)s')

class TaskLogger:
    _next_id = itertools.count().__next__
    _task_ids = weakref.WeakKeyDictionary()

    def __init__(self):
        self._logger = logging.getLogger('myscript.request')

    def _task_name(self):
        task = asyncio.current_task()
        if task not in self._task_ids:
            self._task_ids[task] = self._next_id()
        return f'task-{self._task_ids[task]}'

    def debug(self, *args, **kwargs):
        self._logger.debug(*args, task_name=self._task_name(), **kwargs)

    # the same for info, etc.

logger = TaskLogger()

答案 1 :(得分:1)

您可能不想考虑创建新的记录器,而是考虑通过extra参数在日志消息中使用custom attributes

例如:

FORMAT = '%(asctime)-15s %(unique_name)s %(message)s'
# [Configure/format loggers & handlers]

然后在协程调用中记录调试级别消息,如下所示:

logger.debug('making request with args %r', args, extra={'unique_name': unique_name())

请记住另一件事:unique_name()可能会花费很多,如果您提出很多要求。通过多处理创建并发时的常见模式是通过os.getpid()记录调用进程ID。使用asyncio,也许很粗糙的表亲将是当前Task的一些标识符,您可以通过asyncio.current_task()来找到它。每个任务都有一个_name属性,该属性应该是唯一的,因为它会调用一个递增的_task_name_counter()

class Task(futures._PyFuture): # Inherit Python Task implementation
    def __init__(self, coro, *, loop=None, name=None):
    # ...
        if name is None:
            self._name = f'Task-{_task_name_counter()}'
        else:
            self._name = str(name)

答案 2 :(得分:0)

为了结束这个老问题,在关注 Vinay Sajip's comment about LoggerAdapter 之后,我实际上在文档中找到了我想要的内容。 Quoting from the docs

<块引用>

如果您需要不同的 方法,例如如果您想添加或附加上下文 信息到消息字符串,你只需要子类化 LoggerAdapter 并覆盖 process() 以执行您需要的操作。这里有一个 简单例子:

class CustomAdapter(logging.LoggerAdapter):
    """
    This example adapter expects the passed in dict-like object to have a
    'connid' key, whose value in brackets is prepended to the log message.
    """
    def process(self, msg, kwargs):
        return '[%s] %s' % (self.extra['connid'], msg), kwargs

你可以这样使用:

logger = logging.getLogger(__name__)
adapter = CustomAdapter(logger, {'connid': some_conn_id})

那么您记录到适配器的任何事件都将具有以下值 some_conn_id 附加到日志消息。