在 Python 项目开发中,日志承担着记录运行状态、追踪调用链路和辅助故障定位的重要职责。默认的输出方式通常只能满足最简单的打印需求,很难适应真实业务中对输出目标、信息格式、级别控制和文件管理的综合要求。通过内置的 logging 模块,开发者可以把日志系统拆分为日志器、处理器、格式化器和过滤器等组件,并根据项目需要进行组合,从而实现同时输出到控制台和文件、区分模块级别、定制字段格式以及按大小或时间归档等能力。

自定义日志的核心组件与工作流程
Python 的 logging 模块并不是单一函数打印日志,而是一套分层协作的机制。日志消息首先由 Logger 接收,Logger 会根据自身级别判断是否继续处理。如果消息满足级别要求,它会被传递给一个或多个 Handler。Handler 决定日志最终去往哪里,例如控制台、文件或者网络流。在真正输出前,Handler 还可以使用 Formatter 将日志记录转换成文本格式。若还配置了 Filter,则可以在级别之外进一步筛选日志。
Logger 是开发者最常接触的入口。通过 logging.getLogger 可以获取一个日志器实例。如果传入相同名称,模块会返回同一个日志器,这使得不同文件可以共享同一个日志配置。Logger 的级别相当于总开关,只有达到该级别的日志才会进入后续流程。Handler 则相当于分支开关,即使 Logger 允许 DEBUG 日志通过,控制台处理器也可以只输出 INFO 及以上内容,从而减少开发阶段的干扰信息。
Formatter 负责日志的可读性。一条日志通常不仅包含消息本身,还包含时间、级别、日志器名称、模块名和行号等上下文信息。通过合理配置这些字段,开发者可以在排查问题时快速判断日志发生的位置和严重程度。Filter 则适合更细粒度的场景,例如只记录包含特定关键词的业务日志,或者只保留某个模块的日志记录。理解这些组件的边界,是写出稳定自定义日志系统的前提。
| 组件 | 职责 | 常见配置点 |
|---|---|---|
Logger | 日志系统入口,负责接收和分发日志记录 | 名称、日志级别、挂载的处理器 |
Handler | 决定日志输出位置 | 控制台、文件、日志级别、格式化器 |
Formatter | 定义日志文本格式 | 时间、级别、模块名、行号、消息内容 |
Filter | 提供更细粒度的筛选规则 | 关键词、模块名、自定义条件 |
基础实现:创建日志器、格式化器和处理器
实现自定义日志的第一步是创建日志器并设置基础级别。通常可以把 Logger 设置为较低的级别,例如 DEBUG,让它不要过早拦截日志;再由不同 Handler 设置各自的目标级别。这样可以在同一个项目中同时满足控制台简洁输出和文件详细记录的需求。对于长期运行的服务,文件日志往往需要保留更多细节,而控制台日志则更关注关键状态。
接下来需要定义格式化器。格式化器使用占位符描述最终输出内容,例如 asctime 表示时间,levelname 表示级别,name 表示日志器名称,message 表示业务日志内容。将这些字段组织成统一格式后,日志会更容易被人工阅读,也更方便后续被日志采集系统解析。完成格式化器后,再创建控制台处理器和文件处理器,并分别绑定格式化器。
import logging
def build_logger(name="app_logger"):
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# 如果日志器已经有处理器,直接返回,避免重复添加
if logger.handlers:
return logger
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)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)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
logger = build_logger()
logger.debug("这是一条调试日志")
logger.info("这是一条运行信息日志")
logger.warning("这是一条警告日志")
logger.error("这是一条错误日志")
logger.critical("这是一条严重错误日志")
在上述代码中,build_logger 函数会先检查日志器是否已经拥有处理器。如果已经存在,则直接返回,避免重复添加。控制台处理器被设置为 INFO 级别,文件处理器被设置为 DEBUG 级别。因此,调试日志会写入文件,但不会全部打印到控制台。这样的配置在开发调试和生产观察之间取得了平衡。
如果项目包含多个业务模块,也可以为不同模块使用不同名称的日志器。例如订单模块使用 order_logger,支付模块使用 payment_logger。这样不仅可以在日志文件中快速过滤来源,也可以针对某个模块单独调整级别或输出目标,而不影响全局日志系统。
进阶能力:过滤器、滚动切割与级别控制
当日志量逐渐增加时,仅仅依靠级别控制可能不够。有些日志虽然级别较高,但并不属于当前排障关注的范围;有些低级别日志却包含关键业务信息。此时可以使用过滤器。自定义过滤器通常继承 logging.Filter,并实现 filter 方法。方法接收日志记录对象,返回 True 表示允许输出,返回 False 表示丢弃该条日志。
import logging
class KeywordFilter(logging.Filter):
def __init__(self, keyword):
super().__init__()
self.keyword = keyword
def filter(self, record):
# 只保留日志消息中包含指定关键词的记录
return self.keyword in record.getMessage()
logger = logging.getLogger("order_logger")
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
file_handler = logging.FileHandler("order.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
file_handler.addFilter(KeywordFilter("订单"))
logger.addHandler(file_handler)
logger.info("订单创建成功")
logger.info("用户访问首页")
在上述示例中,只有日志内容包含“订单”两个字时,才会写入 order.log。用户访问首页的日志虽然也是 INFO 级别,但由于不满足过滤条件,不会进入该文件。这种方式非常适合按业务维度拆分日志,也可以用于屏蔽测试环境中的特定噪声。当然,过滤器不应承担过重的业务逻辑,否则会影响日志写入性能。
文件日志还需要考虑容量管理。如果所有日志都写入同一个文件,时间一长会导致文件过大,打开和检索都会变得困难。RotatingFileHandler 可以按文件大小进行切割,当文件达到指定大小后自动重命名并创建新文件。TimedRotatingFileHandler 则适合按时间归档,例如按天生成日志文件。在生产环境中,滚动切割是保障日志可维护性的常见做法。
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("rotate_logger")
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
rotate_handler = RotatingFileHandler(
"service.log",
maxBytes=10 * 1024 * 1024,
backupCount=5,
encoding="utf-8"
)
rotate_handler.setLevel(logging.DEBUG)
rotate_handler.setFormatter(formatter)
logger.addHandler(rotate_handler)
logger.info("服务启动完成")
级别控制也需要结合环境动态考虑。开发阶段可以开启 DEBUG 日志,帮助观察函数调用和参数变化;测试阶段可以关注 WARNING 与 ERROR;生产阶段则通常以 INFO 或 WARNING 为主。对于临时排障,可以短时间降低某个模块的日志级别,而不是让整个系统长期输出大量调试信息。这样既能保留必要细节,也能控制磁盘和性能开销。
工程化封装与常见问题规避
在实际项目中,自定义日志最常见的问题之一是日志重复输出。出现这种情况的原因通常是多次获取同名日志器,并在不同地方重复调用 addHandler。由于 Logger 会把日志分发给所有已注册的 Handler,同一行日志就会被打印多次。解决方法是在添加处理器前判断 logger.handlers 是否已有内容,或者在统一初始化时清空旧处理器后再注册。
import logging
def init_logger():
logger = logging.getLogger("project_logger")
logger.setLevel(logging.DEBUG)
# 如果已经初始化过,则直接返回,避免重复添加处理器
if logger.handlers:
return logger
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
file_handler = logging.FileHandler("project.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
logger = init_logger()
logger.info("项目初始化完成")
更好的做法是把日志初始化逻辑封装到单独模块或函数中,由项目入口统一调用。业务代码只需要通过 logging.getLogger 获取日志器即可,不应关心 Handler 和 Formatter 的创建细节。这样可以避免配置分散导致的级别不一致、格式不一致和文件路径混乱。对于大型项目,还可以根据配置文件、环境变量或命令行参数决定日志级别和输出目标。
除了重复输出,日志编码和文件路径也需要提前规划。写入中文日志时,显式指定 encoding 可以减少乱码风险。日志文件最好放在统一目录中,并结合滚动策略管理历史文件。记录异常时,应保留足够的上下文,例如请求标识、用户标识、操作名称和错误堆栈,但也要避免把密码、令牌、完整银行卡号等敏感信息直接写入日志。必要时可以对敏感字段进行脱敏处理。
日志配置最好集中管理,业务模块只负责调用日志器。配置分散往往会导致日志重复、格式不一致以及文件难以维护。
总而言之,Python 自定义日志的关键并不是简单替换 print,而是围绕 logging 的组件能力建立一套可持续维护的记录体系。合理设置 Logger 和 Handler 的级别,使用 Formatter 提升可读性,通过 Filter 精确筛选日志,再配合滚动切割和统一封装,就能让日志在开发、测试和运维阶段都发挥真正价值。