告别 `print` 调试:Python `logging` 模块的生产级应用实践

41次阅读
没有评论

共计 6494 个字符,预计需要花费 17 分钟才能阅读完成。

记得刚入行那会,写代码调试全靠 print。项目小的时候倒也勉强凑合,但随着代码量和复杂度的提升,尤其是在生产环境里,print 简直就是灾难。线上服务一旦出问题,光靠 print 根本无从下手,只能干瞪眼。我当时就经历过一个 Bug,代码跑在服务器上,用户反馈了一个诡异的异常,本地怎么都复现不了,print 更是啥也看不见。那一刻我才意识到,Python 官方自带的 logging 模块,才是线上问题排查的“救命稻草”。今天,咱们就一起告别 print 的“原始时代”,真正掌握 logging 的高效用法。


一、为什么不能只用 printlogging 的核心价值

很多初学者可能觉得 print 能直接输出信息,简单直观,为什么非要用 logging 这种看起来“更复杂”的东西?其实,logging 远不止“输出信息”那么简单,它是一个成熟的日志管理框架,能解决 print 无法处理的诸多问题:

  1. 分级管理 logging 支持不同的日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL),你可以根据重要性过滤日志,比如只在开发环境看 DEBUG 详细信息,线上只记录 ERROR 和 CRITICAL。
  2. 目标多样化 logging 不仅能输出到控制台,还能轻松输出到文件、网络、数据库等。
  3. 格式化 :可以自定义日志的输出格式,包含时间、模块名、行号等,方便追溯问题。
  4. 可配置性 :通过配置文件或代码动态调整日志行为,无需修改业务逻辑。
  5. 高性能 :在生产环境中,合理配置的 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 是日志内容。了解这些能让你自定义出更易读的日志。

第二步:进阶用法——灵活配置 HandlerFormatter

basicConfig 固然简单,但它有个缺点:不够灵活。比如你想同时把日志输出到控制台和文件,或者想对不同级别的日志使用不同的处理器,basicConfig 就力不从心了。这时,咱们就需要手动配置 LoggerHandlerFormatter

  • 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 模块提供了 RotatingFileHandlerTimedRotatingFileHandler 来实现日志轮转。

实操代码(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'
# )
# 这种按时间轮转的策略,在处理日志量不固定但需要按日期归档的场景下特别方便。

小提醒:

  • maxBytesbackupCount 需要根据你的应用日志量和磁盘空间来合理设置。别像我刚开始那样,只顾着写功能,把日志管理丢在一边。
  • TimedRotatingFileHandler 是按时间(比如每天、每周)进行轮转,适合需要按日期归档日志的场景。

三、常见误区与避坑指南

作为一名摸爬滚打十年的老兵,我见过、也踩过不少 logging 相关的“坑”,这里给大家总结几个最常见的,希望能帮你少走弯路:

  1. 误区一:线上项目还在大量使用 print

    • 后果 print 无法控制输出级别、无法输出到文件、无法灵活配置,最关键的是,频繁的 print 可能会对线上服务的性能产生负面影响。我曾接过一个项目,线上 Bug 出了问题,排查起来简直是噩梦,代码里到处都是 print,但因为部署环境的限制,根本看不到 print 的输出,最后只能靠猜。
    • 建议 :从开发初期就养成使用 logging 的习惯。即使是临时的调试信息,也尽量用 logger.debug(),这样在生产环境可以直接通过日志级别过滤掉。
  2. 误区二:不理解日志级别,或者级别设置不当

    • 后果 :要么线上全是 DEBUG 级别的无关信息,日志文件迅速膨胀,查找关键错误变得困难;要么级别设置过高,重要的 INFOWARNING 信息被过滤掉,导致问题发现不及时。我刚开始用 logging 的时候,就经常忘记 basicConfig 默认是 WARNING 级别,导致自己写的 logger.info() 啥也看不见,还以为 logging 坏了。
    • 建议
      • DEBUG:详细的调试信息,仅在开发环境或需要深度排查问题时开启。
      • INFO:程序正常运行的关键信息,如服务启动、请求处理、重要业务操作等。
      • WARNING:潜在的问题,可能不影响程序正常运行,但值得关注。
      • ERROR:程序运行时发生的错误,需要人工介入处理。
      • CRITICAL:严重错误,导致程序无法继续运行,必须立即处理。
      • 线上环境一般设置为 INFOWARNING 级别,出现问题时可以临时调为 DEBUG
  3. 误区三:不进行日志文件轮转,导致磁盘撑爆

    • 后果 :这是我亲身经历的“血泪教训”。线上服务日志量一大,如果不及时清理或轮转,日志文件会迅速占用所有磁盘空间,轻则影响服务性能,重则导致服务崩溃。
    • 建议 :生产环境务必使用 RotatingFileHandlerTimedRotatingFileHandler。根据日志量和保留策略合理配置 maxBytesbackupCountwheninterval
  4. 误区四:在大型项目中,所有日志都用 Root Logger

    • 后果 :如果所有模块都只用 logging.info() 这样的方式(隐式使用 Root Logger),当项目变得庞大时,所有日志信息都混在一起,你很难区分哪些日志是哪个模块发出的。排查问题时,这就像大海捞针。
    • 建议 :为每个模块或逻辑单元创建独立的 Logger 实例,例如 logger = logging.getLogger(__name__)。这样,日志输出时会包含模块名,更易于追踪。

四、经验总结与互动引导

日志系统是项目健康运行的眼睛,学会合理配置和使用 logging,能让你在生产环境少掉几把头发,快速定位问题,有效提升系统的稳定性和可维护性。从现在开始,就彻底告别 print,拥抱 logging 吧!

大家在用 logging 的时候,还遇到过哪些有意思的“坑”?或者有哪些独门秘籍,欢迎在评论区分享你的经验!

正文完
 0
评论(没有评论)