Python logging 怎样用 contextvars 注入请求标识
在异步服务里,最实用的做法是把请求标识放进 ContextVar,在请求入口调用 set(),再让日志处理器上的 Filter 从当前上下文取值并写入 LogRecord。这样业务函数不用层层传递 request_id,同一事件循环中的并发任务也不会因为共享全局变量而串号。
官方文档:https://docs.python.org/3/library/contextvars.html
下面的示例使用标准库完成这件事,关键是三点:请求开始时保存 set() 返回的 token,请求结束时在 finally 中恢复;把注入逻辑放到 Handler 的过滤器上;为请求外日志准备一个安全的默认值。

图1:请求标识从请求边界进入上下文,再由日志过滤器写入记录字段的原创结构说明图。
先把请求范围和日志记录分开
ContextVar 保存的是当前上下文中的值,不是一个供所有协程读写的普通全局变量。Python 文档说明它适合上下文局部状态,并且原生支持 asyncio。因此可以让请求入口负责设置值,业务层只负责记录事件,日志层再负责把值补到记录中。
变量应该声明一次,并给出默认值。默认值不是用来掩盖调用错误,而是为了让启动日志、定时任务和异常处理日志在没有请求上下文时仍然能够被格式化。
import logging
from contextvars import ContextVar
# 请求外产生的日志使用短占位符,避免 Formatter 找不到字段。
request_id_var = ContextVar("request_id", default="-")
logger = logging.getLogger("demo")
logger.setLevel(logging.INFO)
用 Handler Filter 统一补上 request_id
过滤器不一定只做丢弃判断,也可以修改传给 Handler 的 LogRecord。把过滤器安装到 Handler 上,来自子 logger 的记录也能在输出前经过同一层注入;如果只把过滤器挂到某个父 Logger 上,子 Logger 产生的记录未必会经过那个 Logger 的过滤器。
class RequestContextFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
# 由日志层读取当前请求上下文,不要求每个调用点传 extra。
record.request_id = request_id_var.get()
return True
handler = logging.StreamHandler()
# 格式化器始终能拿到 request_id,包括脱离请求的启动日志。
handler.setFormatter(logging.Formatter(
"%(levelname)s [request_id=%(request_id)s] %(message)s"
))
handler.addFilter(RequestContextFilter())
logger.addHandler(handler)
这里的过滤器只负责添加字段,不改变日志级别,也不把请求对象本身塞进记录。记录中保留字符串形式的标识,通常更适合文本日志、集中式采集和按字段检索。
在请求入口 set,在 finally 中 reset
set() 返回的 token 代表设置前的状态。保存它并在 finally 中调用 reset(),可以保证正常返回和异常退出都恢复上下文。不要只写 set() 而省略恢复,否则复用执行资源的后续任务可能读到上一个请求的标识。
import asyncio
async def load_profile(user_id: int) -> dict:
# 模拟会继续记录日志的下游协程。
logger.info("加载用户资料 user_id=%s", user_id)
await asyncio.sleep(0)
return {"user_id": user_id}
async def handle_request(request_id: str, user_id: int) -> None:
# token 让本次请求可以精确恢复进入前的上下文。
token = request_id_var.set(request_id)
try:
logger.info("请求开始")
await load_profile(user_id)
logger.info("请求完成")
except Exception:
# 记录异常时仍在请求上下文内,便于按 request_id 检索。
logger.exception("请求失败")
raise
finally:
# 无论成功还是失败,都不能把标识留给后续工作。
request_id_var.reset(token)
async def main() -> None:
# 每个 Task 在创建时取得当前上下文的副本;这里分别设置请求标识。
await asyncio.gather(
handle_request("R-2048", 7),
handle_request("R-2051", 8),
)
if __name__ == "__main__":
asyncio.run(main())
并发执行时,两条请求日志应分别带有 R-2048 和 R-2051。这里没有把 request_id 放到模块级可变字符串里,因为那种写法会让任务之间互相覆盖。
create_task 的继承和后台任务边界
asyncio 的 Task 默认会复制创建时的当前上下文,所以在请求入口设置标识后创建的子任务通常可以读到同一个值。这个行为适合一个请求内部的并发工作,例如同时查缓存和数据库。
但“能继承”不等于“生命周期自动正确”。如果创建了会在请求返回后继续运行的后台任务,就要明确它是否仍属于原请求:属于原请求时可以等待它结束;不属于原请求时应显式建立新的上下文,或者给它单独的任务标识,避免把已结束请求的标识带到长期任务里。

