日志调试法:从print到logging的进化之路
说实话,我刚开始写代码那会儿,调试全靠print。那时候觉得这玩意儿简单粗暴,往代码里塞几个print,跑一遍看看输出,问题就找到了。但随着项目越做越大,print调试的弊端越来越明显——代码里到处都是print,改完还得一个个删,删漏了还会污染线上日志。今天我就跟你聊聊,怎么从print进化到专业的logging模块。
print调试:方便但危险
print调试最大的优点就是简单。你不需要学任何新东西,print函数谁都会用。我在小项目里也经常这么干,快速验证一个想法,print一下看看结果对不对。
但它的缺点同样明显:
- 污染代码:print语句散落在业务逻辑中,影响可读性
- 无法分级:所有输出混在一起,分不清哪些是调试信息,哪些是错误
- 输出不可控:不能动态开关,要么全有要么全无
- 性能影响:print是同步IO操作,在高频循环里会拖慢程序
logging模块:专业调试的起点
Python自带的logging模块,说白了就是print的升级版。它解决了print的所有痛点,而且用起来并不复杂。
先看一个最简单的例子:
import logging
logging.debug("这是调试信息")
logging.info("这是普通信息")
logging.warning("这是警告")
logging.error("这是错误")
logging.critical("这是严重错误")
运行这段代码,你会发现只有warning及以上的信息被打印出来了。为什么?因为默认的日志级别是WARNING。
日志级别:五个等级,各有用途
logging模块定义了五个级别,从低到高分别是:
| 级别 | 数值 | 什么时候用 |
|---|---|---|
| DEBUG | 10 | 开发调试时用的详细信息,比如变量值、函数调用栈 |
| INFO | 20 | 程序正常运行的信息,比如启动完成、请求到达 |
| WARNING | 30 | 程序还能跑,但需要注意的情况,比如磁盘快满了 |
| ERROR | 40 | 程序出错了,但还能继续运行,比如数据库连接失败 |
| CRITICAL | 50 | 严重错误,程序可能无法继续运行,比如内存耗尽 |
我个人习惯在开发环境用DEBUG级别,生产环境用INFO或WARNING级别。这样既能保证开发时看到足够的信息,又不会在生产环境里刷屏。
日志格式化:让输出更有价值
光有级别还不够,你还需要控制日志的格式。默认的格式太简陋了,只有一条消息。我建议至少包含时间、级别、模块名和行号。
import logging
logging.basicConfig(
format='%(asctime)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logging.warning("磁盘空间不足")
输出效果:
2024-01-15 14:30:22 - WARNING - main:10 - 磁盘空间不足
你看,有了这些信息,定位问题就方便多了。时间戳告诉你什么时候出的问题,模块名和行号告诉你代码在哪里出的问题。
输出到文件:持久化日志
把日志输出到控制台只是第一步,真正生产环境里,日志必须写到文件里。这样即使程序崩溃了,日志还在。
import logging
logging.basicConfig(
filename='app.log',
filemode='a',
format='%(asctime)s - %(levelname)s - %(message)s',
level=logging.DEBUG
)
logging.info("程序启动成功")
logging.error("数据库连接失败")
这里有几个参数要注意:
- filename:日志文件的路径
- filemode:'a'表示追加,'w'表示覆盖。我建议用'a',除非你明确想清空日志
- level:设置日志级别,低于这个级别的日志不会被记录
嗯,这里有个坑。如果你同时设置了控制台输出和文件输出,basicConfig就不够用了。你需要用到Handler:
import logging
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 控制台输出
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
# 文件输出
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.DEBUG)
# 格式化
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# 添加处理器
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.debug("这条只写到文件")
logger.warning("这条控制台和文件都会写")
这样,控制台只显示WARNING及以上的信息,而文件里记录所有DEBUG及以上的信息。生产环境里我经常这么配,既不会在控制台刷屏,又保留了完整的调试信息。
知识体系总览
下面这张图帮你理清logging模块的核心脉络:
避坑指南
最后分享几个我踩过的坑:
- 不要在生产环境用print:我曾经见过一个项目,线上代码里还留着几十个print,每次请求都往stdout写数据,导致日志系统完全混乱。
- 日志文件要轮转:单文件日志会越来越大,我建议用RotatingFileHandler,按大小或时间自动分割。
- 敏感信息别打日志:密码、身份证号这些,千万别直接打到日志里。我见过有人把用户密码明文打日志的,那画面太美不敢看。
- 日志别打太多:DEBUG级别在开发环境没问题,但生产环境如果开了DEBUG,IO压力会很大。我一般生产环境只开WARNING及以上。
好了,logging模块的基础就这些。说白了,它就是print的工业化版本——更可控、更规范、更适合团队协作。下次你再想print调试的时候,不妨试试logging,用习惯了就回不去了。