推荐一个第三方好用的日志库:https://github.com/Delgan/loguru
pip install loguru
from loguru import logger
一、日志的重要性
在日常开发和维护软件时,日志记录是一个非常重要的环节。它帮助我们追踪程序执行流程、诊断错误、监控系统状态、记录系统行为,是调试代码、运维和分析用户行为的关键。
Python的logging模块提供了一个灵活且强大的日志记录功能,能够满足各种日志记录需求。本文将详细介绍logging模块的使用,包括基本概念、配置方法、日志级别、日志处理器和格式化器的使用,以及实际应用案例。
二、logging模块介绍
logging模块是Python内置的日志模块,支持分级输出、文件持久化、格式化、多模块复用,输出到不同的目标(如控制台、文件、网络等)等核心功能,是生产环境必备的日志工具
三、logging的优势
1、日志级别(Levels):支持 DEBUG, INFO, WARNING, ERROR, CRITICAL 等级别,可以方便地控制不同环境(开发、测试、生产)输出的信息量。
2、灵活的输出目标(Handlers):日志可以同时输出到控制台、文件、网络、邮件、数据库等,而 print 只能输出到标准输出。
3、丰富的格式化(Formatters):可以自定义日志的输出格式,包含时间、模块名、行号、日志级别等信息。
4、持久化:日志写入文件/数据库,不会随程序结束消失
5、性能:在生产环境中,可以将日志级别设为 WARNING,此时 debug() 调用几乎不消耗性能(因为消息不会被格式化),而 print 语句即使注释掉也需要手动操作。
5、结构化与可维护性:logging 提供了清晰的架构,易于配置和管理,适合大型项目。
四、日志级别
级别设置采用白名单机制:Logger 和 Handler 设置级别后,只有大于或等于该级别的日志才会被处理。
默认情况下,logging输出warning及以上级别的日志
优先级:DEBUG < INFO < WARNING < ERROR < CRITICAL
| 级别 | 数值 | 使用场景 |
|---|---|---|
| NOTSET | 0 | 用于表示日志器或处理器的级别尚未被显式设置。此时其有效级别会从父日志器继承。 |
| DEBUG | 10 | 开发调试细节,如变量值、中间结果(生产环境通常关闭) |
| INFO | 20 | 程序关键节点记录,如服务启动、配置加载、重要业务操作 |
| WARNING | 30 | 预期内可能发生的异常情况,需要关注但未影响运行 |
| ERROR | 40 | 需要立即处理的错误,如数据库连接失败 |
| CRITICAL | 50 | 致命错误导致程序无法继续运行,如内存耗尽 |
五、logging模块的核心组件
组件协作流程:
Logger → Filter → Handler → Formatter → 输出目标
5.1、Logger 日志记录器
Logger负责生产日志消息
logging 是单例模式,全局共用一个配置,无需重复定义
如果不同模块需要单独记录日志,可为不同模块创建不同的Logger。
# 语法:
logging.getLogger(name)
# 关键属性:
* name: 一般为"__name__,即模块的全名"
* level: 只有级别大于或等于此级别的日志记录才会被处理
- 若Logger没有设置级别,则会向上传递到父Logger(最终到根Logger)
# 常用方法:
debug()
info()
warning()
error()
critical()
5.1.1、层级结构
Logger 通过 logging.getLogger(name) 获取,多次调用同一 name 返回同一个实例。名字以点号分隔,形成层级结构。
1. Logger名称=层级路径
2. 名称用"."分隔,每一段代表一级子节点
3. 根Logger: 名称为空字符串'',是所有Logger的顶层父节点。
4. 日志传播(propagate): 默认开启,子Logger的日志会向上传递给所有父Logger处理
5. 同名 getLogger() 返回同一个对象,全局单例,不会重复创建
# 父子判断规则
A.B 的父Logger是A
A.B.C的父Logger是A.B,祖父是A
# 树形结构举例
root (名称: "")
├─ app (名称: "app")
│ ├─ app.api (名称: "app.api")
│ │ └─ app.api.user (名称: "app.api.user")
│ └─ app.db (名称: "app.db")
└─ utils (名称: "utils")
└─ utils.log (名称: "utils.log")
# 根 logger
root_logger = logging.getLogger()
# 一级 logger
app_log = logging.getLogger("app")
utils_log = logging.getLogger("utils")
# 二级 logger
app_api_log = logging.getLogger("app.api")
app_db_log = logging.getLogger("app.db")
# 三级 logger
app_api_user_log = logging.getLogger("app.api.user")
5.1.2、日志传播途径
不想子模块日志重复打印、不想底层日志污染全局日志,就关闭 propagate
5.1.2.1、propagate = True(默认开启传播)
传递流程:
1、日志在当前 Logger 先判断日志级别,不满足直接丢弃
2、日志级别通过, 交给当前 Logger 的 Handler 输出
3、再向上传递给父 Logger,判断级别,执行自身 Handler
4、一直传递到 root Logger 为止
import logging
# 1. 统一设置root的Handler
root = logging.getLogger()
root.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
root.addHandler(handler)
# 2. 创建子logger
child = logging.getLogger("app.api")
child.setLevel(logging.INFO)
# 输出日志
child.info("测试消息")
"""
# 输出: app.api: 测试消息
1. app.api级别INFO,日志放行
2. 子logger无Handler,所以不输出
3. 日志向上传播到root,root有Handler,最终输出。
"""
5.1.2.2、propagate = False(关闭传播)
日志停在当前模块,停止向上传递,再判断日志级别,若有handle,则在当前模块输出,若无handle,则停止输出。
child.propagate = False
5.2、Filter 过滤器
提供更精细的日志过滤机制,可以基于 Logger 名称、日志级别或其他属性进行过滤
5.3、Handler 日志处理器
logger本身不负责直接输出日志,而是将LogRecord对象传递给一个或多个Handler处理器,由 Handler 完成实际输出。一个 logger可以绑定多个 handler,实现 "一个日志,多个出口"。
- 关键属性:
- Level:Handler 有自己的级别。
即使 Logger 接受了一条日志,Handler 也会根据自己的级别再次过滤。
例如,Logger 级别是 DEBUG,Handler 级别是 INFO,那么 DEBUG 级别的日志不会被这个 Handler 处理 - Formatter:关联一个 Formatter 来格式化日志消息。
- Level:Handler 有自己的级别。
| 处理器 | 说明 | 关键参数 |
|---|---|---|
| StreamHandler | 输出到控制台(sys.stdout / sys.stderr) | stream |
| FileHandler | 输出到文件 | filename, mode, encoding |
| RotatingFileHandler | 输出到文件,并在文件达到一定大小时切割 | maxBytes, backupCount |
| TimedRotatingFileHandler | 按时间间隔切割日志文件 | when, interval, backupCount |
| HTTPHandler | 通过HTTP将日志发送到服务器 | |
| SysLogHandler | 将日志发送到syslog服务器 | |
| SMTPHandler | 通过邮件发送日志 | mailhost, fromaddr, toaddrs |
| SocketHandler / DatagramHandler | 通过网络发送日志 | |
| NullHandler | 空处理器,用于避免无 handler 时的警告 |
- 按文件大小轮转(RotatingFileHandler)
当 app.log 达到 10 MB 时,自动重命名为 app.log.1,新建 app.log,旧文件依次递增编号,超出备份数的被删除
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
'app.log',
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5 # 保留 app.log, app.log.1, ..., app.log.5
)
- 按时间轮转(TimedRotatingFileHandler)
| when参数 | 含义 |
|---|---|
| ‘S’ | 秒 |
| ‘M’ | 分钟 |
| ‘H’ | 小时 |
| ‘D’ | 天 |
| ‘midnight’ | 每天0点轮转 |
| ‘W0’-‘W6’ | 指定星期几轮转 |
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
'app.log',
when='midnight', # 每天凌晨轮转
interval=1,
backupCount=30 # 保留 30 天的备份
)
5.4、Formatter 格式化器
5.4.1、常用Formatter占位符
为不同的 Handler 设置不同的 Formatter。
例如,文件日志包含详细信息(时间、行号),而控制台日志只显示简洁信息
| 占位符 | 含义 |
|---|---|
| %(asctime)s | 日志创建时间,默认格式为 YYYY-MM-DD HH:MM:SS,mmm(毫秒)(可通过 datefmt 自定义格式) |
| %(name)s | Logger 名称 |
| %(levelname)s | 日志级别名称 |
| %(message)s | 日志消息内容 |
| %(filename)s | 生成日志的文件名 |
| %(lineno)d | 调用日志的源代码行号 |
| %(funcName)s | 调用日志的函数名 |
| %(pathname)s | 源文件完整路径 |
| %(module)s | 生成模块名(文件名去掉 .py) |
| %(process)d | 进程 ID |
| %(thread)d | 线程 ID |
| %(processName)s | 当前进程名称 |
| %(threadName)s | 当前线程名称 |
| %(stack_info)s | 堆栈信息(如果提供了 stack_info=True) |
- format 格式模板
# 1.简洁风格:
"%(asctime)s - %(levelname)s - %(message)s"
# 2.包含模块和行号,适合调试用:
"%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"
"[%(asctime)s] %(filename)s %(funcName)s line:%(lineno)d [%(levelname)s] %(message)s"
# 3.适合生产环境的详细日志格式:
"%(asctime)s | %(levelname)s | %(name)s | %(process)d | %(threadName)s | %(message)s"
# 4.和 Django 默认格式类似:
"%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"
六、使用案例
6.1、简单使用示例(basicConfig)
适合小型脚本的快速配置
basicConfig中filename和handlers不能同时指定
源码中逻辑:
1、当不指定 handles 时,如果指定filename,则自动创建FileHandler,将日志输出到文件
2、当不指定 handles 时,如果不指定filename,则自动创建StreamHandler,将日志输出到控制台
3、指定 handles ,则根据配置的处理器来处理日志
4、输出到文件时,使用encoding指定编码
import logging
# 不指定handlers
# 将日志存储到单个文件
logging.basicConfig(
filename='app.log', # 可有可无,具体根据日志如何处理来确定
level=logging.DEBUG,
# level=10, # 级别的另一种写法
format='%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
encoding='GBK'
)
# 指定handlers
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
handlers=[
logging.FileHandler('app.log'),
logging.StreamHandler()
],
encoding='GBK'
)
def run():
logging.debug('这是调试日志')
logging.info('这是信息日志')
logging.warning('这是警告日志')
logging.error('这是错误日志')
logging.critical('这是严重错误日志')
run()
6.2、自定义logger
import logging
# 1. 获取一个 Logger 实例
logger = logging.getLogger(__name__)
# 2. 设置 Logger 级别
logger.setLevel(logging.INFO)
# 3. 创建输出到控制台 Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
# 4. 创建输出到文件 Handler
"""只有FileHandler有encoding属性"""
file_handler = logging.FileHandler("app.log", encoding="utf-8") # 指定FileHandler编码
file_handler.setLevel(logging.DEBUG) # 设置handler级别
file_handler.encoding = "utf-8" # 第二种:指定FileHandler编码
# 5. 创建Formatter
fmt = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
formatter = logging.Formatter(fmt)
# 6. handle 绑定 formatter
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# 7. 将Handler 添加到 logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# 8. 测试输出
logger.debug("调试信息")
logger.info("普通信息")
logger.warning("警告信息")
logger.error("错误信息")
logger.critical("严重错误")
七、高级用法与最佳实践
1、只配置 root Logger:统一级别、统一 Handler、统一格式
2、业务模块按包 / 模块名创建子 Logger,只单独设置日志级别
3、子 Logger 不额外加 Handler,依靠传播到 root 输出
4、敏感 / 独立模块手动关闭 propagate
7.1、分模块记录日志
理论上 logger 是单例的,全局唯一。但对于较大的爬虫系统(如 Scrapy、分布式爬虫),可以对不同模块(抓取、解析、存储、调度等)使用不同的 logger 进行分类管理。这样可以只查看某一类日志,如只分析解析失败的日志
fetch_logger = logging.getLogger("fetcher")
parse_logger = logging.getLogger("parser")
save_logger = logging.getLogger("saver")
7.2、全局日志工具模块
所有模块共用一套日志配置
日志自动区分来源
日志自动切割
控制台干净,文件完整
# log_writer.py
# 单独控制日志级别,日志里自动显示来源模块
# -*- coding: utf-8 -*-
import logging
import os
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
current_path = os.path.dirname(os.path.abspath(__file__))
logs_path = os.path.join(current_path, "logs")
def get_logger(name: str = __name__, log_path: str = logs_path) -> logging.Logger| None:
"""
生产环境标准日志工具
:param name: 日志器名称(建议传 __name__)
:param log_path: 日志存放目录
:return: logger 实例
"""
# 1. 创建日志目录
if not os.path.exists(log_path):
os.makedirs(log_path)
# 2. 创建 logger
logger = logging.getLogger(name)
# 防止重复添加 Handler
if logger.handlers:
return None
logger.setLevel(logging.DEBUG) # 全局最低级别
logger.handlers.clear() # 清空历史 handler,防止重复
logger.propagate = False # 禁止向上传递,避免重复
# 3. 定义日志格式
fmt = (
"%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s"
)
formatter = logging.Formatter(fmt)
# ===================== Handler 1:控制台输出 =====================
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) # 控制台只看 INFO+
console_handler.setFormatter(formatter)
# ===================== Handler 2:按文件大小切割 =====================
file_size_handler = RotatingFileHandler(
filename=os.path.join(log_path, "app_size.log"),
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5, # 最多保留 5 个文件
encoding="utf-8",
)
file_size_handler.setLevel(logging.DEBUG) # 文件记录所有级别
file_size_handler.setFormatter(formatter)
# ===================== Handler 3:按时间切割(推荐生产用这个) =====================
file_time_handler = TimedRotatingFileHandler(
filename=os.path.join(log_path, "app_time.log"),
when="midnight", # 每天凌晨切割
interval=1,
backupCount=30, # 保留 30 天
encoding="utf-8",
)
file_time_handler.setLevel(logging.DEBUG)
file_time_handler.setFormatter(formatter)
# ===================== 挂载 Handler =====================
logger.addHandler(console_handler)
logger.addHandler(file_size_handler)
logger.addHandler(file_time_handler)
return logger
if __name__ == "__main__":
logger = get_logger()
if logger is not None:
logger.debug("这是调试日志(仅文件可见)")
logger.info("服务启动成功")
logger.error("数据库连接失败")
# --------------- 各个业务模块 ---------------
# 模块1
from log_writer import get_logger
# logger_api = get_logger("myapp.api.user")
logger_api = get_logger(__name__)
logger_api.setLevel(logging.INFO)
# 模块2
from log_writer import get_logger
# logger_db = get_logger("myapp.db.connect")
logger_db = get_logger(__name__)
logger_db.setLevel(logging.DEBUG)
7.3、配置分离:基于 dictConfig 的日志配置
logging.config.dictConfig(config) 是 Python 官方推荐的日志配置方式,它将一个字典结构的配置转化为实际的 Logger、Handler、Formatter、Filter 对象,并正确建立它们之间的引用关系。
dictConfig必须按照 引用依赖顺序 来创建对象:
formatters → filters → handlers → loggers → root。每个步骤中构建的对象会暂存于内部字典,供后续步骤通过名称引用
使用dictConfig配置日志时,注意:
1、handles中每个handle的名字是自定义的,根据需要输出的日志类型配置
2、loggers中每个自定义logger的键名与各模块调用时传入的logger名称一致。若一致,则直接使用该配置项下定义的 handlers、level、propagate;若不一致,则调用时传入的名称在自定义logger的名称中找不到,则向上找到父,一直找到root
3、再根据handler中的日志级别进行过滤,输出大于或等于handler中配置的LEVEL的日志
4、为了通用,logger的键名配置顶级包的名称,包内的模块调用时传入__name__,包内的模块都是包的子类,都能向上找到这个键名的配置。
# logger_dictConfig.py
import os
import logging
from logging.config import dictConfig
current_path = os.path.dirname(os.path.abspath(__file__))
logs_path = os.path.join(current_path, "logs")
LOGGING_CONFIG = {
'version': 1, # 当前只支持 version: 1。如果版本不匹配或缺失,抛出 ValueError
'disable_existing_loggers': False, # 避免禁用第三方库的 Logger
'formatters': {
'detailed': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'
},
'simple': {
'format': '%(asctime)s - %(levelname)s - %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'formatter': 'simple',
'stream': 'ext://sys.stdout'
},
'file_rotating': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'INFO',
'formatter': 'detailed',
'filename': f"{logs_path}/app.log",
'maxBytes': 10 * 1024 * 1024, # 10 MB
'backupCount': 5, # 保留 5 个备份
'encoding': 'utf-8'
},
'file_error': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'ERROR', # 大于或等于ERROR级别的日志会输出
'formatter': 'detailed',
'filename': f'{logs_path}/error.log',
'maxBytes': 10 * 1024 * 1024,
'backupCount': 5,
'encoding': 'utf-8'
}
},
'loggers': {
'': { # root logger
'handlers': ['console', 'file_rotating', 'file_error'],
'level': 'INFO',
'propagate': True
},
'logging_module': { # 配置顶级包的名称
'handlers': ['console', 'file_rotating'],
'level': 'DEBUG',
'propagate': False
},
'database': {
'handlers': ['file_error'],
'level': 'WARNING',
'propagate': False
}
}
}
def setup_logging(name: str=__name__):
dictConfig(LOGGING_CONFIG) # 检查传入的是否为字典,否则抛出 TypeError
logger = logging.getLogger(name)
return logger
在其他模块中使用:
"""
1. setup_logging("logging_module"),则完全匹配配置中的'logging_module'项
2. setup_logging("logging_module.log_test")
logging会查找祖先:'logging_module.log_test' → 父为 'logging_module'
由于 'logging_module' 在配置中存在,但请注意:子 logger 并不会自动获得父 logger 的 handlers。
子 logger 自己没有 handlers 和 level 设置时,它的 level 会继承父 logger 的 level(如果父 logger 有设置),
但它的 handlers 列表是空的。然后 propagate 默认为 True,
所以子 logger 产生的日志会传播给父 logger,由父 logger 的 handlers 处理。
因此 'logging_module.log_test' 产生的日志最终也由 'logging_module' 的 handlers 输出,
并且级别也是 DEBUG(因为继承了父的 level)
3. setup_logging("myrandom"),则没有完全匹配
'myrandom' → 父为 ''(根)。
根 logger 有配置:handlers ['console', 'file_rotating', 'file_error'],level INFO。
子logger自身没有level,会继承根的level(INFO)。没有自己的 handlers,但 propagate=True(默认),
所以日志传播给根 logger,由根 logger 的 handlers 处理。
"""
# logging_module/log_test.py
from logger_dictConfig import setup_logging
logger = setup_logging(__name__) # logger名称是 logging_module.log_test
def run():
logger1.info("我是log_test消息")
run()
# database.py
from logger_dictConfig import setup_logging
logger = setup_logging("database")
def run():
logger1.info("我是database消息")
run()
7.4、单例模式创建日志类
- 项目级别的日志文件存储
# logger_writer_singleton.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import logging
import datetime
import os
import tarfile
from logging.handlers import RotatingFileHandler
formatter = logging.Formatter('[%(asctime)s] %(filename)s->%(funcName)s line:%(lineno)d [%(levelname)s] %(message)s')
current_path = os.path.dirname(os.path.abspath(__file__))
log_path = os.path.join(current_path, "logs")
error_loggers = []
class LoggerWriter(object):
__instance = None
# 重写new方法实现单例模式
def __new__(cls, *args, **kwargs):
if not cls.__instance:
cls.__instance = object.__new__(cls)
return cls.__instance
def __init__(self, logger=None):
# 如果之前没创建过 就新建一个logger对象,后面相同的实例共用一个logger对象
if 'logger' not in self.__dict__:
self.logger = logging.getLogger(logger)
self.logger.setLevel(logging.INFO)
self.log_path = log_path
self.log_name = os.path.join(self.log_path, 'runtime.log')
self.crt_log_name = os.path.join(self.log_path, 'critical.log')
# self.log_name = self.log_path + 'runtime.log' # logsruntime.log
# self.crt_log_name = self.log_path + 'critical.log' #logscritical.log
# 创建两个个handler,用于写入日志文件
fh = logging.FileHandler(self.log_name, mode='a', encoding='utf-8')
fh.setLevel(logging.INFO)
ch = logging.FileHandler(self.crt_log_name, mode='a', encoding='utf-8')
ch.setLevel(logging.CRITICAL)
# 使用RotatingFileHandler进行日志轮转
ch_rotate = RotatingFileHandler(
self.crt_log_name,
maxBytes=1024 * 1024 * 10,
backupCount=5,
encoding='utf-8'
)
# 给handler绑定日志输出格式
fh.setFormatter(formatter)
ch.setFormatter(formatter)
ch_rotate.setFormatter(formatter)
# 给logger添加handler
self.logger.addHandler(fh)
self.logger.addHandler(ch)
self.logger.addHandler(ch_rotate)
# 关闭打开的文件
fh.close()
ch.close()
ch_rotate.close()
# 判断日志文件大小
log_size = os.path.getsize(self.log_name) / (1024 * 1024)
# 要压缩的日志文件名格式
dst_file = log_path + 'runtime_{}.tar.gz'.format(datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
if log_size > 10:
with tarfile.open(dst_file, 'w:gz') as tar:
# 将源文件添加到压缩文件中
tar.add(self.log_name)
os.remove(self.log_name)
def get_logger(self):
return self.logger
def log_command_output(self, command, message):
is_success = True
self.logger.info(message)
for d in error_loggers:
for kw in d['keywords'].keys():
error_count = message.count(kw)
if error_count > 0:
d['logger'].warn("Output of %s contains %s:\n%s", command, d['name'], message)
if error_count > d['keywords'][kw]:
is_success = False
d['logger'].error("Result of %s is marked as failed due to %s count %s > %s",
command, d['name'], error_count, d['keywords'][kw])
return is_success
# 单独创建单个的日志对象
def create_logger(name, error_keywords=None):
logger = logging.getLogger(name)
mode = 'w'
if isinstance(error_keywords, dict):
error_loggers.append({"name": name, "logger": logger, "keywords": error_keywords})
mode = 'a'
fh = logging.FileHandler(log_path + name + '.log', mode)
fh.setFormatter(formatter)
logger.addHandler(fh)
fh.close()
return logger
# pyadvanced.myapp.demo.log_test.py
from logger_writer_singleton import LoggerWriter
log = LoggerWriter().get_logger()
def query_command(rs_index, cs_index, ic_index):
log.info("query data, rs_index:{}, cs_index:{}, ic_index:{}".format(rs_index, cs_index, ic_index))
# test_clear_data.call_log.py
from pyadvanced.myapp.demo import log_test
log_test.query_csu_undo_command(1, 1, 1)
7.5、使用filter过滤日志
class SensitiveInfoFilter(logging.Filter):
"""过滤包含敏感信息的日志记录"""
def __init__(self, keywords=None):
super().__init__()
self.keywords = keywords or ['password', 'token', 'secret', 'credit_card']
def filter(self, record):
# 过滤包含敏感关键词的日志
msg_lower = record.getMessage().lower()
for keyword in self.keywords:
if keyword in msg_lower:
return False
return True
# 使用方法
logger = logging.getLogger(__name__)
sensitive_filter = SensitiveInfoFilter()
logger.addFilter(sensitive_filter)
# 同时可以基于日志级别进行筛选
class LevelRangeFilter(logging.Filter):
def __init__(self, min_level, max_level):
super().__init__()
self.min_level = min_level
self.max_level = max_level
def filter(self, record):
return self.min_level <= record.levelno <= self.max_level
# 只记录 WARNING 到 ERROR 级别的日志
warning_filter = LevelRangeFilter(logging.WARNING, logging.ERROR)
八、常见问题
8.1、解决日志重复打印
原因:多次 addHandler 或 propagate =True向上传递
1、只给root加 Handler,所有子Logger不加
2、子Logger 加完 Handler 后,加 logger.handlers.clear()和app.propagate = False
3、统一封装日志配置类,全局只初始化一次 Handler
8.2、日志不输出
原因:级别太高
解决:logger.setLevel(logging.DEBUG)
8.3、中文乱码
解决:handler 加 encoding=“utf-8”
8.4、日志文件分割
FileHandler 会无限增长文件。使用 RotatingFileHandler 或 TimedRotatingFileHandler 可以自动管理日志文件大小和数量
8.5、多模块日志混乱
解决:统一使用一个日志函数/类
8.6、捕获异常信息
try:
data = response.json()
except Exception as e:
logger.error(f"解析 JSON 失败,url={url}", exc_info=True) # exc_info=True 会自动记录 traceback
# 或者使用
# logger.exception("发生异常") # 等价于 logger.error(..., exc_info=True)
8.7、在多模块项目中使用 name
各模块互相调用,生成的日志中会记录日志产生的来源。
日志中能清晰看到日志来源模块,且 Logger 层级自动与包结构对应,有助于区分不同模块的日志
# myapp/utils.py
import logging
logger = logging.getLogger(__name__) # __name__ 是 'myapp.utils'
def some_function():
logger.info("在 utils 模块中执行")
# 在其他模块中调用
from myapp.utils import some_function
some_function()
# 2026-06-13 15:07:14,886 - myapp.utils - INFO - utils.py:8 - 在utils模块中执行
九、日志流
下图是官方日志流转图:

继承关系及信息传递:
- 每一个 Logger 实例的 level 如同入口,让水流进来,如果这个门槛太高,信息就进不来。例如 log3.warning(‘log3’),如果 log3 定义的级别高,就不会有信息通过 log3
- 如果 level 没有设置,就用父 logger 的,如果父 logger 的 level 没有设置,继续找父的父的,最终可以找到 root 上,如果 root 设置了就用它的,如果 root 没有设置,
则使用root的默认值WARNING - 消息传递流程
- 如果消息在某一个 logger 对象上产生,这个 logger 就是当前 logger,首先消息 level 要和当前 logger 的 EffectiveLevel 比较,如果低于当前 logger 的 EffectiveLevel,则流程结束;否则生成 log 记录
- 日志记录会交给当前 logger 的所有 handler 处理,记录还要和每一个 handler 的级别分别比较,低的不处理,否则按照 handler 输出日志记录
- 当前 logger 的所有 handler 处理完后,就要看自己的 propagate 属性,如果是 True 表示向父 logger 传递这个日志记录,否则到此流程结束
- 如果日志记录传递到了父 logger,不需要和父 logger 的 level 比较,而是直接交给父的所有 handler,父 logger 成为当前 logger。重复2、3步骤,直到当前 logger 的父 logger 是 None 退出,也就是说当前 logger 最后一般是 root logger(是否能到 root logger 要看中间的 logger 是否允许 propagate)
- logger 实例初始的 propagate 属性为 True,即允许向父 logger 传递消息
- logging.basicConfig() 函数,如果 root 没有 handler,就默认创建一个 StreamHandler,如果设置了 filename,就创建一个 FileHandler。如果设置了 format 参数,就会用它生成一个 Formatter 对象,否则会生成缺省 Formatter,并把这个 formatter 加入到刚才创建的 handler 上,然后把这些 handler 加入到 root.handlers 列表上。level 是设置给 root logger 的。如果 root.handlers 列表不为空,logging.basicConfig 的调用什么都不做。
十、日志模块总结
在深入学习并阅读了 logging 模块的源码之后,我们会发现:整个日志系统的设计其实非常清晰 —— 模块化的组件组合(Logger、Handler、Formatter)加上可配置化的等级和输出方式,逻辑非常清楚,上手也并不复杂。但在真实项目中,日志系统真正的挑战不在于 “如何使用 logging”,而在于 “日志应该写在哪里,写多少,写什么”。
这部分并没有标准答案,它是依赖于经验、项目规模、团队协作模式和后期分析工具的。以下是一些实际工作中常见的思考与经验总结:
| 场景 | 应该写日志的位置 |
|---|---|
| 关键业务流程 | 例如:用户下单、支付、扣库存、发货等,建议打 INFO 日志记录业务链路状态 |
| 异常捕获 | 在 try…except 中用 logger.exception() 记录异常栈 |
| 性能瓶颈点 | 比如:数据库慢查询、接口超时、调用外部 API 的耗时,建议使用 WARNING 或 INFO 并记录耗时数据 |
| 调试分支 | 某些重要但不常触发的代码分支,用 DEBUG 打印关键变量值 |
| 用户输入与验证失败 | 用户输入数据异常、验证失败、权限拒绝等,可用 WARNING 等级记录 |
| 第三方服务调用失败 | 例如:请求微信支付、发短信失败等,要及时打日志,方便运维排查 |
如何写出 “对未来有用” 的日志?
- 上下文清晰:日志中要包含发生了什么,在哪儿发生的,哪些参数,结果如何;
- 结构化内容:即便不使用 JSON,日志内容也要方便后续正则匹配、搜索;
- 避免日志泛滥:不要什么都打印,会掩盖重点(特别是在循环、频繁调用中);
- 区分等级与模块:合理使用
DEBUG/INFO/WARNING/ERROR/CRITICAL,并为每个模块设置不同 logger,有助于日志隔离; - 提前考虑分析方式:日志最终可能用于搜索、告警、监控、审计,所以写日志时可以站在 “
未来使用者” 的角度思考。
17万+

被折叠的 条评论
为什么被折叠?



