Python记录上下文

Sta*_*asM 9 python logging

我在python应用程序中有一个日志设置,它登录到文件和MongoDB.设置如下所示:

[logger_root]
handlers=myHandler,mongoHandler
level=DEBUG
qualname=myapp

[handler_myHandler]
class=handlers.RotatingFileHandler
level=DEBUG
formatter=myFormatter
args=('myapp.log', 'a',20000000,10)

[handler_mongoHandler]
class=myapp.MongoLogger.MongoLogger
level=INFO
args=('log',)

[formatter_myFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
Run Code Online (Sandbox Code Playgroud)

而MongoLogger的emit()函数如下:

def emit(self, record):
    logdata = record.__dict__
    try:
        if(self.data == None):
            self.initDb()
        self.logtable.insert(logdata)
    except:
       self.handleError(record)
Run Code Online (Sandbox Code Playgroud)

日志记录就像这样完成:

 logger.info("Processing account %s..." % account)
Run Code Online (Sandbox Code Playgroud)

它工作得相当好,但现在我还有一个额外的要求.我希望它有一些上下文 - 即,能够定义自定义值 - 比如帐户名 - 所以在帐户处理中完成的每个日志都会将帐户名称作为record传递给emit上面的一部分,并且也可用于格式化程序myFormatter串.

是否可以使用日志记录模块执行此操作?有没有其他更好的方法可以做同样的事情?

Ant*_*off 14

获得帐户名后,您可以在帐户处理代码中定义一个功能,如下所示:

# account_name should already be defined
log = lambda msg: logger.info(msg, extra={'account': account_name})

###

log('Processing account...')
Run Code Online (Sandbox Code Playgroud)

请注意extra关键字参数.它用于向日志记录添加其他上下文 - 在本例中为帐户名称.

您可以使用extra格式化程序中传递的上下文:

format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(account)s'
Run Code Online (Sandbox Code Playgroud)

请注意,如果您设置这样的格式化程序并忘记传递account,您将获得字符串格式化异常.

  • LoggingAdapter也可以帮助执行lambda的操作,请参见此处:http://docs.python.org/howto/logging-cookbook.html#context-info (2认同)

Jam*_*hra 7

请原谅自我推销,但我已经编写了一个名为 的 Python 包log-with-context来解决这个问题。您可以在PyPIGitHub上找到它。以下是如何使用log-with-context任意数量的线程本地上下文值来记录消息。

1. 安装log-with-context日志格式化程序。

您必须log-with-context与显示字段的不同记录器配对extra。我最喜欢的日志格式化程序是JSON-log-formatter

您可以使用 pip 安装这两个软件包:

pip3 install log-with-context JSON-log-formatter
Run Code Online (Sandbox Code Playgroud)

2. 配置日志格式化程序。

以下是如何配置JSON-log-formatter打印 JSON 日志INFO以及更糟糕的标准错误。

import logging.config

logging.config.dictConfig({
    "version": 1,
    "disable_existing_loggers": True,
    "formatters": {
        "json": {"()": "json_log_formatter.JSONFormatter"},
    },
    "handlers": {
        "console": {
            "formatter": "json",
            "class": "logging.StreamHandler",
        }
    },
    "loggers": {
        "": {"handlers": ["console"], "level": "INFO"},
    },
})
Run Code Online (Sandbox Code Playgroud)

3. 使用创建一个新的记录器log-with-context

然后,以下是创建记录器的方法:

from log_with_context import add_logging_context, Logger

LOGGER = Logger(__name__)
Run Code Online (Sandbox Code Playgroud)

4. 使用add_logging_context上下文管理器记录消息。

您可以使用上下文管理器推送和弹出上下文键和值add_logging_context。以下是一些示例代码,展示如何使用上下文管理器:

with add_logging_context(current_request="hi"):
    LOGGER.info("Level 1")

    with add_logging_context(more_info="this"):
        LOGGER.warning("Level 2")

    LOGGER.info("Back to level 1")

LOGGER.error("No context at all...")
Run Code Online (Sandbox Code Playgroud)

这就是上面的日志消息以 JSON 格式打印时的样子:

{"current_request": "hi", "message": "Level 1", "time": "2021-08-03T21:29:45.987392"}
{"current_request": "hi", "more_info": "this", "message": "Level 2", "time": "2021-08-03T21:29:45.988786"}
{"current_request": "hi", "message": "Back to level 1", "time": "2021-08-03T21:29:45.989178"}
{"message": "No context at all...", "time": "2021-08-03T21:29:45.989600"}
Run Code Online (Sandbox Code Playgroud)


pet*_*lin 5

Tony 的回答只是提供了一个函数,但是使用LoggerAdapter我们可以获得上下文记录器。

class ILoggerAdapter(LoggerAdapter):
    def __init__(self, logger, extra):
        super(ILoggerAdapter, self).__init__(logger, extra)
        self.env = extra

    def process(self, msg, kwargs):
        msg, kwargs = super(ILoggerAdapter, self).process(msg, kwargs)

        result = copy.deepcopy(kwargs)

        default_kwargs_key = ['exc_info', 'stack_info', 'extra']
        custome_key = [k for k in result.keys() if k not in default_kwargs_key]
        result['extra'].update({k: result.pop(k) for k in custome_key})

        return msg, result
Run Code Online (Sandbox Code Playgroud)

然后只需将您的记录器包装为

new_logger = ILoggerAdapter(old_logger, extra={'name': 'name'})

# new_logger is just same as old_logger    
# but can be pass to record
new_logger.info(msg='haha', id=100)

def emit(self, record):
    print(record.name)
    print(record.id)
Run Code Online (Sandbox Code Playgroud)


Mar*_*man 5

Python 官方文档 ( logging coockbook ) 提出了两种向日志添加上下文信息的方法:

  1. 使用LoggerAdapters - 有关更多详细信息,您可以参考pete lin 的回答
  2. 使用过滤器(和线程局部变量) - 过滤器在记录被发出之前对其进行处理。它的主要目的是允许高级和自定义规则拒绝日志记录(该filter方法返回 bool,指示是否发出记录)。但是,它还允许您处理记录 - 并根据需要添加属性。例如,您可以根据全局threading.local变量设置属性。

下面是一个Filter从全局threading.local变量附加属性的示例:

log_utils.py

import logging
import threading

log_context_data = threading.local()


class ThreadingLocalContextFilter(logging.Filter):
    """
    This is a filter which injects contextual information from `threading.local` (log_context_data) into the log.
    """
    def __init__(self, attributes: List[str]):
        super().__init__()
        self.attributes = attributes

    def filter(self, record):
        for a in self.attributes:
            setattr(record, a, getattr(log_context_data, a, 'default_value'))
        return True
Run Code Online (Sandbox Code Playgroud)

log_context_data可以在开始处理帐户时设置,并在完成后重置。但是,我建议使用上下文管理器设置它:

同样在log_utils.py 中

class SessionContext(object):
    def __init__(self, logger, context: dict = None):
        self.logger = logger
        self.context: dict = context

    def __enter__(self):
        for key, val in self.context.items():
            setattr(log_context_data, key, val)
        return self

    def __exit__(self, et, ev, tb):
        for key in self.context.keys():
            delattr(log_context_data, key)
Run Code Online (Sandbox Code Playgroud)

还有一个用法示例,my_script.py

root_logger = logging.getLogger()
handler = ...
handler.setFormatter(
    logging.Formatter('{name}: {levelname} {account} - {message}', style='{'))
handler.addFilter(ThreadingLocalContextFilter(['account']))
root_logger.addHandler(handler)
...
...
using SessionContext(logger=root_logger, context={'account': account}):
    ...
    ...
     
Run Code Online (Sandbox Code Playgroud)

笔记:

  1. Filter仅适用于logger它所连接的对象。因此,如果我们将其附加到 a logging.getLogger('foo'),则不会影响logging.getLogger('foo.bar')。解决方案是将 附加Filter到 a Handler,而不是 a logger
  2. ThreadingLocalContextFilter如果log_context_data不包含所需的属性,则可能拒绝记录。这取决于你需要什么。

什么时候用什么?

  1. 如果您需要为特定的 looger实例编辑记录,我会推荐LoggerAdapters - 在这种情况下,实例化一个 Adapter 是有意义的。
  2. 如果您想编辑由特定处理程序处理的所有记录(包括其他模块和 3rd 方包),我建议使用Filter。在我看来,这通常是一种更简洁的方法,因为我们只在我们的入口代码中配置了我们的记录器 - 其余代码保持不变(不需要用适配器实例替换记录器实例)。

  • 您可以考虑使用 ContextVar 而不是 threading.local() 来跨异步调用保留日志上下文数据 (2认同)