Python 的默认日志与 logging 模块
Python 的默认日志与 logging 模块
一、问题
1.1 由一道日志题引发的学习
1. Python 默认日志有什么缺陷?
2. logging 模块在 AI 服务端生产日志中应该如何分级配置?
原参考答案:
默认缺陷:日志无链路 ID、无时间戳格式化、文件不切割、无法区分业务/模型/接口日志、线上故障无法溯源。
生产分级配置:DEBUG:本地开发,打印模型入参、向量召回详情;INFO:生产默认级别,记录接口调用、知识库更新、会话创建;WARNING:模型调用超时、检索召回量不足、内存水位偏高;ERROR:接口崩溃、向量库连接中断、模型推理失败,绑定链路 ID 告警。
这个问题看起来是在问“日志级别怎么用”,但背后其实牵涉到服务端开发中非常核心的一组能力:
print()、默认日志、logging模块之间的差异- 日志级别的设计原则
Logger、Handler、Formatter、Filter的职责- 日志格式、日志文件、日志切割
- 异常堆栈记录
- 请求链路 ID / request_id / trace_id
- 模块化日志与不同业务日志分流
- AI / RAG 服务中的模型调用日志、向量检索日志、接口日志
- 结构化日志与线上可观测性
- 日志安全:敏感信息脱敏、避免泄露 token 和隐私数据
因此,本文不只是为了回答这道面试题,而是以它为入口,系统梳理 Python logging 模块从基础使用到生产实践的完整知识。
1.2 简要回答
Python 中最简单的“默认日志”通常指两类:
- 使用
print()直接输出信息; - 直接调用
logging.warning()、logging.error()等模块级函数,而没有做完整日志配置。
它们在小脚本中够用,但在生产服务中问题很多:
| 问题 | 说明 |
|---|---|
| 缺少统一格式 | 不一定有时间、模块名、日志级别、进程线程信息 |
| 默认级别较高 | 默认 root logger 级别是 WARNING,DEBUG、INFO 默认不会输出 |
| 难以定位请求 | 没有 request_id / trace_id,无法串起一次请求的完整链路 |
| 不适合文件管理 | 默认不负责日志文件切割、归档、保留策略 |
| 不区分业务模块 | 接口、模型、向量库、数据库日志混在一起,不利于检索 |
| 异常信息可能不完整 | 如果只打印错误消息而不打印堆栈,线上很难定位根因 |
| 不便于接入告警 | 无结构化字段,不利于 ELK、Loki、云日志平台、APM 分析 |
生产环境中应该使用 logging 建立统一日志体系。AI 服务端常见分级可以这样设计:
| 级别 | 使用场景 | AI 服务端示例 |
|---|---|---|
DEBUG |
开发调试、详细内部状态 | 模型入参摘要、prompt 片段、向量召回详情、SQL 参数 |
INFO |
正常业务事件 | 接口调用、会话创建、知识库更新、模型调用成功、任务完成 |
WARNING |
可恢复但需要关注的问题 | 模型调用超时后重试、召回数量不足、内存水位偏高、限流接近阈值 |
ERROR |
当前请求或任务失败 | 接口异常、向量库连接失败、模型推理失败、数据库写入失败 |
CRITICAL |
系统级严重故障 | 核心依赖不可用、服务无法启动、配置缺失导致系统整体不可用 |
一句话总结:
默认日志适合临时调试,
logging才适合工程化管理;生产日志必须做到有级别、有格式、有链路 ID、有文件策略、有异常堆栈、有模块区分、有安全边界。
二、为什么这个问题值得深入理解?
2.1 日志不是“随便打印一下”
初学阶段,我们经常这样调试:
print("进入函数")
print("user_id=", user_id)
print("result=", result)
这种方式在本地小脚本里很直接,但一旦进入真实服务端场景,就会暴露问题:
- 输出格式不统一;
- 不知道日志来自哪个模块;
- 不知道是普通信息、警告还是错误;
- 多个请求并发时日志交织在一起;
- 线上服务重启后终端输出可能丢失;
- 无法按级别过滤;
- 无法方便接入日志平台;
- 无法设置日志文件大小、保留天数和切割策略。
服务端日志的目的不是“给开发者看一眼”,而是为了在问题发生时回答几个关键问题:
- 什么时候发生的?
- 哪个请求发生的?
- 哪个用户 / 会话 / 任务触发的?
- 执行到了哪个模块?
- 外部依赖是否正常?
- 错误堆栈是什么?
- 问题影响范围多大?
- 是否需要告警和人工介入?
2.2 AI 服务端为什么更依赖日志?
普通 Web 服务已经需要日志,AI 服务端更需要日志。
因为 AI / RAG / Agent 服务往往包含更多不稳定因素:
- 大模型接口可能超时、限流、返回异常;
- prompt 较长,问题定位依赖上下文;
- RAG 检索可能召回不足、召回错误、相似度过低;
- 向量数据库、对象存储、关系数据库都可能成为故障点;
- 一个用户请求可能经过多个阶段:鉴权、检索、重排、拼接 prompt、模型推理、后处理、存储会话;
- 同一个错误可能不是代码 bug,而是模型输出不稳定、数据质量差、第三方服务波动。
如果没有良好的日志,一句“模型回答不对”几乎无法定位。
一个比较完整的 AI 服务端日志,至少应该能看到:
request_id:这一次请求的唯一标识;user_id/session_id:哪个用户、哪个会话;kb_id:使用了哪个知识库;query摘要:用户问题的大致内容;retrieval_count:召回了多少条;top_score:最高相似度是多少;model:调用了哪个模型;latency_ms:模型耗时多少;input_tokens/output_tokens:token 消耗;error:失败原因;stacktrace:异常堆栈。
三、默认日志、print() 与 logging 的差异
3.1 print() 的特点
print() 是最简单的输出方式:
name = "Tom"
print("create user", name)
它的优点是简单直接,但它不是专业日志系统。
| 对比点 | print() |
|---|---|
| 是否有日志级别 | 没有 |
| 是否能按级别过滤 | 不能 |
| 是否有统一格式 | 需要自己拼接 |
| 是否能输出到多个目的地 | 不方便 |
| 是否适合日志切割 | 不适合 |
| 是否适合异常堆栈 | 需要手动处理 |
| 是否适合线上服务 | 不推荐 |
print() 适合:
- 学习语法;
- 临时验证变量;
- 一次性脚本的简单输出。
不适合:
- Web 服务;
- 后台任务;
- 长期运行进程;
- 需要排查线上问题的系统。
3.2 Python 默认 logging 行为
如果直接使用 logging 模块级函数:
import logging
logging.debug("debug message")
logging.info("info message")
logging.warning("warning message")
logging.error("error message")
通常只会看到:
WARNING:root:warning message
ERROR:root:error message
原因是:
- 默认 root logger 的日志级别是
WARNING; DEBUG和INFO低于WARNING,默认不会输出;- 默认输出到标准错误流;
- 默认格式比较简单,类似
LEVEL:logger_name:message。
也就是说,Python 的默认 logging 并不是不能用,而是默认配置太简单,只能满足最基础的控制台输出。
3.3 默认日志的缺陷
默认日志在生产中主要缺陷如下:
| 缺陷 | 影响 |
|---|---|
| 没有标准化时间格式 | 难以按时间排查问题 |
| 没有链路 ID | 多个请求日志混在一起,无法追踪单次请求 |
| 没有模块区分 | 不知道日志来自接口、模型、数据库还是向量检索 |
| 没有文件切割 | 长期运行后日志文件可能无限增长 |
| 没有结构化字段 | 日志平台不容易检索和统计 |
| 没有异常堆栈 | 只能看到错误描述,看不到错误发生位置 |
| 没有敏感信息控制 | 容易把 token、密码、用户隐私写入日志 |
| 没有不同环境策略 | 开发、测试、生产可能需要不同级别和输出方式 |
3.4 print()、默认 logging、完整 logging 配置对比
| 能力 | print() |
默认 logging | 完整 logging 配置 |
|---|---|---|---|
| 日志级别 | 无 | 有,默认 WARNING |
可自定义 |
| 输出格式 | 手动拼接 | 默认简单格式 | 可统一配置 |
| 输出位置 | 标准输出 | 标准错误 | 控制台、文件、远程服务等 |
| 模块区分 | 手动写 | 默认 root | getLogger(__name__) |
| 文件切割 | 无 | 无 | 可用 rotating handler |
| 异常堆栈 | 手动处理 | 支持但需正确使用 | 统一规范 |
| 链路 ID | 无 | 无 | 可通过 Filter 注入 |
| 日志平台接入 | 不方便 | 较弱 | 适合结构化采集 |
| 生产可维护性 | 差 | 一般 | 强 |
四、logging 基础使用方法
4.1 五个常用日志级别
Python 标准库 logging 内置了五个常用级别:
import logging
logging.debug("调试信息")
logging.info("普通信息")
logging.warning("警告信息")
logging.error("错误信息")
logging.critical("严重错误")
级别从低到高:
DEBUG < INFO < WARNING < ERROR < CRITICAL
日志系统会根据配置的阈值决定哪些日志可以输出。
例如配置为 INFO 时:
DEBUG不输出;INFO、WARNING、ERROR、CRITICAL输出。
4.2 使用 basicConfig() 快速配置
最简单的配置方式是 logging.basicConfig():
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logging.debug("debug message")
logging.info("service started")
logging.warning("memory usage is high")
logging.error("database connection failed")
可能输出:
2026-07-09 10:00:00,123 | INFO | root | service started
2026-07-09 10:00:00,124 | WARNING | root | memory usage is high
2026-07-09 10:00:00,125 | ERROR | root | database connection failed
常见格式字段:
| 字段 | 含义 |
|---|---|
%(asctime)s |
日志时间 |
%(levelname)s |
日志级别名称 |
%(name)s |
logger 名称 |
%(module)s |
模块名 |
%(funcName)s |
函数名 |
%(lineno)d |
行号 |
%(message)s |
日志消息 |
%(process)d |
进程 ID |
%(threadName)s |
线程名 |
4.3 输出到文件
import logging
logging.basicConfig(
filename="app.log",
filemode="a",
encoding="utf-8",
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logging.info("application started")
logging.error("something failed")
说明:
filename="app.log":写入文件;filemode="a":追加模式;encoding="utf-8":避免中文乱码;level=logging.INFO:输出INFO及以上级别。
不过,basicConfig(filename=...) 只适合简单程序。生产服务更推荐使用 Handler 或 dictConfig()。
4.4 推荐使用 getLogger(__name__)
在真实项目中,不推荐所有地方都直接写:
logging.info("message")
更推荐每个模块创建自己的 logger:
import logging
logger = logging.getLogger(__name__)
def create_user(user_id: int):
logger.info("create user, user_id=%s", user_id)
__name__ 是当前模块名。
如果文件结构如下:
project/
api/user.py
service/chat.py
service/retrieval.py
那么不同模块的 logger 名可能是:
api.user
service.chat
service.retrieval
这样日志中就能看出消息来自哪个模块。
4.5 为什么推荐占位符,而不是 f-string?
日志中经常看到这种写法:
logger.info("user login, user_id=%s", user_id)
而不是:
logger.info(f"user login, user_id={user_id}")
原因是 logging 会延迟格式化。只有当这条日志真的需要输出时,才会把参数拼到字符串里。
这在 DEBUG 日志很多、生产环境又关闭 DEBUG 时尤其有意义。
推荐写法:
logger.debug("retrieval result: query=%s, docs=%s", query, docs)
不推荐:
logger.debug(f"retrieval result: query={query}, docs={docs}")
当然,对于普通业务代码来说,性能差异通常不是最大问题,但使用占位符是更符合 logging 习惯的写法。
4.6 记录异常堆栈
错误日志最重要的不是一句“出错了”,而是完整堆栈。
不推荐:
try:
1 / 0
except ZeroDivisionError as e:
logger.error("calculate failed: %s", e)
这只能看到错误信息,看不到完整调用链。
推荐:
try:
1 / 0
except ZeroDivisionError:
logger.exception("calculate failed")
logger.exception() 只能在 except 块中使用,它会自动带上当前异常堆栈。
等价写法:
try:
1 / 0
except ZeroDivisionError:
logger.error("calculate failed", exc_info=True)
输出中会包含类似:
Traceback (most recent call last):
File "app.py", line 10, in <module>
1 / 0
ZeroDivisionError: division by zero
自动包含:
出错文件名
出错代码行号
完整函数调用栈
异常类型 + 异常信息
线上排障时,这个堆栈非常关键。
五、logging 核心组件模型
logging 模块的强大之处在于它不是简单打印,而是一套可扩展的日志处理流程。
一次日志调用大致经过:
业务代码
↓
Logger
↓
Filter
↓
Handler
↓
Formatter
↓
控制台 / 文件 / 远程服务
5.1 Logger:日志入口
Logger 是业务代码直接使用的对象。
import logging
logger = logging.getLogger("service.chat")
logger.info("chat request received")
logger.error("chat request failed")
它负责判断:
- 当前日志级别是否应该处理;
- 这条日志交给哪些 handler;
- 是否向父 logger 传播。
5.2 Handler:日志输出目的地
Handler 决定日志输出到哪里。
常见 handler:
| Handler | 作用 |
|---|---|
StreamHandler |
输出到控制台 |
FileHandler |
输出到文件 |
RotatingFileHandler |
按文件大小切割 |
TimedRotatingFileHandler |
按时间切割 |
QueueHandler |
放入队列,适合异步或多进程场景 |
SMTPHandler |
通过邮件发送日志 |
SysLogHandler |
输出到 syslog |
示例:同时输出到控制台和文件:
import logging
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)s | %(levelname)s | %(name)s | %(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.ERROR)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.info("this message goes to console")
logger.error("this message goes to console and file")
这里:
INFO日志只进入控制台;ERROR日志同时进入控制台和文件。
5.3 Formatter:日志格式
Formatter 决定日志最终长什么样。
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
示例输出:
2026-07-09 10:00:00 | INFO | service.chat | ask:42 | chat success
生产中建议至少包含:
- 时间;
- 级别;
- logger 名称;
- 模块 / 函数 / 行号;
- request_id;
- message。
5.4 Filter:过滤或补充上下文
Filter 可以决定一条日志是否输出,也可以给日志记录补充字段。
例如给每条日志加上 request_id:
import logging
class RequestIdFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
record.request_id = "req-001"
return True
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.addFilter(RequestIdFilter())
handler.setFormatter(logging.Formatter(
"%(asctime)s | %(levelname)s | %(request_id)s | %(name)s | %(message)s"
))
logger.addHandler(handler)
logger.info("hello")
输出:
2026-07-09 10:00:00,123 | INFO | req-001 | app | hello
后面我们会用 contextvars 实现真正的请求级 request_id。
5.5 LogRecord:一条日志事件
当你调用:
logger.info("user login, user_id=%s", user_id)
logging 内部会创建一个 LogRecord 对象。
它里面包含:
- 日志级别;
- 日志内容;
- logger 名;
- 文件路径;
- 行号;
- 函数名;
- 时间戳;
- 线程、进程信息;
- 异常信息;
- 自定义字段。
Formatter 最终就是从 LogRecord 中取字段,拼成字符串。
5.6 日志传播与重复打印问题
Logger 是有层级的。
例如:
service
service.chat
service.retrieval
service.chat 是 service 的子 logger。
默认情况下,子 logger 的日志会向父 logger 传播,这叫 propagate。
如果你同时给子 logger 和 root logger 都加了 handler,就可能出现重复输出。
示例:
import logging
root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(logging.StreamHandler())
logger = logging.getLogger("service.chat")
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())
logger.info("hello")
可能输出两次。
解决方式之一:
logger.propagate = False
更常见的实践是:
- 应用入口统一配置 root logger;
- 各模块只
getLogger(__name__),不要随便加 handler; - 如果某个 logger 单独配置 handler,再考虑关闭
propagate。
六、日志级别应该如何设计?
6.1 DEBUG:调试细节
DEBUG 用于开发和问题排查,内容通常较详细。
适合记录:
- 函数入参;
- 中间计算结果;
- SQL 参数;
- prompt 片段;
- 向量召回详情;
- 重排前后的文档列表;
- 调用外部接口前后的原始响应摘要。
示例:
logger.debug(
"retrieval detail, query=%s, kb_id=%s, top_k=%s, scores=%s",
query,
kb_id,
top_k,
scores,
)
注意:生产环境通常不长期打开 DEBUG,因为:
- 日志量巨大;
- 影响性能;
- 可能泄露敏感信息;
- 增加日志存储成本。
6.2 INFO:正常业务事件
INFO 用于记录系统正常运行中的关键事件。
适合记录:
- 服务启动成功;
- 用户登录;
- 接口调用;
- 会话创建;
- 知识库更新;
- 文件上传完成;
- 模型调用成功;
- 后台任务完成。
示例:
logger.info(
"chat completed, user_id=%s, session_id=%s, model=%s, latency_ms=%s",
user_id,
session_id,
model,
latency_ms,
)
生产环境通常默认使用 INFO 级别。
6.3 WARNING:异常苗头,但系统还能继续
WARNING 表示发生了不符合预期的情况,但系统仍然可以继续运行。
适合记录:
- 模型调用超时,但重试成功;
- 向量检索召回数量不足;
- 相似度低于阈值;
- 缓存未命中率异常升高;
- 内存水位偏高;
- 第三方接口响应变慢;
- 用户输入过长,被截断处理。
示例:
if len(docs) < min_docs:
logger.warning(
"retrieval result too few, query=%s, kb_id=%s, expected=%s, actual=%s",
query,
kb_id,
min_docs,
len(docs),
)
WARNING 的重点是:需要关注,但不一定代表当前请求失败。
6.4 ERROR:当前操作失败
ERROR 表示某个请求、任务或操作失败,需要定位原因。
适合记录:
- 接口处理失败;
- 数据库写入失败;
- 向量库连接中断;
- 模型推理失败;
- 文件解析失败;
- 消息队列消费失败。
示例:
try:
result = call_llm(prompt)
except TimeoutError:
logger.exception("llm call failed, model=%s", model)
raise
注意:不是所有“不符合预期”都应该用 ERROR。
例如:
- 用户输错密码:通常是
INFO或WARNING; - 请求参数校验失败:通常是
INFO或WARNING; - 检索没有结果但系统能正常返回兜底回答:通常是
WARNING; - 第三方接口失败导致当前请求无法完成:应该是
ERROR。
6.5 CRITICAL:系统级严重故障
CRITICAL 表示系统处于非常严重的异常状态。
适合记录:
- 服务无法启动;
- 核心配置缺失;
- 数据库整体不可用;
- 模型服务全局不可用;
- 磁盘空间耗尽;
- 消息队列完全不可连接。
示例:
try:
load_required_config()
except Exception:
logger.critical("service startup failed: required config missing", exc_info=True)
raise
CRITICAL 通常应该触发告警。
6.6 AI 服务端日志级别建议表
| 场景 | 推荐级别 | 示例 |
|---|---|---|
| 打印 prompt 调试片段 | DEBUG |
prompt built, tokens=1200 |
| 向量召回文档得分 | DEBUG |
retrieval scores=[0.91, 0.83] |
| 用户发起对话请求 | INFO |
chat request received |
| 模型调用成功 | INFO |
llm call success, latency_ms=820 |
| 知识库更新完成 | INFO |
kb updated, file_count=10 |
| 检索结果为空 | WARNING |
retrieval empty |
| 模型调用超时但重试成功 | WARNING |
llm timeout, retry success |
| 向量库连接失败 | ERROR |
vector db connection failed |
| 模型推理失败 | ERROR |
llm generation failed |
| 服务启动失败 | CRITICAL |
application startup failed |
七、生产环境配置:从简单到可维护
7.1 控制台 + 文件双输出
生产中常见需求:
- 控制台输出给容器平台采集;
- 文件输出用于本机保留和排查;
- 不同 handler 可以有不同级别。
import logging
from logging.handlers import RotatingFileHandler
def setup_logging():
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
file_handler = RotatingFileHandler(
filename="app.log",
maxBytes=10 * 1024 * 1024,
backupCount=5,
encoding="utf-8",
)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
logger = setup_logging()
logger.info("application started")
7.2 按大小切割:RotatingFileHandler
如果日志一直写入同一个文件,文件可能越来越大。
RotatingFileHandler 可以按大小切割:
from logging.handlers import RotatingFileHandler
file_handler = RotatingFileHandler(
filename="app.log",
maxBytes=100 * 1024 * 1024,
backupCount=10,
encoding="utf-8",
)
含义:
maxBytes=100 * 1024 * 1024:单个日志文件最大 100MB;backupCount=10:最多保留 10 个历史文件;- 超过大小后会生成
app.log.1、app.log.2等文件。
适合:
- 日志量比较均匀;
- 希望控制单文件大小;
- 单机部署或传统 VM 部署。
7.3 按时间切割:TimedRotatingFileHandler
如果希望每天生成一个日志文件,可以使用 TimedRotatingFileHandler。
from logging.handlers import TimedRotatingFileHandler
file_handler = TimedRotatingFileHandler(
filename="app.log",
when="midnight",
interval=1,
backupCount=14,
encoding="utf-8",
)
含义:
when="midnight":每天午夜切割;interval=1:每 1 天切割一次;backupCount=14:保留最近 14 天日志。
适合:
- 按天排查问题;
- 日志平台按日期归档;
- 运维习惯按天管理日志。
7.4 用 dictConfig() 集中管理配置
当项目变大时,不建议到处手动创建 handler。
更推荐用 logging.config.dictConfig() 集中配置。
import logging
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "standard",
},
"app_file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "standard",
"filename": "app.log",
"maxBytes": 10485760,
"backupCount": 5,
"encoding": "utf-8",
},
"error_file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "ERROR",
"formatter": "standard",
"filename": "error.log",
"maxBytes": 10485760,
"backupCount": 10,
"encoding": "utf-8",
},
},
"loggers": {
"app": {
"handlers": ["console", "app_file", "error_file"],
"level": "INFO",
"propagate": False,
},
"app.retrieval": {
"handlers": ["console", "app_file"],
"level": "INFO",
"propagate": False,
},
"app.llm": {
"handlers": ["console", "app_file", "error_file"],
"level": "INFO",
"propagate": False,
},
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
}
def setup_logging():
logging.config.dictConfig(LOGGING_CONFIG)
setup_logging()
logger = logging.getLogger("app.llm")
logger.info("llm logger ready")
这个配置的特点:
app日志写入控制台、普通文件、错误文件;ERROR及以上级别额外进入error.log;app.retrieval和app.llm可以单独调整级别和输出位置;root保持WARNING,避免第三方库输出过多低级别日志。
7.5 不同环境使用不同级别
常见环境策略:
| 环境 | 推荐级别 | 说明 |
|---|---|---|
| 本地开发 | DEBUG |
方便看细节 |
| 测试环境 | INFO 或 DEBUG |
根据问题排查需要调整 |
| 预发环境 | INFO |
尽量接近生产 |
| 生产环境 | INFO |
默认记录关键事件 |
| 生产临时排障 | 短时间开启 DEBUG |
排查后及时恢复 |
示例:
import logging
import os
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=getattr(logging, LOG_LEVEL, logging.INFO),
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
八、AI / RAG 服务端日志实战模板
下面用一个简化的 RAG 问答流程演示如何记录日志。
流程包括:
- 接收用户问题;
- 检索知识库;
- 构造 prompt;
- 调用模型;
- 返回答案;
- 记录成功或失败日志。
8.1 普通函数版示例
import logging
import time
logger = logging.getLogger("app.chat")
def retrieve_documents(query: str, kb_id: str, top_k: int = 5):
# 这里用模拟数据代替真实向量检索
docs = [
{"doc_id": "doc-1", "score": 0.91, "content": "Python logging 用于记录日志"},
{"doc_id": "doc-2", "score": 0.84, "content": "Handler 决定日志输出位置"},
]
logger.debug(
"retrieval detail, query=%s, kb_id=%s, top_k=%s, docs=%s",
query,
kb_id,
top_k,
docs,
)
if len(docs) < top_k:
logger.warning(
"retrieval result less than top_k, query=%s, kb_id=%s, expected=%s, actual=%s",
query,
kb_id,
top_k,
len(docs),
)
return docs
def call_llm(prompt: str, model: str):
# 这里用模拟结果代替真实模型调用
time.sleep(0.1)
return {
"answer": "logging 模块可以通过 Logger、Handler、Formatter 实现生产级日志。",
"input_tokens": 320,
"output_tokens": 80,
}
def answer_question(user_id: str, session_id: str, query: str, kb_id: str):
start = time.perf_counter()
model = "example-llm"
logger.info(
"chat request received, user_id=%s, session_id=%s, kb_id=%s",
user_id,
session_id,
kb_id,
)
try:
docs = retrieve_documents(query=query, kb_id=kb_id)
prompt = build_prompt(query, docs)
result = call_llm(prompt, model=model)
latency_ms = round((time.perf_counter() - start) * 1000, 2)
logger.info(
"chat completed, user_id=%s, session_id=%s, kb_id=%s, model=%s, "
"retrieval_count=%s, input_tokens=%s, output_tokens=%s, latency_ms=%s",
user_id,
session_id,
kb_id,
model,
len(docs),
result["input_tokens"],
result["output_tokens"],
latency_ms,
)
return result["answer"]
except Exception:
latency_ms = round((time.perf_counter() - start) * 1000, 2)
logger.exception(
"chat failed, user_id=%s, session_id=%s, kb_id=%s, model=%s, latency_ms=%s",
user_id,
session_id,
kb_id,
model,
latency_ms,
)
raise
def build_prompt(query: str, docs: list[dict]) -> str:
context = "\n".join(doc["content"] for doc in docs)
return f"基于以下资料回答问题:\n{context}\n\n问题:{query}"
这个示例中:
- 请求开始用
INFO; - 检索细节用
DEBUG; - 召回不足用
WARNING; - 成功完成用
INFO; - 异常失败用
logger.exception()。
8.2 模型调用日志应该记录什么?
模型调用建议记录:
| 字段 | 含义 |
|---|---|
request_id |
请求链路 ID |
user_id |
用户 ID |
session_id |
会话 ID |
model |
模型名称 |
latency_ms |
调用耗时 |
input_tokens |
输入 token 数 |
output_tokens |
输出 token 数 |
retry_count |
重试次数 |
status |
成功或失败 |
error_type |
错误类型 |
示例:
logger.info(
"llm call success, model=%s, input_tokens=%s, output_tokens=%s, latency_ms=%s",
model,
input_tokens,
output_tokens,
latency_ms,
)
失败时:
try:
response = call_llm(prompt)
except TimeoutError:
logger.warning("llm timeout, model=%s, retry_count=%s", model, retry_count)
raise
except Exception:
logger.exception("llm call failed, model=%s", model)
raise
8.3 向量检索日志应该记录什么?
RAG 检索建议记录:
| 字段 | 含义 |
|---|---|
kb_id |
知识库 ID |
query |
用户问题摘要 |
top_k |
期望召回数量 |
retrieval_count |
实际召回数量 |
top_score |
最高相似度 |
threshold |
相似度阈值 |
latency_ms |
检索耗时 |
示例:
def log_retrieval_result(query: str, kb_id: str, docs: list[dict], threshold: float):
top_score = max((doc.get("score", 0) for doc in docs), default=0)
logger.info(
"retrieval completed, kb_id=%s, query=%s, retrieval_count=%s, top_score=%.4f, threshold=%.4f",
kb_id,
query[:80],
len(docs),
top_score,
threshold,
)
if not docs:
logger.warning("retrieval empty, kb_id=%s, query=%s", kb_id, query[:80])
elif top_score < threshold:
logger.warning(
"retrieval low score, kb_id=%s, query=%s, top_score=%.4f, threshold=%.4f",
kb_id,
query[:80],
top_score,
threshold,
)
注意:真实生产中不一定要完整记录用户原始问题,尤其是涉及隐私数据时,应该做截断、脱敏或摘要化。
九、链路 ID / request_id 的实现方式
9.1 为什么需要 request_id?
在 Web 服务中,同一时间可能有很多请求同时执行。
如果日志是这样:
start chat
retrieval completed
start chat
llm call success
retrieval completed
llm call failed
你很难判断每行日志属于哪个请求。
如果加上 request_id:
req-001 | start chat
req-001 | retrieval completed
req-002 | start chat
req-001 | llm call success
req-002 | retrieval completed
req-002 | llm call failed
就能清楚串起一次请求的完整链路。
9.2 使用 contextvars 保存请求上下文
contextvars 适合在异步服务中保存请求级变量。
import contextvars
import logging
import uuid
request_id_var = contextvars.ContextVar("request_id", default="-")
class RequestIdFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
record.request_id = request_id_var.get()
return True
def setup_logging():
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.addFilter(RequestIdFilter())
handler.setFormatter(logging.Formatter(
"%(asctime)s | %(levelname)-8s | %(request_id)s | %(name)s | %(message)s"
))
logger.addHandler(handler)
return logger
logger = setup_logging()
def handle_request(user_id: str):
request_id = str(uuid.uuid4())
token = request_id_var.set(request_id)
try:
logger.info("request start, user_id=%s", user_id)
do_something()
logger.info("request finished, user_id=%s", user_id)
except Exception:
logger.exception("request failed, user_id=%s", user_id)
raise
finally:
request_id_var.reset(token)
def do_something():
logger.info("do something")
这样,同一次请求中的日志都会自动带上相同的 request_id。
9.3 FastAPI 中的 request_id 思路
如果是 FastAPI,可以通过中间件设置 request_id。
下面示例需要安装 FastAPI,属于 Web 框架场景,不是 logging 标准库本身:
import contextvars
import logging
import uuid
from fastapi import FastAPI, Request
app = FastAPI()
request_id_var = contextvars.ContextVar("request_id", default="-")
logger = logging.getLogger("app")
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
token = request_id_var.set(request_id)
try:
logger.info("http request start, method=%s, path=%s", request.method, request.url.path)
response = await call_next(request)
logger.info("http request end, status_code=%s", response.status_code)
response.headers["X-Request-ID"] = request_id
return response
except Exception:
logger.exception("http request failed")
raise
finally:
request_id_var.reset(token)
实际项目中,RequestIdFilter 会从 request_id_var 读取值并注入日志。
十、结构化日志与可观测性
10.1 为什么需要结构化日志?
普通文本日志适合人看:
2026-07-09 10:00:00 | INFO | req-001 | app.chat | chat completed, user_id=1001, latency_ms=820
但日志平台更喜欢结构化字段:
{"time":"2026-07-09 10:00:00","level":"INFO","request_id":"req-001","logger":"app.chat","event":"chat_completed","user_id":"1001","latency_ms":820}
结构化日志的优势:
- 可以按字段检索:
request_id=req-001; - 可以统计:平均
latency_ms; - 可以聚合:按
model分组看错误率; - 可以告警:
level=ERROR超过阈值; - 可以和链路追踪、指标系统关联。
10.2 简单 JSON Formatter 示例
下面用标准库实现一个简化版 JSON formatter:
import json
import logging
from datetime import datetime
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
log_data = {
"time": datetime.fromtimestamp(record.created).isoformat(),
"level": record.levelname,
"logger": record.name,
"module": record.module,
"function": record.funcName,
"line": record.lineno,
"message": record.getMessage(),
}
if hasattr(record, "request_id"):
log_data["request_id"] = record.request_id
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data, ensure_ascii=False)
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
logger.info("service started")
输出类似:
{"time":"2026-07-09T10:00:00.123456","level":"INFO","logger":"app","module":"main","function":"<module>","line":30,"message":"service started"}
实际生产中也可以使用成熟的第三方结构化日志库,但理解标准库实现原理很重要。
10.3 日志、指标、链路追踪的关系
可观测性通常包括三类数据:
| 类型 | 作用 | 示例 |
|---|---|---|
| 日志 Logs | 记录具体事件 | 某次请求失败堆栈 |
| 指标 Metrics | 统计趋势和数值 | QPS、错误率、P95 延迟 |
| 链路追踪 Traces | 展示一次请求经过哪些服务 | API → 检索 → 模型 → 数据库 |
日志不是万能的。
例如:
- 想知道“过去 5 分钟错误率是否超过 5%”,更适合指标;
- 想知道“一次请求到底卡在哪个服务”,更适合链路追踪;
- 想知道“这次错误具体堆栈是什么”,更适合日志。
但日志是最基础、最容易落地的一环。
十一、常见坑与最佳实践
11.1 不要用 print() 替代生产日志
print() 不能提供级别、模块、格式、切割、异常堆栈等能力。
本地临时调试可以用,长期保留的服务端代码应使用 logging。
11.2 不要在库代码里随意 basicConfig()
如果你写的是一个被别人导入的模块或库,不应该在库内部主动配置全局日志。
不推荐:
# mylib.py
import logging
logging.basicConfig(level=logging.INFO)
推荐:
# mylib.py
import logging
logger = logging.getLogger(__name__)
def do_work():
logger.info("work started")
让应用入口决定日志如何配置。
11.3 避免重复添加 handler
在某些场景中,setup_logging() 可能被调用多次。
如果每次都 addHandler(),日志会重复输出。
可以这样防御:
import logging
def setup_logging():
logger = logging.getLogger("app")
if logger.handlers:
return logger
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
"%(asctime)s | %(levelname)s | %(name)s | %(message)s"
))
logger.addHandler(handler)
return logger
或者更推荐在应用启动入口只配置一次日志。
11.4 不要记录敏感信息
日志很容易被多人访问,也可能进入第三方日志平台。
不要记录:
- 密码;
- token;
- API key;
- 身份证号;
- 银行卡号;
- 完整手机号;
- 用户隐私文本;
- 未脱敏的 prompt 和模型输出。
示例:
def mask_phone(phone: str) -> str:
if len(phone) != 11:
return "***"
return phone[:3] + "****" + phone[-4:]
logger.info("user login, phone=%s", mask_phone("13812345678"))
对于 AI 服务,尤其要注意用户输入可能包含隐私信息,不要无脑记录完整 query、prompt 和回答。
11.5 异常日志要带堆栈
不推荐:
except Exception as e:
logger.error("failed: %s", e)
推荐:
except Exception:
logger.exception("operation failed")
或者:
except Exception:
logger.error("operation failed", exc_info=True)
11.6 日志内容要有业务语义
不好的日志:
logger.info("success")
logger.error("failed")
问题是:什么成功?什么失败?谁的请求?哪个模块?
更好的日志:
logger.info(
"kb update success, kb_id=%s, file_count=%s, user_id=%s",
kb_id,
file_count,
user_id,
)
logger.exception(
"kb update failed, kb_id=%s, file_name=%s, user_id=%s",
kb_id,
file_name,
user_id,
)
好的日志应该能回答:
- 谁触发的?
- 做了什么?
- 结果如何?
- 关键参数是什么?
- 失败原因是什么?
- 如何继续定位?
11.7 不要把所有问题都打成 ERROR
如果把参数错误、用户取消、检索为空、缓存未命中都打成 ERROR,真正的系统故障会被淹没。
判断标准:
| 问题 | 推荐级别 |
|---|---|
| 用户输入不合法 | INFO / WARNING |
| 查询无结果但有兜底返回 | WARNING |
| 第三方接口偶发超时但重试成功 | WARNING |
| 当前请求失败 | ERROR |
| 系统核心能力不可用 | CRITICAL |
11.8 第三方库日志要控制级别
有些第三方库可能输出大量日志。
可以单独设置:
import logging
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
这样可以避免生产日志被无关细节刷屏。
11.9 多进程 / 高并发日志要谨慎
在多进程场景下,多个进程同时写同一个文件可能带来竞争问题。
常见方案:
- 容器化部署中直接输出到 stdout,由平台采集;
- 使用
QueueHandler+QueueListener集中写日志; - 使用外部日志采集 Agent;
- 避免多个进程同时写同一个普通文件。
简化版队列日志思路:
import logging
import logging.handlers
import queue
log_queue = queue.Queue()
queue_handler = logging.handlers.QueueHandler(log_queue)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter(
"%(asctime)s | %(levelname)s | %(name)s | %(message)s"
))
listener = logging.handlers.QueueListener(log_queue, stream_handler)
listener.start()
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
logger.addHandler(queue_handler)
logger.info("message from application")
listener.stop()
这个例子只是说明思路:业务线程把日志放入队列,由 listener 统一处理输出。
十二、完整示例:一个可复用的生产日志配置
下面给出一个相对完整的标准库版本,适合作为中小型项目起点。
import contextvars
import logging
import logging.config
import uuid
request_id_var = contextvars.ContextVar("request_id", default="-")
class RequestIdFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
record.request_id = request_id_var.get()
return True
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
"request_id": {
"()": RequestIdFilter,
}
},
"formatters": {
"standard": {
"format": (
"%(asctime)s | %(levelname)-8s | %(request_id)s | "
"%(name)s | %(funcName)s:%(lineno)d | %(message)s"
),
"datefmt": "%Y-%m-%d %H:%M:%S",
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "standard",
"filters": ["request_id"],
},
"app_file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "standard",
"filters": ["request_id"],
"filename": "app.log",
"maxBytes": 10485760,
"backupCount": 5,
"encoding": "utf-8",
},
"error_file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "ERROR",
"formatter": "standard",
"filters": ["request_id"],
"filename": "error.log",
"maxBytes": 10485760,
"backupCount": 10,
"encoding": "utf-8",
},
},
"loggers": {
"app": {
"handlers": ["console", "app_file", "error_file"],
"level": "INFO",
"propagate": False,
}
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
}
def setup_logging():
logging.config.dictConfig(LOGGING_CONFIG)
def handle_request(user_id: str):
request_id = str(uuid.uuid4())
token = request_id_var.set(request_id)
logger = logging.getLogger("app.chat")
try:
logger.info("request start, user_id=%s", user_id)
logger.info("chat completed, user_id=%s", user_id)
except Exception:
logger.exception("request failed, user_id=%s", user_id)
raise
finally:
request_id_var.reset(token)
if __name__ == "__main__":
setup_logging()
handle_request("user-001")
这个配置具备:
- 统一格式;
- request_id 注入;
- 控制台输出;
- 普通日志文件;
- 错误日志文件;
- 文件大小切割;
- 模块化 logger;
- 异常堆栈支持。
十三、如何回答更专业?
13.1 问题一:Python 默认日志有什么缺陷?
可以这样回答:
Python 默认日志配置比较简单。默认 root logger 的级别是
WARNING,低于WARNING的DEBUG和INFO默认不会输出;默认格式通常只包含级别、logger 名和消息,不适合生产排障。
如果只用print()或未配置的 logging,会缺少统一时间格式、模块名、函数行号、request_id、异常堆栈、日志切割和结构化字段。
在线上服务中,这会导致无法按请求链路追踪问题,也无法区分接口日志、业务日志、模型日志、数据库日志和向量检索日志,更不方便接入日志平台和告警系统。
因此生产环境应该基于logging配置 logger、handler、formatter、filter,并结合日志级别、文件切割、链路 ID 和异常堆栈形成完整日志体系。
13.2 问题二:AI 服务端生产日志如何分级配置?
可以这样回答:
AI 服务端一般生产默认使用
INFO级别,本地开发或临时排障时开启DEBUG。DEBUG用于记录模型入参摘要、prompt 构造细节、向量召回结果、相似度分数等调试信息,但生产中要注意脱敏和控制日志量。INFO用于记录正常业务事件,比如接口调用、会话创建、知识库更新、模型调用成功、token 消耗、耗时等。WARNING用于记录可恢复但需要关注的问题,比如模型调用超时后重试成功、检索召回量不足、相似度过低、缓存异常、内存水位偏高。ERROR用于记录当前请求或任务失败的问题,比如接口崩溃、向量库连接中断、模型推理失败、数据库写入失败,并且应该带上 request_id 和异常堆栈。CRITICAL用于核心服务不可用、启动失败、关键配置缺失等系统级严重故障,通常需要触发告警。
同时日志中应包含 request_id、user_id、session_id、model、kb_id、latency、token 数等关键字段,方便后续检索、追踪和告警。
十四、围绕这个问题还应该举一反三掌握什么?
14.1 异常处理机制
日志和异常处理关系非常紧密。
应该掌握:
try-except-else-finally;- 捕获具体异常,而不是无脑捕获
Exception; raise重新抛出异常;- 自定义异常;
logger.exception()记录堆栈。
14.2 文件 IO 与上下文管理
日志最终经常写入文件,因此需要理解:
- 文件打开模式;
- 编码问题;
- 文件句柄释放;
with上下文管理;- 文件大小增长和切割策略。
14.3 服务端可观测性
日志只是可观测性的一部分,还应该了解:
- Metrics:QPS、错误率、P95 延迟;
- Tracing:链路追踪;
- Alerting:告警规则;
- Dashboard:监控面板;
- OpenTelemetry:统一可观测性标准。
14.4 安全与合规
日志不是越详细越好。
必须注意:
- 敏感字段脱敏;
- 控制 prompt 和模型输出记录范围;
- 避免记录 access token、API key;
- 日志访问权限控制;
- 日志保留周期;
- 用户隐私和合规要求。
14.5 性能与成本
日志也有成本:
- 字符串格式化成本;
- 磁盘 IO 成本;
- 日志采集和存储成本;
- 日志平台索引成本;
- 高并发下锁竞争或队列积压。
因此生产中需要:
- 合理设置日志级别;
- 避免无意义高频日志;
- 对大字段截断;
- 必要时做采样;
- 使用异步或队列日志方案。
十五、总结
Python 的默认日志能力适合简单脚本,但不适合生产服务。
真正的生产级日志体系应该至少具备:
- 日志级别清晰:
DEBUG、INFO、WARNING、ERROR、CRITICAL各司其职。 - 格式统一:时间、级别、模块、函数、行号、消息结构一致。
- 模块区分:通过
getLogger(__name__)区分不同代码模块。 - 输出可控:控制台、文件、错误文件、远程日志平台按需配置。
- 文件可管理:使用
RotatingFileHandler或TimedRotatingFileHandler防止无限增长。 - 异常可定位:错误日志必须带堆栈。
- 链路可追踪:通过 request_id / trace_id 串起一次请求。
- 字段可检索:关键字段结构化,便于日志平台查询。
- 内容有边界:避免敏感信息泄露。
- 适应业务场景:AI 服务端要重点记录模型、检索、token、耗时、知识库、会话等信息。
最后可以用一句话收束:
logging不只是 Python 的一个标准库模块,而是服务端工程中定位问题、理解系统运行状态、支撑告警和可观测性的基础设施。能否设计好日志,往往体现了一个开发者是否具备生产环境思维。
更多推荐



所有评论(0)