为什么我暂时抛弃了 logging

📅 2026/6/25 13:34:19
为什么我暂时抛弃了 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, rotation10 MB, retention7 days, levelINFO)你看rotation 按文件大小自动轮转retention 自动清理老日志而且压缩、自定义格式都能在同一个方法里搞定。这比 logging 的RotatingFileHandler TimedRotatingFileHandler那种套娃组合直观太多了。但说个容易翻车的点在 FastAPI 这种异步框架里千万别漏了 enqueueTrue 参数。官方示例可能没写但根据我线上血的教训不加这个参数多并发下日志写入会阻塞事件循环轻则响应变慢重则日志串行甚至丢数据。正确姿势logger.add(app.log, rotation10 MB, retention7 days, levelINFO, enqueueTrue)loguru 会把日志消息扔进一个线程安全的队列专门有后台线程负责写入你的主流程该干嘛干嘛完全不用分心。这就像点一杯奶茶小程序下单后不用在店里干等做好了自然叫你。 和 Uvicorn 的日志整合是不是以为这样就完了还有个折磨过我的地方FastAPI 底层用的 Uvicorn 自己也哗哗地打印日志两边各玩各的管理起来特难受。我的做法是在应用启动的地方把 Uvicorn 的日志也“绑架”到 loguru 里来import logging from loguru import logger class InterceptHandler(logging.Handler): def emit(self, record): logger_opt logger.opt(depth6, exceptionrecord.exc_info) logger_opt.log(record.levelname, record.getMessage()) # 在 FastAPI 的 lifespan 或 startup 事件里执行 logging.basicConfig(handlers[InterceptHandler()], level0)这样所有日志都汇聚到一个口子输出规则统一查找问题就像过红绿灯一次看清。但在实际开发时还有个问题就是如果启用了--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(depthdepth, exceptionrecord.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 时加上backtraceTrue。 敏感信息密码、token一定要在日志里脱敏loguru 支持 filter 功能可以优雅地过滤字段。 日志文件路径别写成相对路径否则在守护进程启动时可能写到莫名其妙的地方建议使用绝对路径或基于项目根目录拼接。最后啰嗦一句有优点也有不足这也是为什么前面我说“暂时”替代标准库logging的原因享受它带来的便利避开它会引起的坑日志是项目上线后你的“眼睛”花半小时好好配一下未来无数个深夜调试都会感谢现在的自己。