为什么我暂时抛弃了 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的原因,享受它带来的便利,避开它会引起的坑!
日志是项目上线后你的“眼睛”,花半小时好好配一下,未来无数个深夜调试都会感谢现在的自己。
