简介与实现逻辑
logging
是 Python 标准库中用于记录日志信息的模块,用于跟踪程序运行过程中的状态、错误、调试信息等。它是 print() 调试方法的升级版,具有更强大的功能和更灵活的配置选项。与 print()
相比,logging
更适合用于生产环境和复杂项目,其主要优势有:
- 分级记录不同严重程度的信息
- 支持日志输出到文件、终端、网络等多个位置
- 支持统一格式化输出,便于排查问题
- 可灵活配置不同模块的日志行为
- 支持日志轮换、归档、按时间保存等高级功能
logging
模块的核心架构分为四大组件:
组件名 | 作用说明 |
---|---|
Logger | 日志记录器,提供了应用程序使用的接口(如 logger.info()) |
Handler | 处理器,决定日志记录到哪里(如文件、控制台) |
Formatter | 格式化器,决定日志的输出格式 |
Filter | 过滤器,控制哪些日志能够被输出 |
具体功能与运行流程图如下:
Python 默认提供以下 5 个标准日志等级(从低到高):
等级名称 | 函数 | 数值 | 用途说明 |
---|---|---|---|
DEBUG | logging.debug() | 10 | 最详细的日志信息,主要用于调试 |
INFO | logging.info() | 20 | 一般信息,程序正常运行时的关键记录 |
WARNING | logging.warning() | 30 | 警告信息,表示潜在问题 |
ERROR | logging.error() | 40 | 错误信息,程序发生异常但未崩溃 |
CRITICAL | logging.critical() | 50 | 严重错误,可能导致程序终止 |
可以通过设置 level 来控制日志的输出门槛。例如设置为 WARNING
之后,DEBUG
和 INFO
信息将不会显示。
import logging
logging.basicConfig(level=logging.INFO)
logging.debug("这是 debug 信息") # 不会输出
logging.info("这是 info 信息") # 会输出
logging.warning("这是 warning 信息") # 会输出
logging.error("这是 error 信息") # 会输出
logging.critical("这是 critical 信息") # 会输出
与此同时,logging
模块也是线程安全的,可以放心在多线程程序中使用。例如,在 Web 应用或数据处理程序中记录日志而不必担心冲突。
相关基本配置
本小节主要介绍调用 logging.basicConfig()
进行基础的配置与修改。
配置输出格式
可以使用 $\text{format}$ 参数设置日志日志的输出格式,常见的格式化字段有:
占位符 | 含义 |
---|---|
%(asctime)s | 日志事件发生时间 |
%(levelname)s | 日志等级名称 |
%(name)s | Logger 名称 |
%(filename)s | 调用日志的文件名 |
%(lineno)d | 调用日志的代码行号 |
%(message)s | 日志消息本体 |
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
)
logging.info("启动程序")
设置日志等级
第一种方法可以直接在 basicConfig()
中进行设置如:
logging.basicConfig(level=logging.WARNING)
此时会过滤掉低于该等级的日志,比如上面这句会忽略 DEBUG 和 INFO。
第二种可以在单独的 Logger
对象上设置等级,此时该等级只会对该对象生效:
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
- 设置了 logging.DEBUG 但日志没有输出 → 可能是因为默认等级是
WARNING
,未显式设置level
。 setLevel()
只作用于Logger
,不会影响已添加的Handler
。
输出重定向
需要输入到文件当中时,只需要如下配置:
logging.basicConfig(
filename='app.log',
filemode='a', # 或 'w' 表示覆盖写入
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logging.info("写入日志到文件")
快速配置
参数名 | 功能说明 |
---|---|
filename | 指定日志文件 |
filemode | 文件写入模式 (a, w) |
format | 设置日志输出格式 |
level | 设置最低记录等级 |
encoding | 指定编码方式(如 UTF-8) |
datefmt | 设置时间格式(可选) |
常用配置参考:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s',
filename='my_log.log',
filemode='w',
encoding='utf-8',
datefmt='%Y-%m-%d %H:%M:%S'
)
错误写法
如果更换代码顺序写成了:
import logging
logging.info("This log will show in terminal.")
logging.basicConfig(filename='log.txt', level=logging.INFO)
此时 basicConfig
的配置将会失效。原因是:
logging.basicConfig()
只能在程序第一次使用 logging 之前生效。- 一旦调用了如
logging.info()
、logging.warning()
等函数,Python 会自动创建一个默认的控制台Handler
。 - 后续再调用
basicConfig()
时会被忽略,因为logging
模块一旦检测到已经有Handler
存在时不会重新添加配置。
如果因为某些什么原因导致已有一些 Handler
出现,可以尝试手动清空之后再进行相关配置:
import logging
# 移除默认 root logger 中所有 handler
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
# 重新配置
logging.basicConfig(filename='log.txt', level=logging.INFO)
logging.info("This log will be written at log.txt")
相关高级用法
使用 Logger
、Handler
、Formatter
的组合方式进行高级配置
basicConfig()
是快速配置方法,适用于简单脚本,但在复杂项目中可能需要更细致的控制:
- 各模块使用自己的
logger
(防止日志冲突) - 同时输出到 控制台 + 文件
- 对不同
Handler
设置不同输出格式或等级 - 多个文件/模块写入同一个日志
结构总览:
Logger(记录器)
├── Handler(处理器)→ FileHandler / StreamHandler / RotatingFileHandler 等
│ └── Formatter(格式器)
例如想同时输出到某一日志文件和控制台中,只需要给出两个 Handler
进行配置添加即可:
import sys
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
if __name__ == '__main__':
log_txt = "basic_info.log"
if not logger.handlers:
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# 文件日志
file_handler = logging.FileHandler(log_txt, mode='w') # 默认模式是 'a'
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# 控制台日志
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
不能与 Handler
混用! 如果已经使用了 logger.addHandler
,则调用 basicConfig()
将不再生效。
不同模块日志记录
当有多个模块进行日志记录的写操作时,如果配置不当,有可能会出现同一条日志被输出多次的场景。在 Python
的 logging
模块中,Logger
是一棵树状结构,以 $\text{.}$ 分隔。例如:
root
├── myapp
│ └── myapp.module
这意味着:
logging.getLogger("myapp")
是root logger
的子logger
logging.getLogger("myapp.module")
是myapp logger
的子logger
默认情况下,当在子 logger
中记录日志,其执行流程为:
- 被子
logger
自己的Handler
处理(如果有) - 继续向上冒泡 给父
logger
的Handler
也就是说,如果你只给 root
添加了 handler
,那么子 logger
的日志也会被 root
的 handler
输出。而如果设置 root logger
和子 logger
都添加了 handler
且没有关闭冒泡,就会有重复输出的问题:
# 假设设置了 root logger 和子 logger 且都添加了 handler
# 且没有关闭冒泡
import logging
# root logger
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("myapp.module")
# 给子 logger 再加一个 handler
handler = logging.StreamHandler()
formatter = logging.Formatter("SUB: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("测试日志")
# output
# SUB: 测试日志
# 测试日志
此时一种写法是关闭冒泡操作,子 logger
的日志 不会传递到父 logger
,只由自己配置的 handler
处理:
logger = logging.getLogger("myapp.module")
logger.propagate = False
另一种正确写法是:只在主程序中进行 logger
的配置,其他模块文件直接进行调用即可:
# main.py
import logging
from module_a import run_a
# 配置 root logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
if __name__ == '__main__':
# 添加 handler(控制台 + 文件)
stream = logging.StreamHandler()
file = logging.FileHandler('project.log', encoding='utf-8')
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
stream.setFormatter(formatter)
file.setFormatter(formatter)
logger.addHandler(stream)
logger.addHandler(file)
run_a()
# module_a.py
import logging
logger = logging.getLogger(__name__)
def run_a():
logger.info("module A start.")
或者也可以在多文件中使用名字找到同一个 Logger
进行输出。总体来说,实现多文件输出到同一个日志中有以下几种思路:
- 所有模块用
getLogger('统一名')
获取相同的logger
- 使用
root logger
进行配置设置 - 主程序中统一添加
handler
,子模块不要重复添加
不同文件保存与异常追踪
在实际使用中,有时希望不同的日志信息可以分开文件进行储存,例如希望单独有一个文件进行报错信息的储存。这时可以合理利用之前的分级策略对日志信息进行筛选,在不同的日志文件中保存不同权限的信息即可:
error_handler = logging.FileHandler("error.log", encoding="utf-8")
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
info_handler = logging.FileHandler("info.log", encoding="utf-8")
info_handler.setLevel(logging.INFO)
info_handler.setFormatter(formatter)
logger.addHandler(info_handler)
logger.addHandler(error_handler)
logger.info("basic info")
logger.error("this is an error.")
另一种写法是通过自定义模块 Filter
来实现对特定信息的筛选,例如:过滤特定级别的日志,过滤包含某特定关键词的日志,以及来自特定模块或日志记录器的日志。logging
模块中的日志过滤器需要继承 logging.Filter
类,重写 filter(record)
方法。record
是一个日志对象,包含了日志的所有信息,比如日志级别、消息和模块名等。
重写的 filter(record) 方法返回 True 时,日志会被记录。返回 False 时,日志会被忽略。
给出一个重写的日志过滤器实现有:
import logging
# 自定义过滤器
class KeywordFilter(logging.Filter):
def __init__(self, keyword, level=logging.DEBUG):
self.keyword = keyword
self.level = level
def filter(self, record):
tag1 = self.keyword in record.getMessage() # 只允许包含关键词的日志通过
tag2 = record.levelno == self.level # 只允许指定级别的日志通过
ans = tag1 || tag2
return ans
# 创建日志记录器
logger = logging.getLogger('keyword_filter_logger')
logger.setLevel(logging.DEBUG)
# 创建 Handler
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
# 添加过滤器
handler.addFilter(KeywordFilter('important')) # 只记录包含 "important" 的日志
# 添加处理器
logger.addHandler(handler)
# 记录日志
logger.info('This is a normal message') # 不会被记录
logger.info('This is an important message') # 会被记录
特别地,如果发生了保存信息的话,我们希望可以保存保存信息的发生现场(如 $\text{traceback}$)等方便之后的 debug
,此时可以设置参数 exc_info=True
即可实现:
import logging
logger = logging.getLogger("exception_logger")
logger.setLevel(logging.ERROR)
handler = logging.FileHandler("error_with_traceback.log", encoding="utf-8")
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
try:
1 / 0
except ZeroDivisionError:
logger.error("发生异常", exc_info=True)
报错信息保存为:
2025-04-06 14:32:10 - ERROR - 发生异常
Traceback (most recent call last):
File "main.py", line 11, in <module>
1 / 0
ZeroDivisionError: division by zeros
日志轮转
当日志文件过大时,可以通过文件轮转 Rotating
将日志分割成多个文件。logging
提供了两种文件轮转处理器:
RotatingFileHandler
:基于文件大小轮转。TimedRotatingFileHandler
:基于时间轮转。
基于文件大小的轮转:
import logging
from logging.handlers import RotatingFileHandler
# 创建日志记录器
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建 RotatingFileHandler
handler = RotatingFileHandler(
filename='app.log', # 日志文件名
maxBytes=10 * 1024 * 1024, # 每个日志文件最大 10MB
backupCount=5, # 保留 5 个备份文件
encoding='utf8'
)
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
# 添加处理器
logger.addHandler(handler)
# 记录日志
for i in range(10000):
logger.info(f'This is log message {i}')
基于时间的轮转:
import logging
from logging.handlers import TimedRotatingFileHandler
# 创建日志记录器
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建 TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
filename='app.log', # 日志文件名
when='midnight', # 每天午夜轮转
interval=1, # 轮转间隔
backupCount=7, # 保留 7 个备份文件
encoding='utf8'
)
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
# 添加处理器
logger.addHandler(handler)
# 记录日志
logger.info('This is a log message')