不知道对面的朋友是不是也经历过这种绝望:FastAPI 服务跑得好好的,一查日志文件却是空的,或者更惨------日志全串成一锅粥,根本看不出哪个请求在哪儿挂了?
之前用 Python 自带的 logging 模块写了一大坨配置,又是 handlers 又是 formatters,结果放进异步协程里直接"精神分裂"。
直到看见那句"Loguru is a library which aims to bring enjoyable logging in Python."心想:"还能这么玩?赶紧试试..."
好家伙,那种感觉就像手动档开惯了,突然换了一辆自动档性能车------又快又丝滑。
今天咱就来聊聊,在 FastAPI 里怎么把 loguru 用得明明白白,不只要"能跑",还得"在生产环境稳如泰山"。我会把从踩坑到解脱的全过程讲给你听。🎯
⚡️ 为什么我暂时抛弃了 logging
先说我踩过最大的坑:logging 的默认输出是同步阻塞的,FastAPI 的异步特性一来,日志不但会打乱顺序,还可能悄无声息地丢失。
而且你要拿到一个像样的日志,得先写几十行配置,每次开新项目都要先把以前的代码复制过来,烦得要死。
而 loguru 一上来就告诉我:别折腾了,直接打就行。
它只有一个全局的 logger 对象,装上就能用,彩色控制台输出,自带日期、级别和超好看的格式。一句话:清爽,真的清爽。
这里为什么我说是"暂时"呢?是因为Loguru的魅力在于它直击痛点的简洁。用 logger.add() 一行代码就能搞定输出目标、格式、轮转策略等所有事情。
不过它也存在短板。如果直接代替标准库,可能出现业务代码用Loguru日志,uvicorn服务器依然打官方日志的混乱局面,或是要对标准库日志做好"拦截",避免重复刷屏。
能用和用好,是完全不同的两个维度。在大型项目里,选择标准 logging,本质上不是因为它更好用,而是因为它能让整个复杂的系统更好地在一起工作,更具确定性 和可控性 。
所以,对于新项目,建议从 Loguru 起步,享受它的便利。当项目成长为大型/微服务架构时,再将日志系统核心回迁到标准库logging + dictConfig 上。
或 组合使用,各司其职: Loguru 作用于你的业务代码 + 标准库 logging 作为"基础设施",兼顾开发体验和生产系统的兼容性。
📦 安装与第一个日志
好,咱们先来安装。就一行:
uv add loguru
然后在 FastAPI 里直接开怼:
from loguru import logger
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def hello():
logger.info("有人访问了首页,美滋滋")
return {"msg": "Hello"}
跑起来后,你的终端会立马出现一条带有时间戳、级别和高亮颜色的日志,再也不用对着黑白海量的输出发呆了。
这里有个问题,在正式环境一定不要只往控制台打印,接下来重点来了:怎么把日志稳稳地写到文件里。
🔧 常用配置,一次搞懂
你可能会问:"loguru 写文件是不是又要配一堆东西?"完全不用,一行搞定:
logger.add("app.log", rotation="10 MB", retention="7 days", level="INFO")
你看,rotation 按文件大小自动轮转,retention 自动清理老日志,而且压缩、自定义格式都能在同一个方法里搞定。
这比 logging 的 RotatingFileHandler + TimedRotatingFileHandler 那种套娃组合直观太多了。
但说个容易翻车的点:在 FastAPI 这种异步框架里,千万别漏了 enqueue=True 参数。
官方示例可能没写,但根据我线上血的教训,不加这个参数,多并发下日志写入会阻塞事件循环,轻则响应变慢,重则日志串行甚至丢数据。正确姿势:
logger.add("app.log", rotation="10 MB", retention="7 days",
level="INFO", enqueue=True)
loguru 会把日志消息扔进一个线程安全的队列,专门有后台线程负责写入,你的主流程该干嘛干嘛,完全不用分心。
这就像点一杯奶茶,小程序下单后不用在店里干等,做好了自然叫你。😊
🧩 和 Uvicorn 的日志整合
是不是以为这样就完了?还有个折磨过我的地方:FastAPI 底层用的 Uvicorn 自己也哗哗地打印日志,两边各玩各的,管理起来特难受。
我的做法是,在应用启动的地方,把 Uvicorn 的日志也"绑架"到 loguru 里来:
import logging
from loguru import logger
class InterceptHandler(logging.Handler):
def emit(self, record):
logger_opt = logger.opt(depth=6, exception=record.exc_info)
logger_opt.log(record.levelname, record.getMessage())
# 在 FastAPI 的 lifespan 或 startup 事件里执行
logging.basicConfig(handlers=[InterceptHandler()], level=0)
这样,所有日志都汇聚到一个口子输出,规则统一,查找问题就像过红绿灯,一次看清。🚦
但在实际开发时,还有个问题就是如果启用了 --reload模式,或者使用命令 fastapi dev main.py 启动的项目,那有些日志拦截还是会漏掉,
想办法把自定义的 InterceptHandler拦截的更彻底些:
import logging
from loguru import logger
class InterceptHandler(logging.Handler):
def emit(self, record):
# 拿到对应的 loguru 级别
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
# 找到调用栈里真正发出日志的地方
frame, depth = logging.currentframe(), 2
while frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
# 这里是关键:启动的时候,把 root logger 的所有旧 handler 都干掉,
# 只留我们自己的 InterceptHandler
def setup_logging():
logging.root.handlers = [InterceptHandler()]
logging.root.setLevel(logging.INFO)
# 把 uvicorn 那几个 logger 也顺手接管
for name in ("uvicorn", "uvicorn.access", "uvicorn.error"):
logging.getLogger(name).handlers = []
logging.getLogger(name).propagate = True
# 在 FastAPI 应用实例化之前就调用
setup_logging()
⚠️ 这些不足和注意点,请刻在脑子里
loguru 虽香,但也别盲目吹。我总结了几个生产环境必须注意的:
🔸 全局只有一个 logger,多进程部署(如Gunicorn多个worker)时务必注意隔离,避免写入冲突,推荐每个进程单独 add 文件,文件名可以用 PID 区分。
🔸 异常回溯虽然默认就漂亮,但要捕获完整 traceback,记得用 logger.exception() 或者在 add 时加上 backtrace=True。
🔸 敏感信息(密码、token)一定要在日志里脱敏,loguru 支持 filter 功能,可以优雅地过滤字段。
🔸 日志文件路径别写成相对路径,否则在守护进程启动时可能写到莫名其妙的地方,建议使用绝对路径或基于项目根目录拼接。
最后啰嗦一句:
有优点,也有不足,这也是为什么前面我说"暂时"替代标准库logging的原因,享受它带来的便利,避开它会引起的坑!
日志是项目上线后你的"眼睛",花半小时好好配一下,未来无数个深夜调试都会感谢现在的自己。
好啦,今天掏心窝子的分享就到这儿。如果你也被日志折腾过、踩过同样的坑,点个「赞」加个「关注」吧,别让这篇溜走,说不定下次就能帮你省掉一大把掉头发的时间。收藏起来,需要的时候心里不慌。我们下个坑里见~ 🛠️