查看: 318|回复: 0

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

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
很多 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 一行配置
  1. import logging
  2. logging.basicConfig(
  3.     level=logging.DEBUG,
  4.     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
  5.     datefmt='%Y-%m-%d %H:%M:%S',
  6.     filename='app.log',
  7.     filemode='a',
  8.     encoding='utf-8'
  9. )
  10. logger = logging.getLogger(__name__)
  11. logger.info('服务启动成功')
复制代码
basicConfig() 只在首次调用时生效,第二次调用不会覆盖已有配置。它适合单文件小脚本;filename、filemode、encoding 分别控制输出文件、追加模式和中文编码。

三、正式项目:手动组装 Logger、Handler、Formatter
  1. import logging
  2. def setup_logger():
  3.     logger = logging.getLogger(__name__)
  4.     logger.setLevel(logging.DEBUG)
  5.     logger.propagate = False
  6.     console_handler = logging.StreamHandler()
  7.     console_handler.setLevel(logging.INFO)
  8.     file_handler = logging.FileHandler('app.log', encoding='utf-8')
  9.     file_handler.setLevel(logging.DEBUG)
  10.     formatter = logging.Formatter(
  11.         '%(asctime)s | %(levelname)-8s | %(name)s | %(filename)s:%(lineno)d | %(message)s',
  12.         datefmt='%Y-%m-%d %H:%M:%S'
  13.     )
  14.     console_handler.setFormatter(formatter)
  15.     file_handler.setFormatter(formatter)
  16.     logger.addHandler(console_handler)
  17.     logger.addHandler(file_handler)
  18.     return logger
  19. logger = setup_logger()
  20. logger.debug('调试信息')
  21. logger.info('正常信息')
  22. logger.error('错误信息', exc_info=True)
复制代码
这里把控制台限定为 INFO 及以上,文件保留 DEBUG,方便排查。propagate=False 可避免日志继续向 root Logger 传播;如果反复调用 addHandler 或 propagate 未关闭,容易出现重复打印,必要时先执行 logger.handlers.clear()。

四、大型项目:dictConfig 字典配置
  1. import logging.config
  2. config = {
  3.     'version': 1,
  4.     'disable_existing_loggers': False,
  5.     'formatters': {
  6.         'standard': {
  7.             'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  8.         }
  9.     },
  10.     'handlers': {
  11.         'console': {
  12.             'class': 'logging.StreamHandler',
  13.             'level': 'DEBUG',
  14.             'formatter': 'standard',
  15.             'stream': 'ext://sys.stdout'
  16.         },
  17.         'file': {
  18.             'class': 'logging.FileHandler',
  19.             'level': 'INFO',
  20.             'formatter': 'standard',
  21.             'filename': 'app.log',
  22.             'encoding': 'utf8'
  23.         }
  24.     },
  25.     'loggers': {
  26.         'my_app': {
  27.             'level': 'DEBUG',
  28.             'handlers': ['console', 'file'],
  29.             'propagate': False
  30.         }
  31.     }
  32. }
  33. logging.config.dictConfig(config)
  34. 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 占位符:
  1. logger.info('用户 %s 登录成功,ID: %d', username, user_id)
复制代码
不推荐 f-string,因为即使当前级别不够,f-string 也会先完成字符串拼接,浪费性能。%s 占位符会延迟格式化,级别不足时直接跳过。

六、日志轮转:避免日志文件撑爆磁盘
长期运行的程序必须处理日志文件增长。按大小轮转可用 RotatingFileHandler:
  1. from logging.handlers import RotatingFileHandler
  2. handler = RotatingFileHandler(
  3.     'app.log',
  4.     maxBytes=10 * 1024 * 1024,
  5.     backupCount=5,
  6.     encoding='utf-8'
  7. )
复制代码
按时间轮转可用 TimedRotatingFileHandler:
  1. from logging.handlers import TimedRotatingFileHandler
  2. handler = TimedRotatingFileHandler(
  3.     'app.log',
  4.     when='D',
  5.     interval=1,
  6.     backupCount=7,
  7.     encoding='utf-8'
  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() 记录完整堆栈。

八、生产级日志配置示例
  1. import logging
  2. import logging.handlers
  3. import json
  4. from pathlib import Path
  5. class JsonFormatter(logging.Formatter):
  6.     def format(self, record):
  7.         log_data = {
  8.             'time': self.formatTime(record),
  9.             'level': record.levelname,
  10.             'logger': record.name,
  11.             'message': record.getMessage(),
  12.             'module': record.module,
  13.             'line': record.lineno,
  14.         }
  15.         if record.exc_info:
  16.             log_data['exception'] = self.formatException(record.exc_info)
  17.         return json.dumps(log_data, ensure_ascii=False)
  18. def setup_production_logging(log_dir='logs'):
  19.     log_path = Path(log_dir)
  20.     log_path.mkdir(exist_ok=True)
  21.     root_logger = logging.getLogger()
  22.     root_logger.setLevel(logging.DEBUG)
  23.     fmt = logging.Formatter(
  24.         '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
  25.         datefmt='%Y-%m-%d %H:%M:%S'
  26.     )
  27.     console = logging.StreamHandler()
  28.     console.setLevel(logging.WARNING)
  29.     console.setFormatter(fmt)
  30.     file_handler = logging.handlers.RotatingFileHandler(
  31.         log_path / 'app.log',
  32.         maxBytes=10 * 1024 * 1024,
  33.         backupCount=5,
  34.         encoding='utf-8',
  35.     )
  36.     file_handler.setLevel(logging.DEBUG)
  37.     file_handler.setFormatter(fmt)
  38.     error_handler = logging.handlers.RotatingFileHandler(
  39.         log_path / 'error.log',
  40.         maxBytes=5 * 1024 * 1024,
  41.         backupCount=3,
  42.         encoding='utf-8',
  43.     )
  44.     error_handler.setLevel(logging.ERROR)
  45.     error_handler.setFormatter(fmt)
  46.     root_logger.addHandler(console)
  47.     root_logger.addHandler(file_handler)
  48.     root_logger.addHandler(error_handler)
  49. setup_production_logging()
  50. logger = logging.getLogger(__name__)
  51. logger.info('应用启动')
  52. logger.error('请求失败', exc_info=True)
复制代码
这个示例把控制台限定为 WARNING,app.log 记录所有级别并按 10MB 轮转保留 5 份,error.log 只记录 ERROR 及以上并按 5MB 轮转保留 3 份。JsonFormatter 可把日志输出为 JSON,便于 ELK、Loki 等系统采集。核心分工仍然是:Logger 管记录什么,Handler 管输出到哪,Formatter 管长什么样。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-22 12:35 , Processed in 0.019003 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部