日志调试法:从print到logging的进化之路

说实话,我刚开始写代码那会儿,调试全靠print。那时候觉得这玩意儿简单粗暴,往代码里塞几个print,跑一遍看看输出,问题就找到了。但随着项目越做越大,print调试的弊端越来越明显——代码里到处都是print,改完还得一个个删,删漏了还会污染线上日志。今天我就跟你聊聊,怎么从print进化到专业的logging模块。

print调试:方便但危险

print调试最大的优点就是简单。你不需要学任何新东西,print函数谁都会用。我在小项目里也经常这么干,快速验证一个想法,print一下看看结果对不对。

但它的缺点同样明显:

  • 污染代码:print语句散落在业务逻辑中,影响可读性
  • 无法分级:所有输出混在一起,分不清哪些是调试信息,哪些是错误
  • 输出不可控:不能动态开关,要么全有要么全无
  • 性能影响:print是同步IO操作,在高频循环里会拖慢程序
我曾经在生产环境遇到过一个问题:同事在循环里留了个print,结果日志文件一天涨了10个G,直接把磁盘撑爆了。从那以后,我对print调试就格外谨慎。

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 - 磁盘空间不足

你看,有了这些信息,定位问题就方便多了。时间戳告诉你什么时候出的问题,模块名和行号告诉你代码在哪里出的问题。

小技巧:我习惯在格式化字符串里加上%(process)d和%(thread)d,这样在多进程或多线程环境下,能清楚知道是哪个进程或线程输出的日志。

输出到文件:持久化日志

把日志输出到控制台只是第一步,真正生产环境里,日志必须写到文件里。这样即使程序崩溃了,日志还在。

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模块的核心脉络:

日志调试法知识体系 logging模块 日志级别(5级) DEBUG → INFO → WARNING ERROR → CRITICAL 日志格式化 时间 / 级别 / 模块 / 行号 进程ID / 线程ID 输出目标 控制台(StreamHandler) 文件(FileHandler) 核心:分级控制 + 灵活输出 + 结构化信息

避坑指南

最后分享几个我踩过的坑:

  • 不要在生产环境用print:我曾经见过一个项目,线上代码里还留着几十个print,每次请求都往stdout写数据,导致日志系统完全混乱。
  • 日志文件要轮转:单文件日志会越来越大,我建议用RotatingFileHandler,按大小或时间自动分割。
  • 敏感信息别打日志:密码、身份证号这些,千万别直接打到日志里。我见过有人把用户密码明文打日志的,那画面太美不敢看。
  • 日志别打太多:DEBUG级别在开发环境没问题,但生产环境如果开了DEBUG,IO压力会很大。我一般生产环境只开WARNING及以上。

好了,logging模块的基础就这些。说白了,它就是print的工业化版本——更可控、更规范、更适合团队协作。下次你再想print调试的时候,不妨试试logging,用习惯了就回不去了。

可可精品资料库