
本文介绍一种通过替换 pytest 内置日志插件、注入 structlog 处理链的方式,使 pytest 捕获的日志(而非 stdout/stderr)也能完整显示 logging.Logger.info(..., extra={...}) 中动态传入的键值对,解决默认格式器忽略 extra 字段的问题。
本文介绍一种通过替换 pytest 内置日志插件、注入 structlog 处理链的方式,使 pytest 捕获的日志(而非 stdout/stderr)也能完整显示 `logging.logger.info(..., extra={...})` 中动态传入的键值对,解决默认格式器忽略 `extra` 字段的问题。
在使用 structlog 与标准库 logging 混合架构时,常通过 extra= 参数向日志注入运行时上下文(如 {"request_id": "abc123", "user_id": 42})。这类字段在应用正常运行时能被 structlog.stdlib.ExtraAdder() 和 ConsoleRenderer 正确提取并渲染为 key='value' 形式。但当测试运行于 pytest 下并启用 --log-cli-level 时,pytest 会接管日志捕获流程,使用其内部 _pytest.logging.LoggingPlugin 创建的 Formatter —— 该格式器不感知 extra 字典结构,仅支持预设字段(如 %(name)s, %(levelname)s),导致所有动态上下文丢失。
直接配置 log_cli_format 无法解决问题,因为标准 logging.Formatter 的格式字符串(如 '%(message)s %(foo)s')要求字段名硬编码,而 extra 中的键是动态、不可预知的。因此,必须绕过原生 Formatter,改用 structlog 的处理器链(ProcessorFormatter),它能自动遍历 event_dict 并将全部剩余键值对格式化输出。
实现方案是在项目根目录的 conftest.py 中重写 pytest 日志插件:
# conftest.py
import _pytest.logging
import logging
import structlog.dev
import structlog.stdlib
import structlog.processors
import structlog.typing
def drop_structlog_fields(
logger: logging.Logger | structlog.typing.FilteringBoundLogger,
method_name: str,
event_dict: structlog.types.EventDict,
) -> structlog.types.EventDict:
"""清理 structlog 特有元字段,避免污染最终日志输出"""
_ = logger, method_name
_ = event_dict.pop("_from_structlog", None)
_ = event_dict.pop("_record", None)
_ = event_dict.pop("message", None) # message 已由 ConsoleRenderer 单独处理
return event_dict
class MyLoggingPlugin(_pytest.logging.LoggingPlugin):
def _create_formatter(self, log_format, log_date_format, auto_indent):
# 使用 ProcessorFormatter 替代原生 Formatter
return structlog.stdlib.ProcessorFormatter(
processors=[
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.stdlib.ExtraAdder(), # 将 logging.extra → event_dict
drop_structlog_fields, # 清理冗余字段
structlog.dev.ConsoleRenderer(), # 格式化为 'key=value' 形式
]
)
def pytest_configure(config):
# 替换 pytest 默认日志插件类(注意:依赖内部 API,需关注 pytest 版本兼容性)
log_plugin = config.pluginmanager.get_plugin("logging")
if log_plugin is not None:
log_plugin.LoggingPlugin = MyLoggingPlugin
✅ 效果验证:运行 pytest --log-cli-level INFO 后,原输出:
Miller (mlr) 是一个命令行工具,用于查询、整形和重新格式化名称索引数据,如 CSV、TSV、JSON 和 JSON Lines。它将 awk、sed、cut、join 和 sort 的功能整合到一个专为结构化数据处理而构建的单一工具中。
----------------------------------------- Captured log call ----------------------------------------- INFO root:test_me.py:8 message here
将变为:
----------------------------------------- Captured log call ----------------------------------------- [info ] message here [root] baz='qux' foo='bar'
⚠️ 注意事项:
- 此方案依赖
_pytest.logging.LoggingPlugin等内部类,属于 pytest 实现细节,未来版本可能变更接口。建议在pyproject.toml或setup.cfg中锁定兼容的 pytest 版本(如pytest>=7.4,),并在升级前回归测试。 -
ConsoleRenderer默认启用颜色和缩进;若需纯文本(如 CI 日志),可替换为structlog.dev.ConsoleRenderer(colors=False, pad_event=0)。 - 若项目未使用
structlog,可自行实现轻量级处理器(如遍历event_dict构造f"{' '.join(f'{k}={repr(v)}' for k, v in event_dict.items())}"),但structlog.dev.ConsoleRenderer已提供健壮、可扩展的解决方案。
综上,通过定制 LoggingPlugin 并集成 ProcessorFormatter,我们成功将动态 extra 字段无缝注入 pytest 的捕获日志流,兼顾灵活性与可维护性,是 structlog + pytest 场景下的推荐实践。










