如何使用Python的logging模块按日滚动记录Web日志并保存请求链路信息?

2026-05-07 01:551阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1189个文字,预计阅读时间需要5分钟。

如何使用Python的logging模块按日滚动记录Web日志并保存请求链路信息?

使用 `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 上直接 setLeveladdHandler 后再套 Adapter,Adapter 只负责 enrich 日志 record,不接管 handler。handler 还得自己配好再挂上去。

立即学习“Python免费学习笔记(深入)”;

  • 每次请求进 middleware 时生成 request_id = str(uuid.uuid4()),存进 flask.gcontextvars.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 属于谁。这些点没卡准,文件天天切,链路照样断。

标签:Python

本文共计1189个文字,预计阅读时间需要5分钟。

如何使用Python的logging模块按日滚动记录Web日志并保存请求链路信息?

使用 `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 上直接 setLeveladdHandler 后再套 Adapter,Adapter 只负责 enrich 日志 record,不接管 handler。handler 还得自己配好再挂上去。

立即学习“Python免费学习笔记(深入)”;

  • 每次请求进 middleware 时生成 request_id = str(uuid.uuid4()),存进 flask.gcontextvars.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 属于谁。这些点没卡准,文件天天切,链路照样断。

标签:Python