图2:set/reset 控制请求范围,Task A 和 Task B 各自携带标识;这是上下文关系说明图,不是运行截图。
Filter、LoggerAdapter 和 LogRecord 工厂怎么选
如果目标是让一组 Handler 的输出都带上当前请求标识,Handler Filter 通常最直接:它靠近输出边界,不要求业务函数修改调用方式。若只有少量业务入口需要固定附加字段,可以使用 LoggerAdapter;但它更像调用侧的包装,第三方库发出的日志不会自动经过你的 adapter。
全局 logging.setLogRecordFactory() 也能注入字段,不过它会影响进程内所有日志记录,适合有明确统一规范的基础设施层。只为一个服务或一个 Handler 添加 request_id 时,先用过滤器更容易控制影响范围。
常见坑点
- 不要在 Formatter 里直接假设字段一定存在;请求外日志会导致
KeyError,应在过滤器中写入默认值。 - 不要把过滤器只挂到父 Logger,然后认为所有后代记录都会自动被它修改;输出 Handler 是更稳定的注入边界。
- 不要在请求结束时只清空字符串;使用对应 token 的
reset()才能恢复嵌套上下文。 - 不要把密码、完整 Authorization 头或身份证明等敏感信息放入 request_id;标识应短小、可检索且不可承载秘密。
小结
Python logging 搭配 contextvars 的最小配方是:用 ContextVar(default="-") 保存请求标识,在请求边界 set(),把返回的 token 放进 finally,再由 Handler Filter 将 get() 的结果写到 LogRecord。这样既保留了业务日志调用的简洁性,也能适应 asyncio 的并发上下文。
相关问题
contextvars 能替代所有全局配置吗? 不能。它适合请求、任务或调用链范围的临时上下文,不适合承载需要跨请求共享的配置和连接池。
为什么不用 threading.local? 线程本地变量只按线程隔离;多个 asyncio Task 可以在同一线程交替运行,ContextVar 更符合任务上下文的边界。
为什么日志里出现 request_id=-? 说明记录产生时没有进入请求上下文,或者调用发生在 reset 之后。先判断这是不是启动任务、定时任务或真正丢失了请求边界。
Python typing.Protocol 怎样描述可替换依赖
- 上一篇
- Python typing.Protocol 怎样描述可替换依赖
- 下一篇
- Python sqlite3 serialize 与 deserialize 的内存数据库迁移方法
-
- 文章 · python教程 | 40分钟前 |
- Python Fraction 近似浮点数时怎样控制最大分母
- 417浏览 收藏
-
- 文章 · python教程 | 56分钟前 |
- Python compression.zstd 怎样读取大压缩流
- 288浏览 收藏
-
- 文章 · python教程 | 57分钟前 |
- Python 3.15 t-string 怎样用于模板化文本
- 491浏览 收藏
-
- 文章 · python教程 | 59分钟前 | 连接池 · python ·
- Python httpx 怎样复用异步客户端连接
- 299浏览 收藏
-
- 文章 · python教程 | 1小时前 | SQLite · Python教程 · 内存数据库 serialize Python sqlite3 deserialize SQLite迁移
- Python sqlite3 serialize 与 deserialize 的内存数据库迁移方法
- 497浏览 收藏
-
- 文章 · python教程 | 1小时前 |
- Python typing.Protocol 怎样描述可替换依赖
- 236浏览 收藏
-
- 文章 · python教程 | 1小时前 |
- Python multiprocessing 怎样选择 spawn 和 fork
- 322浏览 收藏
-
- 文章 · python教程 | 1小时前 | JSON · Python教程 · Python JSON enum 反序列化 序列化 json.dumps json.loads StrEnum
- Python StrEnum 怎样与 JSON 字符串互转
- 393浏览 收藏
-
- 文章 · python教程 | 1小时前 |
- Python 3.15 延迟导入怎样评估启动收益
- 133浏览 收藏
-
- 文章 · python教程 | 1小时前 |
- Python 3.15 profiling.sampling 怎样选择 wall 和 cpu 模式
- 138浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 471次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 530次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 549次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 499次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 317次使用
-
- Python sqlite3 Connection serialize 怎么导出数据库快照:备份窗口、内存占用与恢复校验
- 2026-08-26 501浏览
-
- Python监控网页状态:requests异常处理实战
- 2026-05-29 501浏览
-
- TensorFlow模型部署为API的TF Serving方法
- 2026-05-26 501浏览
-
- Python字符串编码转换:encode与decode详解
- 2026-05-16 501浏览
-
- TensorFlow裁剪无用算子方法详解
- 2026-05-15 501浏览
