先给结论 logging 出问题的九成原因是搞错了这三件事:① logger 和 handler 是两级过滤,都要过;② basicConfig() 只在 root 没配置过时生效;③ handler 会重复叠加,而 logger 默认还会向上传到 root。 本文把最常见的 9 个坑做成症状 → 原因 → 修法对照表,每一条都配可复现的最小代码。 --- 一、先记住这张对照表(排错时按症状查) 症状 / 真正的原因 / 修法 info() 一点都不输出:真正的原因=root logger 默认级别是 WARNING;修法=logging.basicConfig(level=logging.INFO) basicConfig 设了还是不输出:真正的原因=之前已经有人配置过 root(导库时触发);修法=显式给自己的 logger 加 handler 一条日志打印 2 次**:真正的原因=既给 logger 加了 handler,又传播到 root**;修法=logger.propagate = False 一条日志打印 3 次以上**:真正的原因=handler 被重复添加**(模块级/函数级各加一次);修法=加之前先判断 if not logger.handlers 文件里中文是 乱码****:真正的原因=FileHandler 默认用系统编码(Windows 是 GBK);修法=显式 encoding='utf-8' 文件里啥都没有:真正的原因=没 flush,进程被强杀;修法=handler.flush() 或用 RotatingFileHandler 时间字段是 UTC****:真正的原因=asctime 用 time.localtime 的时区;修法=自定义 formatter 的 converter 多进程写日志 丢行/串行错乱****:真正的原因=多个进程写同一个文件句柄;修法=用 ConcurrentRotatingFileHandler 或队列 在 uvicorn/gunicorn 里失效****:真正的原因=框架接管了 root logger;修法=用 dictConfig 或框架提供的配置 --- 二、坑 1:info() 不输出(级别问题) import logging logging.info("看不到我") # 什么都不打印 logging.warning("看得到我") # 只有这个会打印 原因:root logger 的默认级别是 WARNING,INFO/DEBUG 直接被拦掉。 import logging logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") logging.info("这次能看到了") 关键点:级别判断发生在产生日志的第一关(logger 自身级别),随后还有 handler 的级别再过滤一次。 --- 三、坑 2:basicConfig 设了却没生效 import logging import requests # 很多库在 import 时就碰过 logging logging.basicConfig(level=logging.INFO) # 无效,root 已有 handler 原因:basicConfig() 内部有一句 "如果 root 已经有 handler,就直接 return"。第三方库只要 import 时调过一次,你的 basicConfig 就成了空操作。 正确做法:别依赖 basicConfig,显式建自己的 logger。 import logging logger = logging.getLogger("myapp") # 用模块名/应用名,不要用 root logger.setLevel(logging.INFO) if not logger.handlers: # 关键:防止重复添加 h = logging.StreamHandler() h.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) logger.addHandler(h) --- 四、坑 3:一条日志打印两次(propagate) logger = logging.getLogger("myapp") logger.addHandler(logging.StreamHandler()) logging.basicConfig(level=logging.INFO) # root 也有 handler logger.info("hello") # hello # hello ← 打两遍 原因:logger 的 propagate 默认为 True,日志传给自己的 handler 后,还会向上传给 root,root 的 handler 再打一遍。 logger.propagate = False # 一刀切:不再往上传 判断标准:如果你的 logger 自己配了 handler,就把 propagate 关掉。 --- 五、坑 4:打印 N 次(handler 重复叠加) def get_logger(): lg = logging.getLogger("myapp") lg.addHandler(logging.StreamHandler()) # 每次调用都加一个 return lg get_logger(); get_logger(); get_logger().info("hi") # 打印 3 次 原因:getLogger 返回的是同一个单例,但 addHandler 会无脑追加。 修法:加之前先判空,或用 dictConfig 一次性配置。 if not logger.handlers: logger.addHandler(h) --- 六、坑 5:中文乱码(编码问题,Windows 高发) h = logging.FileHandler("app.log") # ❌ Windows 下默认 GBK h = logging.FileHandler("app.log", encoding="utf-8") # ✅ 原因:FileHandler 不指定 encoding 时,用 locale.getpreferredencoding(),中文 Windows 是 GBK/CP936。写 UTF-8 文本进去就乱码或抛 UnicodeEncodeError。 顺带:读取时用 open("app.log", encoding="utf-8"),两边编码一致才不会看花。 --- 七、坑 6:日志时间不对(UTC / 时区) import time formatter = logging.Formatter("%(asctime)s %(message)s") formatter.converter = time.localtime # 改成本地时区 asctime 默认走 time.localtime(其实是本地时间),但容器里 TZ 没设置时容器默认是 UTC,看起来就"差 8 小时"。 修法:容器启动时带环境变量 TZ=Asia/Shanghai,或者在代码里指定 converter。 --- 八、坑 7:多进程写同一个文件(丢日志) # ❌ 多进程下 RotatingFileHandler 会互相覆盖、切割混乱 h = logging.handlers.RotatingFileHandler("app.log", maxBytes=10_000_000, backupCount=5) 原因:多个进程各自持有文件句柄和偏移量,追加写会互相踩。 方案 A(推荐):用 concurrent-log-handler 的 ConcurrentRotatingFileHandler(文件锁)。 from concurrent_log_handler import ConcurrentRotatingFileHandler h = ConcurrentRotatingFileHandler("app.log", maxBytes=10_000_000, backupCount=5) 方案 B:每个进程写自己的文件(app.{pid}.log),采集时再合并。 --- 九、坑 8:f-string 的性能与聚合问题 logger.debug(f"user={user} cost={cost}") # ❌ 即使不输出也会先格式化 logger.debug("user=%s cost=%s", user, cost) # ✅ 惰性格式化 两个理由:① 惰性写法在日志被过滤掉时不做字符串拼接,省 CPU;② 接入 Sentry / ELK 这类系统时,模板相同才能聚合成一条,f-string 会让每个用户都变成独立事件。 --- 十、坑 9:logger.exception 只能在 except 里用 try: risky() except ValueError: logger.exception("处理失败") # ✅ 自动带 traceback logger.exception() 等价于 logger.error(..., exc_info=True)。在 except 块外调用,Python 3 会提示没有活动异常,打印 NoneType: None。 另外:捕获后要重新抛出时,用 logger.error("...", exc_info=True) + raise,别重复打两遍。 --- 十一、可直接抄的配置(dictConfig 版) import logging.config LOGGING = { "version": 1, "disable_existing_loggers": False, # 别让第三方库的 logger 失效 "formatters": { "std": {"format": "%(asctime)s %(levelname)s [%(name)s] %(message)s"}, }, "handlers": { "console": {"class": "logging.StreamHandler", "level": "INFO", "formatter": "std"}, "file": {"class": "logging.FileHandler", "level": "DEBUG", "formatter": "std", "filename": "app.log", "encoding": "utf-8"}, # ← 编码一定要写 }, "loggers": { "myapp": {"level": "DEBUG", "handlers": ["console", "file"], "propagate": False}, # ← 防止打两遍 }, } logging.config.dictConfig(LOGGING) logger = logging.getLogger("myapp") 三个必须注意的参数:disable_existing_loggers: False、encoding: "utf-8"、propagate: False。 --- 十二、5 分钟排查清单 1. logger.level 是多少(0 表示继承父级)? 2. logger.handlers 里有几个(>1 就是重复打印的元凶)? 3. logger.propagate 是 True 吗(自己配了 handler 就该关掉)? 4. 写文件时传了 encoding="utf-8" 吗? 5. 多进程场景用了带锁的 handler 吗? --- 小结 logging 的坑不在 API 难,而在于它是"logger + handler + formatter + propagate" 四层结构,任何一层配错都会表现成"日志不见了"或者"日志打两遍"。 三条硬记住:① 级别要过两道(logger 和 handler);② basicConfig 只对"干净的 root"生效;③ 自己加了 handler 就把 propagate 关掉、且加之前判空。 --- 你最常被哪种症状折磨? 评论区留个"重复打印/不打印/乱码"三选一,我把出现频率最高的坑再单独展开一篇。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。