共计 6494 个字符,预计需要花费 17 分钟才能阅读完成。
记得刚入行那会,写代码调试全靠 print。项目小的时候倒也勉强凑合,但随着代码量和复杂度的提升,尤其是在生产环境里,print 简直就是灾难。线上服务一旦出问题,光靠 print 根本无从下手,只能干瞪眼。我当时就经历过一个 Bug,代码跑在服务器上,用户反馈了一个诡异的异常,本地怎么都复现不了,print 更是啥也看不见。那一刻我才意识到,Python 官方自带的 logging 模块,才是线上问题排查的“救命稻草”。今天,咱们就一起告别 print 的“原始时代”,真正掌握 logging 的高效用法。
一、为什么不能只用 print?logging 的核心价值
很多初学者可能觉得 print 能直接输出信息,简单直观,为什么非要用 logging 这种看起来“更复杂”的东西?其实,logging 远不止“输出信息”那么简单,它是一个成熟的日志管理框架,能解决 print 无法处理的诸多问题:
- 分级管理 :
logging支持不同的日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL),你可以根据重要性过滤日志,比如只在开发环境看 DEBUG 详细信息,线上只记录 ERROR 和 CRITICAL。 - 目标多样化 :
logging不仅能输出到控制台,还能轻松输出到文件、网络、数据库等。 - 格式化 :可以自定义日志的输出格式,包含时间、模块名、行号等,方便追溯问题。
- 可配置性 :通过配置文件或代码动态调整日志行为,无需修改业务逻辑。
- 高性能 :在生产环境中,合理配置的
logging相比频繁的print,对性能的影响更小。
可以说,logging 是一个健壮 Python 应用的“眼睛”和“耳朵”,帮你监控应用运行状况,并在第一时间发现并定位问题。
二、从零开始:logging 的基础配置与实操
咱们先从最简单的开始,一步步搭建一个实用的日志系统。
第一步:快速上手 basicConfig(新手进阶必看)
很多新手刚接触 logging 模块时,会直接写 logging.info("这是一条信息"),然后发现什么都没打印出来,心里一万个问号。这其实是 logging 的一个“坑”,默认情况下,logging 的日志级别是 WARNING。也就是说,低于 WARNING 级别的日志(如 DEBUG, INFO)是不会被打印出来的。
实操代码:
import logging
# 配置日志,这里我们把级别设置为 INFO,这样 INFO 及以上级别的日志都会被处理
# filename 参数指定了日志输出到哪个文件
# filemode='w' 表示写入模式,每次运行会覆盖旧文件。生产环境通常用 'a' (append)
# format 定义了日志的输出格式
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
filename='app.log', # 第一次运行可以注释掉,看控制台输出
filemode='a' # 提醒下,生产环境咱们通常用 'a' (append) 模式,不然每次启动程序,以前的日志就全没了。我刚开始学的时候就犯过这个错误,好几次宝贵的调试信息都被覆盖了。)
logging.debug("这是一条调试信息,通常只在开发环境有用")
logging.info("程序正常运行,处理了一个请求。")
logging.warning("发现一个潜在问题,请关注!")
logging.error("程序出错啦!")
logging.critical("严重错误,程序可能无法继续运行!")
print("日志已输出到 app.log 文件(如果指定了 filename),或控制台。")
# 再次尝试打印一条 DEBUG 级别日志,看看能否显示
logging.debug("这应该不会在默认 INFO 级别下显示,除非你把 basicConfig 的 level 改成 DEBUG")
小提醒:
logging.basicConfig()只能被调用一次。如果你在程序的不同部分多次调用它,只有第一次的配置会生效。这是个常见的“坑”,很多人在封装日志工具时,不小心会多次调用,导致后续配置不生效。level参数是关键,它决定了哪些级别的日志会被处理。务必根据你的需求设置。format字符串里的%()语法是占位符,比如%(asctime)s是时间,%(levelname)s是日志级别,%(message)s是日志内容。了解这些能让你自定义出更易读的日志。
第二步:进阶用法——灵活配置 Handler 和 Formatter
basicConfig 固然简单,但它有个缺点:不够灵活。比如你想同时把日志输出到控制台和文件,或者想对不同级别的日志使用不同的处理器,basicConfig 就力不从心了。这时,咱们就需要手动配置 Logger、Handler 和 Formatter。
- Logger:日志记录器,是日志系统的入口,负责接收日志消息。
- Handler:处理器,负责将日志消息发送到指定目的地(文件、控制台等)。
- Formatter:格式器,负责定义日志消息的输出格式。
实操代码:
import logging
import sys
# 1. 创建一个 Logger 实例
# 这里我们给 Logger 起名叫 'my_app',生产环境中,咱们通常会为不同的模块或服务创建不同的 Logger,方便区分。logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG) # 设置 Logger 的总日志级别为 DEBUG
# 2. 创建一个 Console Handler (StreamHandler)
# 负责把日志输出到控制台
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO) # 控制台只输出 INFO 及以上级别的日志
# 3. 创建一个 File Handler
# 负责把日志输出到文件
file_handler = logging.FileHandler('detailed_app.log', mode='a', encoding='utf-8')
file_handler.setLevel(logging.DEBUG) # 文件里记录所有 DEBUG 及以上级别的日志
# 4. 创建一个 Formatter
# 定义日志的输出格式
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s'
)
# 5. 为 Handler 设置 Formatter
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# 6. 将 Handler 添加到 Logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# 现在就可以使用 logger 记录日志了
logger.debug("这条 DEBUG 消息只会出现在 detailed_app.log 文件中。")
logger.info("这条 INFO 消息会同时出现在控制台和文件中。")
logger.warning("这条 WARNING 消息也会同时出现在控制台和文件中。")
def some_function():
logger.error("在 some_function 中发生了错误!") # 注意看日志输出会带上 funcName 和 lineno
# 这里加 try-except 是因为之前爬取豆瓣时遇到过空值报错,当时没加日志,排查了半天,# 后来才明白,任何可能出错的地方都应该有详细的日志记录,而且要带上上下文信息!try:
1 / 0
except ZeroDivisionError:
logger.exception("除零错误发生!logger.exception 会自动记录异常栈信息。")
some_function()
小提醒:
sys.stdout可以让日志输出到标准输出,sys.stderr则输出到标准错误。logger.exception()方法非常有用,它会自动记录当前异常的完整栈信息(Traceback),这在排查 Bug 时能省你大把时间。%(name)s占位符会显示你创建 Logger 时传入的名称(比如 ‘my_app’),这对于在大项目中区分不同模块的日志非常关键。
第三步:生产环境利器——日志文件轮转(RotatingFileHandler)
随着应用的长时间运行,日志文件会越来越大,最终可能会撑爆服务器磁盘。我刚开始工作时就遇到过这种“惊魂时刻”,一个服务跑了几个月,磁盘空间忽然告急,一查才发现是日志文件几十个 G,当时吓出一身冷汗。为了避免这种情况,咱们必须引入日志轮转机制。
logging 模块提供了 RotatingFileHandler 和 TimedRotatingFileHandler 来实现日志轮转。
实操代码(RotatingFileHandler):
import logging
from logging.handlers import RotatingFileHandler
import time
import os
# 清理旧的日志文件,方便测试
if os.path.exists('rotated_app.log'):
os.remove('rotated_app.log')
if os.path.exists('rotated_app.log.1'):
os.remove('rotated_app.log.1')
if os.path.exists('rotated_app.log.2'):
os.remove('rotated_app.log.2')
logger_rot = logging.getLogger('my_rotated_app')
logger_rot.setLevel(logging.INFO)
# 创建一个 RotatingFileHandler
# filename: 日志文件名
# maxBytes: 单个日志文件的最大字节数(这里设置 10KB,方便演示)# backupCount: 保留的备份文件数量。当 rotated_app.log 达到 maxBytes 后,# 它会被重命名为 rotated_app.log.1,如果 rotated_app.log.1 存在,则会被重命名为 rotated_app.log.2,以此类推。# 超过 backupCount 的文件会被删除。rotating_handler = RotatingFileHandler(
'rotated_app.log',
maxBytes=10 * 1024, # 10KB,实际生产环境通常设置为几十 MB 甚至 GB
backupCount=3, # 保留 3 个旧日志文件
encoding='utf-8'
)
formatter_rot = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
rotating_handler.setFormatter(formatter_rot)
logger_rot.addHandler(rotating_handler)
# 模拟写入大量日志,观察文件轮转
print("开始写入日志,模拟文件轮转,请观察 rotated_app.log 及其备份文件。")
for i in range(500): # 循环写入 500 条日志
logger_rot.info(f"这是第 {i+1} 条日志消息,测试文件轮转功能。")
# time.sleep(0.01) # 生产环境日志可能写得非常快,这里为了演示效果,可以稍微暂停
print("n 日志写入完成,请检查当前目录下的日志文件。")
# 生产环境咱们通常还会用 TimedRotatingFileHandler,按天 / 小时轮转,例如:# from logging.handlers import TimedRotatingFileHandler
# timed_handler = TimedRotatingFileHandler(
# 'timed_app.log',
# when='midnight', # 每天午夜轮转
# interval=1,
# backupCount=7, # 保留最近 7 天的日志
# encoding='utf-8'
# )
# 这种按时间轮转的策略,在处理日志量不固定但需要按日期归档的场景下特别方便。
小提醒:
maxBytes和backupCount需要根据你的应用日志量和磁盘空间来合理设置。别像我刚开始那样,只顾着写功能,把日志管理丢在一边。TimedRotatingFileHandler是按时间(比如每天、每周)进行轮转,适合需要按日期归档日志的场景。
三、常见误区与避坑指南
作为一名摸爬滚打十年的老兵,我见过、也踩过不少 logging 相关的“坑”,这里给大家总结几个最常见的,希望能帮你少走弯路:
-
误区一:线上项目还在大量使用
print- 后果 :
print无法控制输出级别、无法输出到文件、无法灵活配置,最关键的是,频繁的print可能会对线上服务的性能产生负面影响。我曾接过一个项目,线上 Bug 出了问题,排查起来简直是噩梦,代码里到处都是print,但因为部署环境的限制,根本看不到print的输出,最后只能靠猜。 - 建议 :从开发初期就养成使用
logging的习惯。即使是临时的调试信息,也尽量用logger.debug(),这样在生产环境可以直接通过日志级别过滤掉。
- 后果 :
-
误区二:不理解日志级别,或者级别设置不当
- 后果 :要么线上全是
DEBUG级别的无关信息,日志文件迅速膨胀,查找关键错误变得困难;要么级别设置过高,重要的INFO或WARNING信息被过滤掉,导致问题发现不及时。我刚开始用logging的时候,就经常忘记basicConfig默认是WARNING级别,导致自己写的logger.info()啥也看不见,还以为logging坏了。 - 建议 :
- DEBUG:详细的调试信息,仅在开发环境或需要深度排查问题时开启。
- INFO:程序正常运行的关键信息,如服务启动、请求处理、重要业务操作等。
- WARNING:潜在的问题,可能不影响程序正常运行,但值得关注。
- ERROR:程序运行时发生的错误,需要人工介入处理。
- CRITICAL:严重错误,导致程序无法继续运行,必须立即处理。
- 线上环境一般设置为
INFO或WARNING级别,出现问题时可以临时调为DEBUG。
- 后果 :要么线上全是
-
误区三:不进行日志文件轮转,导致磁盘撑爆
- 后果 :这是我亲身经历的“血泪教训”。线上服务日志量一大,如果不及时清理或轮转,日志文件会迅速占用所有磁盘空间,轻则影响服务性能,重则导致服务崩溃。
- 建议 :生产环境务必使用
RotatingFileHandler或TimedRotatingFileHandler。根据日志量和保留策略合理配置maxBytes、backupCount或when、interval。
-
误区四:在大型项目中,所有日志都用 Root Logger
- 后果 :如果所有模块都只用
logging.info()这样的方式(隐式使用 Root Logger),当项目变得庞大时,所有日志信息都混在一起,你很难区分哪些日志是哪个模块发出的。排查问题时,这就像大海捞针。 - 建议 :为每个模块或逻辑单元创建独立的
Logger实例,例如logger = logging.getLogger(__name__)。这样,日志输出时会包含模块名,更易于追踪。
- 后果 :如果所有模块都只用
四、经验总结与互动引导
日志系统是项目健康运行的眼睛,学会合理配置和使用 logging,能让你在生产环境少掉几把头发,快速定位问题,有效提升系统的稳定性和可维护性。从现在开始,就彻底告别 print,拥抱 logging 吧!
大家在用 logging 的时候,还遇到过哪些有意思的“坑”?或者有哪些独门秘籍,欢迎在评论区分享你的经验!