程序出了问题时,一句“执行失败”通常不够。我们还需要知道何时发生、来自哪个模块、严重程度如何,以及对应的异常。日志格式的作用,就是把这些上下文稳定地放在同一条记录里。
一个清晰的起点
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | "
"%(filename)s:%(lineno)d | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
def load_documents():
logger.info("开始读取资料,文件数=%d", 3)
load_documents()
输出包含时间、等级、记录器名、源文件与行号、消息。实际时间和行号随运行环境而变化,不应把示例输出中的固定行号当成程序保证。
常用字段
| 字段 | 意义 |
|---|---|
%(asctime)s |
格式化时间 |
%(levelname)s |
INFO、WARNING、ERROR 等等级名称 |
%(name)s |
记录器名称 |
%(message)s |
完成参数替换后的消息 |
%(filename)s |
发出日志的源文件名 |
%(pathname)s |
源文件完整路径 |
%(module)s |
源模块名 |
%(funcName)s |
发出日志的函数名 |
%(lineno)d |
源代码行号 |
%(process)d |
进程 ID |
%(thread)d |
线程 ID |
这些字段来自 LogRecord;格式中的 %s 与 %d
分别表示字符串与整数形式。Python logging
文档
不是字段越多越好。单进程小脚本通常不必为每条日志增加进程和线程
ID;多服务请求排查时,能够关联请求的标识往往更有价值。
basicConfig 为什么有时不生效
如果根记录器已经配置了处理器,basicConfig()
通常不会再次配置。因此在
Notebook、测试框架或其他库先配置日志之后,重复调用可能看不到变化。
应用一般在入口统一配置,各模块使用
logging.getLogger(__name__)
获取记录器。不要在每个模块里重复初始化全局日志。
force=True
可以重配根记录器,但会移除已有处理器。只有当应用明确拥有日志配置权时才应使用,库代码不应擅自覆盖宿主配置。
错误消息与异常堆栈
try:
int("not-a-number")
except ValueError:
logger.exception("配置中的数量字段无法解析")
logger.exception
应在异常处理块中使用,它会同时记录异常堆栈。相比只记录
str(exc),堆栈更容易定位调用路径。
日常消息可以写成 logger.info("文件数=%d", count),让
logging
在需要输出时完成字符串格式化。若参数本身由昂贵函数计算,这种写法并不会省掉函数求值,仍需另行判断日志等级。
让日志服务于排查
RAG
流程可以记录阶段、耗时、召回条数、知识库版本和请求标识。默认不必把完整用户问题、检索原文或凭据写进日志;记录摘要信息通常就足以判断处理链路是否异常。
格式解决“怎样读”,日志等级与采样解决“输出多少”,文件轮转和保留策略解决“保存多久”。三者配合,日志才不会从排障工具变成新的负担。