如何使用Python的logging模块按日滚动记录Web日志并保存请求链路信息?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1189个文字,预计阅读时间需要5分钟。
使用 `FileHandler` 仅能写入一个文件,基本不会进行日期滚动。真正按天生成类似 `app.log.2024-05-20` 这样的文件,必须使用 `TimedRotatingFileHandler —— 它是专门为时间维度滚动设计,不是依赖文件大小,而是基于 `when='midnight'` 触发每天午夜滚动分割。
常见错误是把 backupCount 设太小(比如 3),结果日志只留最近三天,审计时发现上周五的请求链路没了;或者漏设 encoding='utf-8',中文路径或日志内容一写就报 UnicodeEncodeError。
-
when='midnight'是最稳的选择,避免用'D'(已弃用)或'MIDNIGHT'(大小写敏感,错写就静默失效) -
interval=1必须配when='midnight',单独设 interval 没用 -
utc=False(默认),别开 UTC,否则凌晨 0 点可能在你本地时间 7 点才切 - 文件名里别硬编码日期,
TimedRotatingFileHandler自动追加后缀,起始文件就叫app.log
Web 请求链路怎么塞进每条日志里?靠 LoggerAdapter + request_id
Flask 或 FastAPI 里,每个请求进来时生成唯一 request_id,但 logging 模块本身不感知上下文。硬在每个 logger.info() 里手动传 extra={'request_id': xxx} 既啰嗦又容易漏。正确姿势是用 LoggerAdapter 封一层,让它自动注入。
注意:不能在全局 logger 上直接 setLevel 或 addHandler 后再套 Adapter,Adapter 只负责 enrich 日志 record,不接管 handler。handler 还得自己配好再挂上去。
立即学习“Python免费学习笔记(深入)”;
- 每次请求进 middleware 时生成
request_id = str(uuid.uuid4()),存进flask.g或contextvars.ContextVar - Adapter 的
process()方法里从上下文取request_id,拼进extra返回 - Formatter 的
format字符串里必须包含%(request_id)s,否则 Adapter 塞了也白塞 - 别用线程局部变量(
threading.local)存 request_id —— 异步框架(如 FastAPI + uvicorn)下会串数据
Formatter 里加 trace_id 要防 KeyError,空值得兜底
如果某次请求没走完整中间件(比如 400 错误提前返回),request_id 可能压根没设,Formatter 一读 %(request_id)s 就崩,整条日志变空行甚至中断输出。这不是配置错,是日志系统对缺失字段零容忍。
解决方法不是改 Formatter 模板语法,而是让 Adapter 的 process() 总返回 dict,且 key 存在、value 可为空字符串。
- Adapter 的
process()返回(msg, kwargs)时,确保kwargs['extra']里request_id键一定存在 - 值可以是
''或'N/A',但不能是None(Formatter 会把它转成字符串'None',看着像真值) - Formatter 模板建议写成:
%(asctime)s %(name)s %(levelname)s [%(request_id)-12s] %(message)s,-12s对齐,空值也占位 - 别在 Formatter 里写 Python 表达式(比如
%(request_id or 'MISSING')s)—— logging 不支持这种语法
FastAPI + uvicorn 下日志重复打印的根源和解法
开 uvicorn 时加了 --log-level info,又在代码里配了 TimedRotatingFileHandler,结果同一条请求日志在终端打两遍、文件里再写一遍。这不是 bug,是 uvicorn 自带 logger 和你手配的 logger 同时生效了。
uvicorn 默认把日志发给 root logger,而你的 getLogger('myapp') 如果没设 propagate=False,消息会一路冒泡到 root,被 uvicorn 的 handler 再捕一次。
- 所有自定义 logger 创建后立刻设
logger.propagate = False - uvicorn 启动时加
--access-log=False --error-log=False关掉它的输出,只留你配的 handler - 别信文档里“uvicorn 用 structlog 就不重”——你没换日志库,只是关掉它的 handler 更直接
- 检查 handler 是否被重复
addHandler:每次 reload(比如 debug 模式)都可能新挂一个,用if not logger.handlers:包一层
按天滚动本身不难,难的是 request_id 在异步/多进程/热重载场景下不丢、不串、不空;而日志不重复的前提,是你得先分清哪条 handler 属于谁。这些点没卡准,文件天天切,链路照样断。
本文共计1189个文字,预计阅读时间需要5分钟。
使用 `FileHandler` 仅能写入一个文件,基本不会进行日期滚动。真正按天生成类似 `app.log.2024-05-20` 这样的文件,必须使用 `TimedRotatingFileHandler —— 它是专门为时间维度滚动设计,不是依赖文件大小,而是基于 `when='midnight'` 触发每天午夜滚动分割。
常见错误是把 backupCount 设太小(比如 3),结果日志只留最近三天,审计时发现上周五的请求链路没了;或者漏设 encoding='utf-8',中文路径或日志内容一写就报 UnicodeEncodeError。
-
when='midnight'是最稳的选择,避免用'D'(已弃用)或'MIDNIGHT'(大小写敏感,错写就静默失效) -
interval=1必须配when='midnight',单独设 interval 没用 -
utc=False(默认),别开 UTC,否则凌晨 0 点可能在你本地时间 7 点才切 - 文件名里别硬编码日期,
TimedRotatingFileHandler自动追加后缀,起始文件就叫app.log
Web 请求链路怎么塞进每条日志里?靠 LoggerAdapter + request_id
Flask 或 FastAPI 里,每个请求进来时生成唯一 request_id,但 logging 模块本身不感知上下文。硬在每个 logger.info() 里手动传 extra={'request_id': xxx} 既啰嗦又容易漏。正确姿势是用 LoggerAdapter 封一层,让它自动注入。
注意:不能在全局 logger 上直接 setLevel 或 addHandler 后再套 Adapter,Adapter 只负责 enrich 日志 record,不接管 handler。handler 还得自己配好再挂上去。
立即学习“Python免费学习笔记(深入)”;
- 每次请求进 middleware 时生成
request_id = str(uuid.uuid4()),存进flask.g或contextvars.ContextVar - Adapter 的
process()方法里从上下文取request_id,拼进extra返回 - Formatter 的
format字符串里必须包含%(request_id)s,否则 Adapter 塞了也白塞 - 别用线程局部变量(
threading.local)存 request_id —— 异步框架(如 FastAPI + uvicorn)下会串数据
Formatter 里加 trace_id 要防 KeyError,空值得兜底
如果某次请求没走完整中间件(比如 400 错误提前返回),request_id 可能压根没设,Formatter 一读 %(request_id)s 就崩,整条日志变空行甚至中断输出。这不是配置错,是日志系统对缺失字段零容忍。
解决方法不是改 Formatter 模板语法,而是让 Adapter 的 process() 总返回 dict,且 key 存在、value 可为空字符串。
- Adapter 的
process()返回(msg, kwargs)时,确保kwargs['extra']里request_id键一定存在 - 值可以是
''或'N/A',但不能是None(Formatter 会把它转成字符串'None',看着像真值) - Formatter 模板建议写成:
%(asctime)s %(name)s %(levelname)s [%(request_id)-12s] %(message)s,-12s对齐,空值也占位 - 别在 Formatter 里写 Python 表达式(比如
%(request_id or 'MISSING')s)—— logging 不支持这种语法
FastAPI + uvicorn 下日志重复打印的根源和解法
开 uvicorn 时加了 --log-level info,又在代码里配了 TimedRotatingFileHandler,结果同一条请求日志在终端打两遍、文件里再写一遍。这不是 bug,是 uvicorn 自带 logger 和你手配的 logger 同时生效了。
uvicorn 默认把日志发给 root logger,而你的 getLogger('myapp') 如果没设 propagate=False,消息会一路冒泡到 root,被 uvicorn 的 handler 再捕一次。
- 所有自定义 logger 创建后立刻设
logger.propagate = False - uvicorn 启动时加
--access-log=False --error-log=False关掉它的输出,只留你配的 handler - 别信文档里“uvicorn 用 structlog 就不重”——你没换日志库,只是关掉它的 handler 更直接
- 检查 handler 是否被重复
addHandler:每次 reload(比如 debug 模式)都可能新挂一个,用if not logger.handlers:包一层
按天滚动本身不难,难的是 request_id 在异步/多进程/热重载场景下不丢、不串、不空;而日志不重复的前提,是你得先分清哪条 handler 属于谁。这些点没卡准,文件天天切,链路照样断。

