logging 日志
print() 适合显示程序结果和临时检查。程序开始变复杂后,如何长期记录运行过程、区分信息的重要程度,并在出问题时还原现场?这时应该使用 logging 模块。
日志通常需要回答这些问题:
- 程序在什么时候执行了什么操作?
- 哪个模块记录了这条信息?
- 这条信息是普通状态、警告还是错误?
- 出现问题时,能否根据记录还原现场?
本章将为一个小型多文件程序建立统一日志配置。
日志级别
logging 提供五个常用级别,严重程度从低到高排列:
| 级别 | 适合记录的内容 |
|---|---|
DEBUG | 调试细节和中间状态 |
INFO | 正常运行过程中的重要信息 |
WARNING | 出现异常情况,但程序还能继续 |
ERROR | 某项操作失败 |
CRITICAL | 严重错误,程序可能无法继续 |
可以直接使用模块提供的日志函数:
import logging
logging.debug("这里是调试信息")
logging.info("程序正在运行")
logging.warning("输入内容需要检查")
logging.error("文件保存失败")
logging.critical("程序无法继续运行")没有主动配置时,第一次调用模块级日志函数会为根日志器建立默认配置。默认级别是 WARNING,因此通常只能看到 WARNING 及以上内容:
WARNING:root:输入内容需要检查
ERROR:root:文件保存失败
CRITICAL:root:程序无法继续运行默认的 StreamHandler 把日志写入 sys.stderr。虽然日志显示在终端中,但它和普通 print() 默认使用的 stdout 不是同一条标准流。
基础配置
小型程序可以在入口处调用一次 logging.basicConfig():
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("程序启动")一次可能的输出:
2026-09-12 10:30:00 | INFO | root | 程序启动常用参数如下:
| 参数 | 作用 |
|---|---|
level | 设置根日志器的最低记录级别 |
format | 设置日志格式 |
datefmt | 设置日志时间格式 |
filename | 把日志写入指定文件 |
filemode | 文件打开模式,默认是追加模式 a |
encoding | 日志文件编码 |
basicConfig() 通常只在根日志器还没有 Handler 时生效。在同一个交互环境中连续运行多个配置示例,后面的配置可能看不到效果。最简单的处理方式是重新启动解释器,并保证程序入口只配置一次。
文件日志
只写入一个文件时,可以直接使用 filename:
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" 可以稳定保存中文日志。追加模式不会在每次启动时覆盖旧记录,但日志文件会不断增长;长期运行的项目通常还需要日志轮转,本基础章节暂不展开。
日志路径也是文件路径。相对路径会根据当前工作目录解析,需要固定位置时,应先计算明确的绝对路径。
变量记录
日志中经常需要记录变量。推荐把格式字符串和变量分别传给日志函数:
import logging
name = "张三"
score = 85
logging.info("学生 %s 的成绩是 %d", name, score)这种写法由日志模块完成格式化。当前级别不需要输出这条日志时,可以避免提前构造最终字符串。
因此,记录日志时通常优先使用参数形式,而不是提前使用 f-string:
logging.debug("当前学生数量:%d", student_count)模块级 Logger
实际项目不应该让所有模块都直接使用根日志器。功能模块通常使用当前模块名创建 Logger:
import logging
logger = logging.getLogger(__name__)
def add_student(name):
logger.info("已添加学生:%s", name)getLogger() 多次接收同一个名称时,会返回同一个 Logger 对象。使用 __name__ 后,日志中会显示来源模块,例如:
INFO | student_service | 已添加学生:张三功能模块只负责获取 Logger 和记录信息,不负责决定日志写到哪里。控制台、文件和日志格式都由程序入口统一配置。
日志组件
完整日志流程由三个主要角色组成:
Logger 产生记录 → Handler 决定输出位置 → Formatter 决定显示格式| 角色 | 职责 |
|---|---|
Logger | 记录不同级别的日志 |
Handler | 把日志发送到终端、文件等位置 |
Formatter | 控制每条日志的显示格式 |
如果日志需要同时进入控制台和文件,可以创建两个 Handler:
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],
)这里有两层级别判断:
- 根日志器接收
DEBUG及以上日志。 - 控制台只输出
INFO及以上,文件保存DEBUG及以上。
因此,调试细节可以保存在文件里,而控制台不会被大量细节占满。
重复日志控制
日志重复出现,通常不是日志函数执行错了,而是同一条记录经过了多个 Handler。
基础项目遵循下面三条规则,就能避免大部分重复问题:
- 只在程序入口配置日志。
- 功能模块只使用
logging.getLogger(__name__),不要自行添加 Handler。 - 不要在循环或每次调用函数时重复执行日志配置。
如果确实要给某个子 Logger 单独添加 Handler,还要注意日志默认会继续传递给上级 Logger。可以根据项目结构设置:
logger.propagate = False设置后,该 Logger 的日志不会再向上级传播。但对本章的小型项目来说,入口统一配置更简单,也更不容易出错。
综合案例:记录学生操作
需求: 为学生操作程序建立统一日志配置,让控制台显示重要过程,同时将调试细节保存到文件。
功能分析: 功能模块只记录业务日志,程序入口统一创建控制台和文件 Handler,并为两个输出设置不同的最低级别。
项目包含一个入口文件和一个功能模块:
01-student-logging/
├── main.py
└── student_service.pystudent_service.py 只创建模块 Logger,并记录业务过程:
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 Truemain.py 统一配置控制台和文件输出:
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,不会因为业务函数被多次调用而重复配置日志。
日志输出的时间会随运行时刻变化。文件中可以看到类似记录:
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决定显示格式。- 控制台和文件设置不同级别,可以兼顾简洁输出与调试细节。
练习
- 分别记录
DEBUG、INFO、WARNING、ERROR和CRITICAL日志,观察默认显示哪些内容。 - 使用
basicConfig()把中文日志追加到 UTF-8 文件中。 - 创建两个功能模块,分别使用
logging.getLogger(__name__)记录信息。 - 配置控制台只显示
WARNING及以上日志,文件保存DEBUG及以上日志。 - 修改综合案例,多次调用
add_student(),确认日志不会因为重复配置而成倍出现。