在编写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)
这种方法有不利之处吗?我唯一能想到的就是创建一个新的记录器可能会很昂贵,但是实际上是这样吗?我没有看到任何陷阱吗?
答案 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 附加到日志消息。