Python logging 正确配置:告别 print 调试与日志重复打印线上服务出问题,你翻日志却发现:要么啥都没有(全靠print早被丢进 stdout 黑洞),要么同一条日志打了三遍,要么日志文件涨到几个 G 把磁盘撑爆。Python 的logging模块功能齐全,但默认行为反直觉,踩坑的人比用对的人多。这篇把生产级 logging 配置一次讲清:为什么别用 root logger、怎么避免重复打印、怎么按大小/时间轮转文件。别再用 print,也别只会 logging.info很多人对 logging 的认知停留在import logging; logging.info(xxx)。这行代码用的是root logger,问题在于:importlogging logging.info(这条你根本看不到)# 默认级别是 WARNING,INFO 被丢弃logging.warning(这条才会打印)默认级别是WARNING,所以info/debug直接被吞。更重要的是,在 root logger 上乱配置会污染所有第三方库的日志。正确姿势是每个模块拿自己的 logger:importlogging# 惯例:用模块名做 logger 名字,自动形成层级loggerlogging.getLogger(__name__)defdo_work():logger.info(开始处理)# 名字会是 myapp.service 这种logging.getLogger(__name__)让日志自带来源(哪个模块打的),而且 logger 名字用.分隔会形成父子层级,后面配置时能按模块精细控制。一个能用的基础配置在程序入口(如main.py)集中配一次,其他模块只管getLogger(__name__)拿来用:importloggingimportsysdefsetup_logging(levellogging.INFO):loggerlogging.getLogger()# 拿 root logger 做统一配置logger.setLevel(level)# Handler 决定日志去哪:这里输出到 stderrhandlerlogging.StreamHandler(sys.stderr)# Formatter 决定长什么样:时间 级别 模块 消息fmtlogging.Formatter(%(asctime)s [%(levelname)s] %(name)s: %(message)s,datefmt%Y-%m-%d %H:%M:%S,)handler.setFormatter(fmt)logger.addHandler(handler)if__name____main__:setup_logging()logging.getLogger(__name__).info(服务启动)# 输出: 2026-07-20 09:00:00 [INFO] __main__: 服务启动三个核心概念理清:Logger是你调用的入口、Handler决定日志输出到哪(控制台/文件/网络)、Formatter决定每条长什么样。一个 Logger 可以挂多个 Handler,同时输出到控制台和文件。头号大坑:日志重复打印配置完跑起来,发现每条日志打了两遍甚至三遍——这是 logging 最经典的坑。根因通常是handler 被重复添加,或者子 logger 的消息向上传播到 root 又被打了一遍。第一种,setup_logging被调了多次(比如在循环或被重复 import 的模块里):defsetup_logging():loggerlogging.getLogger()# 危险:每调一次就多挂一个 handler,日志翻倍logger.addHandler(logging.StreamHandler())修复:加之前先清空,或判断是否已有 handler:defsetup_logging():loggerlogging.getLogger()iflogger.handlers:# 已经配过就别再挂了returnlogger.addHandler(logging.StreamHandler())第二种,你给某个模块 logger 单独挂了 handler,但它的消息默认还会propagate 到 root,root 上也有 handler,于是打两遍:mod_loggerlogging.getLogger(myapp.worker)mod_logger.addHandler(logging.StreamHandler())# 消息会:先被自己的 handler 打一遍,再冒泡到 root 又打一遍mod_logger.propagateFalse# 关掉冒泡,只打一遍记住:要么只在 root 上配 handler、子 logger 靠冒泡输出;要么给子 logger 单独配 handler 并关掉 propagate。别两头都挂。文件轮转:别让日志把磁盘撑爆生产日志写文件,必须轮转,否则单文件无限增长。标准库自带两种 handler,不用装任何东西。按文件大小轮转,单文件 10MB、保留 5 个历史:fromlogging.handlersimportRotatingFileHandler handlerRotatingFileHandler(app.log,maxBytes10*1024*1024,# 单文件 10MBbackupCount5,# 超了就滚动,最多留 app.log.1 ~ .5encodingutf-8,)按时间轮转,每天午夜切一个新文件、保留 7 天:fromlogging.handlersimportTimedRotatingFileHandler handlerTimedRotatingFileHandler(app.log,whenmidnight,# 每天午夜滚动;也可 H(每小时)、W0(每周一)backupCount7,# 保留最近 7 天encodingutf-8,)按天轮转更适合排查(一天一个文件好定位),按大小轮转更适合控制磁盘占用。两者选一即可,别叠加。一个实用技巧:异常带堆栈记录异常时,别只logger.error(str(e))——那样丢了堆栈,等于没记。用exc_infoTrue或直接logger.exception:try:risky()exceptException:# exception() 等价于 error(..., exc_infoTrue),自动带完整 tracebacklogger.exception(处理失败)# 只能在 except 块里用logger.exception会把完整 traceback 一起写进日志,排查线上问题时这几行堆栈往往就是关键。注意它只在except块里用才有意义。小结每个模块用logging.getLogger(__name__),别直接用 root logger 或logging.info。理清Logger / Handler / Formatter三者职责:入口、去哪、长啥样。日志重复打印几乎都是 handler 重复添加或 propagate 冒泡导致,给子 logger 配了 handler 就设propagate False。写文件必用RotatingFileHandler(按大小)或TimedRotatingFileHandler(按时间)轮转,别让单文件无限涨。记异常用logger.exception,带上堆栈才有排查价值。一句话记忆点:日志打两遍先查 handler 挂了几个、propagate 关没关;这是 logging 最省时间的一条排查经验。