Skip to content

logging 日志 ​

print() 适合显示程序结果和临时检查。程序开始变复杂后,如何长期记录运行过程、区分信息的重要程度,并在出问题时还原现场?这时应该使用 logging 模块。

日志通常需要回答这些问题:

  • 程序在什么时候执行了什么操作?
  • 哪个模块记录了这条信息?
  • 这条信息是普通状态、警告还是错误?
  • 出现问题时,能否根据记录还原现场?

本章将为一个小型多文件程序建立统一日志配置。

logging 的三层结构

日志级别 ​

logging 提供五个常用级别,严重程度从低到高排列:

级别适合记录的内容
DEBUG调试细节和中间状态
INFO正常运行过程中的重要信息
WARNING出现异常情况,但程序还能继续
ERROR某项操作失败
CRITICAL严重错误,程序可能无法继续

可以直接使用模块提供的日志函数:

python
import logging

logging.debug("这里是调试信息")
logging.info("程序正在运行")
logging.warning("输入内容需要检查")
logging.error("文件保存失败")
logging.critical("程序无法继续运行")

没有主动配置时,第一次调用模块级日志函数会为根日志器建立默认配置。默认级别是 WARNING,因此通常只能看到 WARNING 及以上内容:

text
WARNING:root:输入内容需要检查
ERROR:root:文件保存失败
CRITICAL:root:程序无法继续运行

默认的 StreamHandler 把日志写入 sys.stderr。虽然日志显示在终端中,但它和普通 print() 默认使用的 stdout 不是同一条标准流。

日志级别严重度与默认阈值门限过滤机制

基础配置 ​

小型程序可以在入口处调用一次 logging.basicConfig():

python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

logging.debug("这条日志不会显示")
logging.info("程序启动")

一次可能的输出:

text
2026-09-12 10:30:00 | INFO | root | 程序启动

常用参数如下:

参数作用
level设置根日志器的最低记录级别
format设置日志格式
datefmt设置日志时间格式
filename把日志写入指定文件
filemode文件打开模式,默认是追加模式 a
encoding日志文件编码

basicConfig() 通常只在根日志器还没有 Handler 时生效。在同一个交互环境中连续运行多个配置示例,后面的配置可能看不到效果。最简单的处理方式是重新启动解释器,并保证程序入口只配置一次。

文件日志 ​

只写入一个文件时,可以直接使用 filename:

python
import logging

logging.basicConfig(
    filename="app.log",
    filemode="a",
    encoding="utf-8",
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
)

logging.info("程序启动")
logging.warning("数据不完整")

设置 encoding="utf-8" 可以稳定保存中文日志。追加模式不会在每次启动时覆盖旧记录,但日志文件会不断增长;长期运行的项目通常还需要日志轮转,本基础章节暂不展开。

日志路径也是文件路径。相对路径会根据当前工作目录解析,需要固定位置时,应先计算明确的绝对路径。

变量记录 ​

日志中经常需要记录变量。推荐把格式字符串和变量分别传给日志函数:

python
import logging

name = "张三"
score = 85

logging.info("学生 %s 的成绩是 %d", name, score)

这种写法由日志模块完成格式化。当前级别不需要输出这条日志时,可以避免提前构造最终字符串。

因此,记录日志时通常优先使用参数形式,而不是提前使用 f-string:

python
logging.debug("当前学生数量:%d", student_count)

模块级 Logger ​

实际项目不应该让所有模块都直接使用根日志器。功能模块通常使用当前模块名创建 Logger:

python
import logging

logger = logging.getLogger(__name__)


def add_student(name):
    logger.info("已添加学生:%s", name)

getLogger() 多次接收同一个名称时,会返回同一个 Logger 对象。使用 __name__ 后,日志中会显示来源模块,例如:

text
INFO | student_service | 已添加学生:张三

功能模块只负责获取 Logger 和记录信息,不负责决定日志写到哪里。控制台、文件和日志格式都由程序入口统一配置。

日志组件 ​

完整日志流程由三个主要角色组成:

text
Logger 产生记录 → Handler 决定输出位置 → Formatter 决定显示格式
角色职责
Logger记录不同级别的日志
Handler把日志发送到终端、文件等位置
Formatter控制每条日志的显示格式

如果日志需要同时进入控制台和文件,可以创建两个 Handler:

python
import logging

formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)

file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)

logging.basicConfig(
    level=logging.DEBUG,
    handlers=[console_handler, file_handler],
)

这里有两层级别判断:

  1. 根日志器接收 DEBUG 及以上日志。
  2. 控制台只输出 INFO 及以上,文件保存 DEBUG 及以上。

