python3从入门到精通(十五): logging模块

推荐一个第三方好用的日志库: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

级别数值使用场景
NOTSET0用于表示日志器或处理器的级别尚未被显式设置。此时其有效级别会从父日志器继承。
DEBUG10开发调试细节,如变量值、中间结果(生产环境通常关闭)
INFO20程序关键节点记录,如服务启动、配置加载、重要业务操作
WARNING30预期内可能发生的异常情况,需要关注但未影响运行
ERROR40需要立即处理的错误,如数据库连接失败
CRITICAL50致命错误导致程序无法继续运行,如内存耗尽

五、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 来格式化日志消息。
处理器说明关键参数
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)sLogger 名称
%(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模块中执行

九、日志流

下图是官方日志流转图:
在这里插入图片描述
继承关系及信息传递:

  1. 每一个 Logger 实例的 level 如同入口,让水流进来,如果这个门槛太高,信息就进不来。例如 log3.warning(‘log3’),如果 log3 定义的级别高,就不会有信息通过 log3
  2. 如果 level 没有设置,就用父 logger 的,如果父 logger 的 level 没有设置,继续找父的父的,最终可以找到 root 上,如果 root 设置了就用它的,如果 root 没有设置,则使用root的默认值WARNING
  3. 消息传递流程
    • 如果消息在某一个 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)
  4. logger 实例初始的 propagate 属性为 True,即允许向父 logger 传递消息
  5. 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 等级记录
第三方服务调用失败例如:请求微信支付、发短信失败等,要及时打日志,方便运维排查

如何写出 “对未来有用” 的日志?

  1. 上下文清晰:日志中要包含发生了什么,在哪儿发生的,哪些参数,结果如何;
  2. 结构化内容:即便不使用 JSON,日志内容也要方便后续正则匹配、搜索;
  3. 避免日志泛滥:不要什么都打印,会掩盖重点(特别是在循环、频繁调用中);
  4. 区分等级与模块:合理使用DEBUG/INFO/WARNING/ERROR/CRITICAL,并为每个模块设置不同 logger,有助于日志隔离;
  5. 提前考虑分析方式:日志最终可能用于搜索、告警、监控、审计,所以写日志时可以站在 “未来使用者” 的角度思考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一位不知名民工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值