日志必须统一用 logging.Formatter 控制格式,禁用字符串拼接;使用标准模板和 python-json-logger;避免提前格式化、硬编码 debug 判断、extra 字段滥用、多进程下非线程安全 handler。

日志格式必须统一用 logging.Formatter 控制,别拼字符串
直接用 "%s - %s" % (level, msg) 或 f"{time} {msg}" 会导致字段缺失、时区混乱、无法被 ELK 解析。统一格式的唯一可控入口是 Formatter 实例。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 固定使用
%(asctime)s %(name)s %(levelname)-8s %(funcName)s:%(lineno)d %(message)s基础模板,asctime自动带时区(需配logging.basicConfig(..., datefmt=...)) - 若需 JSON 输出(如对接 Loki),用第三方库
python-json-logger,而非手写json.dumps()—— 它能正确处理异常对象、嵌套字典、非字符串extra字段 - 禁止在
logger.info("user_id=%s, action=%s" % (uid, act))中提前格式化;应写成logger.info("user_id=%s, action=%s", uid, act),否则占位符失效且影响性能
不同环境要分开关掉 DEBUG 日志,但不能靠 if DEBUG: 判断
硬编码 if DEBUG: logger.debug(...) 会绕过日志级别控制,导致生产环境仍执行字符串拼接或函数调用(比如 logger.debug("data=%s", expensive_func())),浪费 CPU 且不可控。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 全局只设一次
logger.setLevel(),开发用DEBUG,预发/生产强制设为WARNING或更高 - 所有日志调用保持原样,依赖
Logger自身的级别过滤机制——它会在格式化前就丢弃低优先级消息 - 检查
root logger和各模块getLogger(__name__)的 handler 是否都继承了同一 level 设置;常见坑是某个 handler 单独setLevel(DEBUG),导致日志重复或越权输出
extra 字典传参必须预定义字段,禁止运行时自由塞 key
随意用 logger.info("msg", extra={"user_id": 123, "trace_id": "abc"}) 会导致日志结构不一致,下游解析失败(比如 Grafana 查询 trace_id 字段时部分日志缺失该 key)。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 定义一个封装函数,如
log_with_context(logger, level, msg, **kwargs),内部只允许接收白名单字段:user_id、trace_id、req_id、ip - 在
Formatter中用%(user_id)s %(trace_id)s显式声明字段,缺失时自动填-(通过自定义Formatter.format()覆盖实现) - 避免在
extra里传可变对象(如 dict/list),序列化可能出错;一律转成字符串或用repr()包裹
多进程场景下,RotatingFileHandler 不安全,改用 ConcurrentRotatingFileHandler
标准库的 RotatingFileHandler 在多进程写同一个日志文件时会损坏内容(截断、覆盖、乱序),尤其在 logrotate 配合下更明显。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 安装
concurrent-log-handler库,替换 handler:from concurrent_log_handler import ConcurrentRotatingFileHandler - 注意它的
maxBytes和backupCount行为与原生一致,但底层用文件锁保证原子写入 - 若用 systemd 管理多实例,也可考虑按进程名/ID 分日志文件(如
app-worker-001.log),避免共享文件,此时可用原生 handler
统一日志不是加个配置就完事,真正难的是让所有人遵守字段约束、不绕过 level 控制、不在 extra 里自由发挥——这些地方一松动,日志就退化成不可检索的文本堆。










