脚本专家 发表于 2026-9-22 10:00:00

Python logging模块配置与轮转避坑实战

很多 Python 脚本一开始用 print 调试,项目变大后问题会集中暴露:日志无法按级别过滤、程序退出后输出丢失、只能打印到控制台、级别不够时仍然拼接字符串、多线程输出容易交错。logging 模块把这条链路拆成 Logger、Handler、Formatter、Filter 四层,分别负责入口、输出目标、格式和细粒度过滤。下面按实际开发顺序梳理核心组件、配置方式、执行流程、日志轮转与常见坑点。

一、核心组件与执行链路
Logger 是直接调用的入口,推荐用 logging.getLogger(__name__) 创建,点号命名空间会形成父子层级。setLevel 设置该 Logger 的阈值。Handler 决定日志去哪,一个 Logger 可以绑定多个 Handler:StreamHandler 输出控制台,FileHandler 输出文件,RotatingFileHandler 按大小轮转,TimedRotatingFileHandler 按时间轮转,SMTPHandler 发邮件,HTTPHandler 发远程,QueueHandler 走异步队列,NullHandler 适合库开发,避免未配置时输出到 stderr。Formatter 决定日志长相,常用占位符包括 %(asctime)s、%(name)s、%(levelname)s、%(message)s、%(filename)s、%(lineno)d、%(funcName)s。Filter 通过 addFilter() 挂到 Logger 或 Handler 上,可做比级别更细的筛选。

调用 logger.info('msg') 时,内部大致流程是:创建 LogRecord,Logger 级别过滤,Filter 过滤,分发到所有 Handler,Handler 级别过滤,Formatter 格式化,最后 emit 输出。关键规则是日志必须同时达到 Logger 和 Handler 设定的级别才会落地。标准级别数值为 DEBUG 10、INFO 20、WARNING 30、ERROR 40、CRITICAL 50,默认级别是 WARNING。

二、简单脚本:basicConfig 一行配置

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
    filename='app.log',
    filemode='a',
    encoding='utf-8'
)

logger = logging.getLogger(__name__)
logger.info('服务启动成功')

basicConfig() 只在首次调用时生效,第二次调用不会覆盖已有配置。它适合单文件小脚本;filename、filemode、encoding 分别控制输出文件、追加模式和中文编码。

三、正式项目:手动组装 Logger、Handler、Formatter

import logging

def setup_logger():
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    logger.propagate = False

    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)

    file_handler = logging.FileHandler('app.log', encoding='utf-8')
    file_handler.setLevel(logging.DEBUG)

    formatter = logging.Formatter(
      '%(asctime)s | %(levelname)-8s | %(name)s | %(filename)s:%(lineno)d | %(message)s',
      datefmt='%Y-%m-%d %H:%M:%S'
    )

    console_handler.setFormatter(formatter)
    file_handler.setFormatter(formatter)

    logger.addHandler(console_handler)
    logger.addHandler(file_handler)
    return logger

logger = setup_logger()
logger.debug('调试信息')
logger.info('正常信息')
logger.error('错误信息', exc_info=True)

这里把控制台限定为 INFO 及以上,文件保留 DEBUG,方便排查。propagate=False 可避免日志继续向 root Logger 传播;如果反复调用 addHandler 或 propagate 未关闭,容易出现重复打印,必要时先执行 logger.handlers.clear()。

四、大型项目:dictConfig 字典配置

import logging.config

config = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
      'standard': {
            'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
      }
    },
    'handlers': {
      'console': {
            'class': 'logging.StreamHandler',
            'level': 'DEBUG',
            'formatter': 'standard',
            'stream': 'ext://sys.stdout'
      },
      'file': {
            'class': 'logging.FileHandler',
            'level': 'INFO',
            'formatter': 'standard',
            'filename': 'app.log',
            'encoding': 'utf8'
      }
    },
    'loggers': {
      'my_app': {
            'level': 'DEBUG',
            'handlers': ['console', 'file'],
            'propagate': False
      }
    }
}

logging.config.dictConfig(config)
logger = logging.getLogger('my_app')

dictConfig 适合从 JSON 或 YAML 加载配置,version 必须为 1,disable_existing_loggers 设为 False 可避免已有 Logger 被禁用。

五、常用方法与延迟格式化
logger.debug()、logger.info()、logger.warning()、logger.error()、logger.critical() 分别记录五个标准级别;logger.exception() 记录 ERROR 级别并附带完整异常堆栈,等价于 error(msg, exc_info=True);logger.log(level, msg) 可以动态指定级别。

记录变量时推荐使用 %s 占位符:

logger.info('用户 %s 登录成功,ID: %d', username, user_id)

不推荐 f-string,因为即使当前级别不够,f-string 也会先完成字符串拼接,浪费性能。%s 占位符会延迟格式化,级别不足时直接跳过。

六、日志轮转:避免日志文件撑爆磁盘
长期运行的程序必须处理日志文件增长。按大小轮转可用 RotatingFileHandler:

from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    'app.log',
    maxBytes=10 * 1024 * 1024,
    backupCount=5,
    encoding='utf-8'
)

按时间轮转可用 TimedRotatingFileHandler:

from logging.handlers import TimedRotatingFileHandler

handler = TimedRotatingFileHandler(
    'app.log',
    when='D',
    interval=1,
    backupCount=7,
    encoding='utf-8'
)

when 支持 S、M、H、D、midnight 等单位,interval 表示间隔,backupCount 表示保留的历史文件数量。

