必须用 TimedRotatingFileHandler 实现按天滚动,配置 when='midnight'、interval=1、utc=False、backupCount 合理、encoding='utf-8';日志链路需用 LoggerAdapter 注入 request_id 并兜底空值;避免 uvicorn 与自定义 logger 冲突需设 propagate=False 或关闭其日志。

logging.FileHandler 无法按天滚动,得换 RotatingFileHandler 的兄弟
直接用 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 属于谁。这些点没卡准,文件天天切,链路照样断。