因此,调试细节可以保存在文件里,而控制台不会被大量细节占满。

双层级别过滤机制与控制台文件分流管道

重复日志控制 ​

日志重复出现,通常不是日志函数执行错了,而是同一条记录经过了多个 Handler。

基础项目遵循下面三条规则,就能避免大部分重复问题:

  1. 只在程序入口配置日志。
  2. 功能模块只使用 logging.getLogger(__name__),不要自行添加 Handler。
  3. 不要在循环或每次调用函数时重复执行日志配置。

如果确实要给某个子 Logger 单独添加 Handler,还要注意日志默认会继续传递给上级 Logger。可以根据项目结构设置:

python
logger.propagate = False

设置后,该 Logger 的日志不会再向上级传播。但对本章的小型项目来说,入口统一配置更简单,也更不容易出错。

综合案例:记录学生操作 ​

需求: 为学生操作程序建立统一日志配置,让控制台显示重要过程,同时将调试细节保存到文件。

功能分析: 功能模块只记录业务日志,程序入口统一创建控制台和文件 Handler,并为两个输出设置不同的最低级别。

项目包含一个入口文件和一个功能模块:

text
01-student-logging/
├── main.py
└── student_service.py

student_service.py 只创建模块 Logger,并记录业务过程:

python
import logging


logger = logging.getLogger(__name__)
student_names = []


def add_student(name):
    if not name:
        logger.warning("添加学生失败:姓名不能为空")
        return False

    student_names.append(name)
    logger.info("已添加学生:%s", name)
    logger.debug("当前学生数量:%d", len(student_names))
    return True

main.py 统一配置控制台和文件输出:

python
import logging
import os

import student_service


def configure_logging():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    log_path = os.path.join(script_dir, "app.log")

    formatter = logging.Formatter(
        "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )

    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)

    file_handler = logging.FileHandler(log_path, encoding="utf-8")
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)

    logging.basicConfig(
        level=logging.DEBUG,
        handlers=[console_handler, file_handler],
    )
    return log_path


def main():
    log_path = configure_logging()
    logger = logging.getLogger(__name__)

    logger.info("学生程序启动")
    student_service.add_student("张三")
    student_service.add_student("")
    logger.info("学生程序结束")

    print(f"日志文件:{log_path}")


if __name__ == "__main__":
    main()

运行后:

  • INFO 和 WARNING 会显示在终端的标准错误流中。
  • DEBUG、INFO 和 WARNING 都会写入 UTF-8 编码的 app.log。
  • student_service 没有添加 Handler,不会因为业务函数被多次调用而重复配置日志。

日志输出的时间会随运行时刻变化。文件中可以看到类似记录:

text
2026-09-12 10:30:00 | INFO | __main__ | 学生程序启动
2026-09-12 10:30:00 | INFO | student_service | 已添加学生:张三
2026-09-12 10:30:00 | DEBUG | student_service | 当前学生数量:1
2026-09-12 10:30:00 | WARNING | student_service | 添加学生失败:姓名不能为空
2026-09-12 10:30:00 | INFO | __main__ | 学生程序结束

日志边界与注意事项 ​

  • 不要记录密码、访问令牌、身份证号等敏感信息。
  • 不要在循环中无节制地记录大量低价值日志。
  • 文件日志默认会持续增长,长期运行项目需要考虑轮转和保留周期。
  • logging.exception() 应在捕获异常后使用,它会附带异常堆栈;完整用法将在异常处理章节学习。
  • 日志用于记录运行过程,正常的程序输出仍然可以使用 print()。

小结 ​

  • 日志级别用于区分信息的重要程度,默认显示 WARNING 及以上级别。
  • basicConfig() 适合在小型程序入口完成一次统一配置。
  • 文件日志应明确使用 UTF-8 编码,并注意默认追加模式会让文件持续增长。
  • 记录变量时,优先把格式字符串和变量分别传给日志函数。
  • 功能模块使用 logging.getLogger(__name__),不要自行配置 Handler。
  • Logger 产生记录,Handler 决定输出位置,Formatter 决定显示格式。
  • 控制台和文件设置不同级别,可以兼顾简洁输出与调试细节。

练习 ​

  1. 分别记录 DEBUG、INFO、WARNING、ERROR 和 CRITICAL 日志,观察默认显示哪些内容。
  2. 使用 basicConfig() 把中文日志追加到 UTF-8 文件中。
  3. 创建两个功能模块,分别使用 logging.getLogger(__name__) 记录信息。
  4. 配置控制台只显示 WARNING 及以上日志,文件保存 DEBUG 及以上日志。
  5. 修改综合案例,多次调用 add_student(),确认日志不会因为重复配置而成倍出现。