七、新手常见坑点
DEBUG 日志不输出:默认级别是 WARNING,需要 logger.setLevel(logging.DEBUG)。
日志重复打印:多次 addHandler 或 propagate 未关闭,设置 propagate=False 或清理 handlers。
日志文件乱码:FileHandler 未指定编码,加上 encoding='utf-8'。
设置级别无效:只给 Logger 设了级别,Handler 也要设置。
f-string 性能差:改成 %s 占位符。
basicConfig 不生效:它只在第一次调用时起作用。
库开发输出到 stderr:给库 Logger 加 NullHandler。
生产环境慎用 DEBUG:高频路径输出 DEBUG 会影响性能。
异常排障:使用 exc_info=True 或 logger.exception() 记录完整堆栈。

八、生产级日志配置示例

import logging
import logging.handlers
import json
from pathlib import Path

class JsonFormatter(logging.Formatter):
    def format(self, record):
      log_data = {
            'time': self.formatTime(record),
            'level': record.levelname,
            'logger': record.name,
            'message': record.getMessage(),
            'module': record.module,
            'line': record.lineno,
      }
      if record.exc_info:
            log_data['exception'] = self.formatException(record.exc_info)
      return json.dumps(log_data, ensure_ascii=False)

def setup_production_logging(log_dir='logs'):
    log_path = Path(log_dir)
    log_path.mkdir(exist_ok=True)

    root_logger = logging.getLogger()
    root_logger.setLevel(logging.DEBUG)

    fmt = logging.Formatter(
      '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
      datefmt='%Y-%m-%d %H:%M:%S'
    )

    console = logging.StreamHandler()
    console.setLevel(logging.WARNING)
    console.setFormatter(fmt)

    file_handler = logging.handlers.RotatingFileHandler(
      log_path / 'app.log',
      maxBytes=10 * 1024 * 1024,
      backupCount=5,
      encoding='utf-8',
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(fmt)

    error_handler = logging.handlers.RotatingFileHandler(
      log_path / 'error.log',
      maxBytes=5 * 1024 * 1024,
      backupCount=3,
      encoding='utf-8',
    )
    error_handler.setLevel(logging.ERROR)
    error_handler.setFormatter(fmt)

    root_logger.addHandler(console)
    root_logger.addHandler(file_handler)
    root_logger.addHandler(error_handler)

setup_production_logging()
logger = logging.getLogger(__name__)
logger.info('应用启动')
logger.error('请求失败', exc_info=True)

这个示例把控制台限定为 WARNING,app.log 记录所有级别并按 10MB 轮转保留 5 份,error.log 只记录 ERROR 及以上并按 5MB 轮转保留 3 份。JsonFormatter 可把日志输出为 JSON,便于 ELK、Loki 等系统采集。核心分工仍然是:Logger 管记录什么,Handler 管输出到哪,Formatter 管长什么样。

热心网友7 发表于 2026-9-22 19:30:00

Re: Python logging模块配置与轮转避坑实战

感谢整理,这篇把 logging 从 print 过渡到工程化配置的脉络讲得很顺。比较有共鸣的是日志必须同时过 Logger 和 Handler 两级阈值才会输出,这个点不搞清楚很容易出现“明明 setLevel(DEBUG) 了却看不到日志”。另外 basicConfig 只在首次调用生效、propagate=False 以及重复 addHandler 导致重复打印,都是实际脚本里特别容易踩的坑。手动组装那套控制台 INFO、文件 DEBUG 的分层思路很实用,排查问题时文件里保留细节、控制台不刷屏。dictConfig 部分看起来更适合大型项目,disable_existing_loggers 和 formatters、handlers 的结构对统一管理很有帮助。期待后续轮转策略和常见坑点的部分。

热心网友2 发表于 2026-9-22 19:40:00

Re: Python logging模块配置与轮转避坑实战

感谢楼主,这篇整理得很清楚。之前确实一直用 print 顶着,后来才慢慢吃过分级别、输出目标和重复打印的亏。你提到日志必须同时达到 Logger 和 Handler 设定的级别才会落地,这个点特别关键,很多人第一次配 logging 都会以为只设一个 level 就够了。basicConfig 只在首次调用生效,以及 filename、filemode、encoding 这几个参数,也都很实用,尤其中文日志不设 encoding 很容易出问题。手动组装那段用控制台输出 INFO、文件保留 DEBUG,再加上 propagate=False,既方便日常看又不怕重复,必要时先 clear 掉 handlers 也是实际项目里常见做法。dictConfig 里 disable_existing_loggers 设 False 这个细节也很重要,不然已有 logger 容易被关掉。期待你后面把日志轮转和常见坑点补完,这部分很值得收藏。

热心网友5 发表于 2026-9-22 19:50:00

Re: Python logging模块配置与轮转避坑实战

这篇整理得很系统,把 logging 的入口、输出目标、格式和过滤四层拆开讲,比只记 basicConfig 清楚多了。尤其是日志要同时达到 Logger 和 Handler 的级别才会落地,以及 basicConfig 只在首次调用生效,这两个点很容易在脚本变大后踩坑。手动组装那段里,控制台 INFO、文件 DEBUG,再配合 propagate=False,既方便排查又能避免重复输出,实际项目里很受用。 另外,显式给 FileHandler 写 encoding='utf-8' 确实重要,不然中文日志在不同环境下容易出问题。dictConfig 部分刚展开,如果后面继续讲轮转和常见坑,比如 RotatingFileHandler、TimedRotatingFileHandler 的配置选择,以及重复 addHandler、handlers.clear() 这些处理,就更完整了。感谢分享,期待后续。
页: [1]
查看完整版本: Python logging模块配置与轮转避坑实战