2、Python日志处理基础:内置logging模块、日志级别、日志格式配置

做审计日志巡检,第一关就是搞懂日志到底是怎么写出来的。很多人一上来就写 print(),等上了生产环境才发现——日志没时间戳、没级别、没有代码位置,想查个问题比大海捞针还难。我个人习惯,不管项目大小,先把 Python 内置的 logging 模块搭好。这套东西标准库自带,不用装任何第三方包,而且灵活度足够应对 99% 的场景。

2.1 logging 模块长什么样

说白了,logging 模块就是一套“日志生产流水线”。你写代码的时候往里面丢消息,它帮你过滤、格式化、输出到文件或终端。下面这张图能让你一眼看清它的骨架。

logging 模块核心组件 Logger 日志记录器 Handler 日志处理器 Formatter 格式化器 Filter 过滤器 工作流程: 1. 你的代码调用 logger.debug/info/warning/error 发送消息 2. Logger 检查消息级别是否大于等于当前设置的最低级别 3. 通过 Filter 后,将消息交给已绑定的 Handler 4. Handler 使用 Formatter 将消息转成文本,再输出到目的地

你看,Logger 是你打交道的入口,Handler 决定日志去哪(文件、控制台、网络等),Formatter 决定日志长什么样。Filter 这东西嘛,我平时用得不算多,但遇到要按用户 IP 或者模块名过滤日志时,它特别好使。

2.2 日志级别:从 DEBUG 到 CRITICAL

为什么要有级别?我在项目中遇到过好多次:调试阶段刷了一堆 print,上线前懒得删,结果生产日志里全是无关信息。级别就是用来做“关卡”的,你设一个门槛,低于门槛的日志直接忽略。

Python 内置了 5 个标准级别,按严重程度从低到高排列:

级别 数值 典型用途 什么时候用
DEBUG 10 详细诊断信息 开发/调试模式,排查问题
INFO 20 常规运行信息 确认程序按预期运行
WARNING 30 潜在问题提醒 不影响当前功能,但值得关注
ERROR 40 严重错误,功能异常 程序无法执行某个操作
CRITICAL 50 致命错误,程序可能崩溃 整个系统或核心功能不可用

小技巧:我一直保持一个习惯——开发环境设 DEBUG,测试环境设 INFO,生产环境设 WARNING。审计日志巡检场景下,建议把 ERROR 和 CRITICAL 级别的日志单独收集到报警系统里。

级别数值是整数,10、20、30、40、50。你可以通过 logger.setLevel(logging.INFO) 来设门槛。嗯,这里要注意:门槛设定是针对 Logger 的,但 Handler 自己也可以再设一道门槛。我曾经踩过这个坑——Logger 设了 INFO,Handler 却设了 ERROR,结果 INFO 信息全丢了。记住,两条关卡是“与”的关系,只有都通过才会输出。

2.3 日志格式配置:让日志有“身份证”

原始日志长啥样?不加格式化的话,默认只有消息文本。对于审计巡检来说,这根本不够用。你需要知道:这条日志什么时候发生的?在哪个模块、哪行代码?进程号是多少?请求的 Trace ID 是什么?

Formatter 就是干这个的。你给它一个格式字符串,它帮你把各种信息填进去。我常用的格式之一:

import logging

fmt = '%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s'
formatter = logging.Formatter(fmt)

常用占位符一览:

占位符 含义 示例输出
%(asctime)s 时间戳 2025-03-18 14:23:10,456
%(levelname)s 日志级别名称 INFO / ERROR
%(name)s Logger 名称(通常用模块名) audit.parser
%(lineno)d 日志调用所在行号 42
%(message)s 你写的日志消息 用户登录失败
%(process)d 进程 PID 12345
%(thread)d 线程 ID 140736198343232

避坑指南:我曾经因为没加 %(name)s,查半天不知道日志来自哪个模块。审计日志巡检时,建议至少包含:时间、级别、模块名、行号、消息正文。如果涉及多线程,加上进程/线程 ID。

2.4 完整示例:从零配置日志系统

把上面讲的串起来,写一个实用配置。直接看代码:

import logging

# 创建 Logger
logger = logging.getLogger('audit_patrol')
logger.setLevel(logging.DEBUG)  # 门槛放最低,由 Handler 细分

# 创建控制台 Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# 创建文件 Handler(审计日志专用)
file_handler = logging.FileHandler('patrol_audit.log', encoding='utf-8')
file_handler.setLevel(logging.INFO)

# 创建格式化器
formatter = logging.Formatter(
    fmt='%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# 绑定
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# 使用
logger.info('审计巡检脚本启动完成')
logger.error('连接数据库超时,重试中...')

运行后,终端会输出 INFO 及以上级别的日志,同时所有日志(包括 DEBUG)都会写进 patrol_audit.log。为什么文件用 DEBUG?因为审计日志有时候需要记录更细的调用链路,而终端只显示关键信息。你想想看,这样是不是很灵活?

2.5 实战中容易翻车的地方

  • 重复添加 Handler:每次调用 logger.addHandler() 都会新增一个,不小心会看到日志重复输出。建议在模块初始化时只做一次。
  • 编码问题:FileHandler 默认用系统编码(Windows 可能是 gbk),写中文日志会乱码。记得传 encoding='utf-8'。
  • 日志文件轮转:单个日志文件越来越大,可以用 RotatingFileHandler 按大小或时间切割。这个后面专讲文件处理时会展开。
  • 根 Logger 的默认配置:你没配置时,logging.info() 会调用根 Logger,输出到控制台,格式简单。一旦你自定义了 Logger,根 Logger 可能会“双重输出”。解决办法:logger.propagate = False。

重点提醒:审计日志巡检最怕日志丢、日志乱、日志漏。上面提到的每个细节都可能导致你排查问题时抓瞎。我建议你写完配置后,专门做一轮“日志完整性测试”——手动制造一条 ERROR 日志,确认它出现在正确的位置,带上完整的上下文信息。

好了,掌握这些,日志处理的基础你就基本拿下了。后面章节我们会把它用到真正的审计巡检场景里,比如如何结构化日志、如何按时间切片、如何从海量日志里快速定位异常。一步步来,不急。