那天半夜两点多吧,我正躺床上刷短视频,手机一震,钉钉消息蹦出来: “线上有个 Python 接口一直 500,看下日志?”
我心里咯噔一下。你们懂的,程序员听到“看下日志”这仨字,基本等于——今晚别睡了。
登上服务器一看,项目目录里一堆奇奇怪怪的文件:run.log、log.log、debug.log.bak.bak,还有一个 3G 大小的 nohup.out,仿佛谁往地上撒了一把日志骰子,看缘分才能对上。那一刻我就觉得,Python 自带那个 logging 啊……挺强的,但用起来,真有点像在组装乐高。
更离谱的是,打开代码一搜:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
logger = logging.getLogger(__name__)
到处都是这段,复制粘贴套娃,十几个模块,每个文件都自己 basicConfig 一遍,有的输出到文件,有的只打到控制台,有的还少写了个 %,日志直接报错。我当时脑子里只有一句话:谁写的,站出来我们当面交流。结果一看 git 记录……是我半年前写的,行,闭环了。
说真的,Python 的 logging 模块设计得很专业,handler、filter、formatter、logger 分得特别清楚,就像音响工程师调台那种感觉。但你上班只是想简单看个“哪个接口挂了”,突然要你理解“日志传播”“logger 层级”“根 logger”,心情就有点复杂。
比如你要搞一个需求:
按官方 logging 写,大概是这种味道:
import logging
from logging.handlers import TimedRotatingFileHandler
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)
fmt = logging.Formatter(
"%(asctime)s [%(levelname)s] [%(name)s] "
"[%(filename)s:%(lineno)d] %(message)s"
)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(fmt)
file_handler = TimedRotatingFileHandler(
"app.log", when="midnight", backupCount=7, encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(fmt)
error_handler = logging.FileHandler("error.log", encoding="utf-8")
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(fmt)
logger.addHandler(console)
logger.addHandler(file_handler)
logger.addHandler(error_handler)
你看,这还没真正开始写业务,就先撸了小二十行配置。等你要往里加个 trace_id 啊、user_id 啊,再搞些上下文信息,format 字符串越写越长,跟写 SQL 拼接似的,一不小心还少写一个 )。
我那会儿就有点崩溃:为啥 Java 那边搞个日志都用框架包一层,Python 这边还在徒手堆 handler?之前 SpringBoot 默认日志不改,磁盘被打爆那次我刚翻过去没多久,阴影还在呢。
直到有天同事跟我说,“你别折磨 logging 了,试试 loguru 吧,真香。” 我当时还有点脾气:就一个打日志的东西,有必要换库吗? 然后我用了一下午,就把之前那堆 logging 配置全删了……
你感受一下最开始那个需求,用 loguru 写,大概就长这样:
from loguru import logger
# 初始化,一般在项目入口搞一次就行
logger.add(
"app.log",
rotation="00:00", # 午夜轮转
retention="7 days", # 只保留7天
encoding="utf-8",
enqueue=True, # 多进程安全
backtrace=True, # 打完整调用栈
diagnose=True# 打更多调试信息
)
# 控制台默认就有,懒得配
然后业务里就很朴素:
deflogin(user_id: int):
logger.info("用户 {user_id} 请求登录", user_id=user_id)
# 假装这里有一堆业务逻辑
if user_id == 0:
raise ValueError("非法用户")
logger.success("用户 {user_id} 登录成功", user_id=user_id)
你看,它连字符串都不用 f"{}",直接 {user_id} 就行,传参数进去,它帮你格式化好,手抖少打个 f 也不会变成 "用户 {user_id} 登录成功" 这种尴尬字符串。
那天线上那个 500,我就是这样搞的:在入口给 loguru 加了个输出到文件,顺手把 traceback 也打开,结果下一次错误立马就看出来了——是某个第三方 SDK 返回的数据结构变了。之前用 logging 的时候,只打了个 str(e),现在 loguru 自动给我打了完整堆栈,连哪一行 JSON 解析的都标出来了。那一刻我心里说了句:这玩意儿,行。
还有个特别爽的点,就是“自动带上下文”。以前用 logging,我要给每一条日志都拼上 trace_id,写着写着就变成:
logger.info("trace=%s user=%s action=%s", trace_id, user_id, action)
写久了你都怀疑自己是不是在写 log4j 配置。
loguru 直接:
from loguru import logger
defwith_trace(trace_id: str):
# 绑定上下文,返回一个新的 logger
return logger.bind(trace_id=trace_id)
defhandle_request(trace_id: str, user_id: int):
log = with_trace(trace_id)
log.info("收到请求,user_id={}", user_id)
# ... 业务逻辑
log.info("处理完成")
然后你在格式里加一下 {extra[trace_id]} 就行:
logger.add(
"app.log",
format="{time} | {level} | {extra[trace_id]} | {message}"
)
以后只要你用 log = logger.bind(trace_id=xxx) 得到的那个 log 打日志,这个 trace_id 就跟着走,根本不用每条日志都手动拼,调接口链路的时候,看起来就很丝滑。
更变态的是,它把异常捕获这事也给你管了。以前你得这么写:
try:
do_something()
except Exception:
logger.exception("处理请求失败")
现在你可以直接挂个装饰器在入口函数上:
from loguru import logger
@logger.catch
defmain():
# 所有没捕获的异常,帮你打出来
run_server()
有一次我本地跑一个脚本,忘了处理某个边界条件,按理说脚本就蹦了。但加了 @logger.catch 之后,它不仅把异常打了,还顺手给你标注了哪一行的变量值是啥,调试的时候像开了透视挂一样。
当然,真实项目里也不能瞎用。比如你想兼容现有那些到处乱飞的 logging.getLogger(__name__),也不是不行,loguru 甚至还贴心给你准备了个桥接方案,大概这种感觉:
import logging
from loguru import logger
classInterceptHandler(logging.Handler):
defemit(self, record):
# 把老 logging 的日志转发给 loguru
level = record.levelname
logger_opt = logger.opt(depth=6, exception=record.exc_info)
logger_opt.log(level, record.getMessage())
# 替换根 logger 的 handler
logging.basicConfig(handlers=[InterceptHandler()], level=logging.INFO)
这样老代码里用 logging 打的东西,最后都跑到 loguru 里统一输出,你也不用一行一行去改历史债务。这个对老项目迁移,是真的有用,不然没人敢在老系统动日志。
当然啦,loguru 也不是银弹,场面话不说你也懂:
- 你要是那种极致控制狂,非得自己配十几个 handler 精细化调度,那 logging 还是更合适
- 特殊场景,比如框架已经强制帮你把 logging 配好了,你要么乖乖接着用,要么就认真桥接,不然日志会乱成一锅粥
但对大部分日常写业务的人来说,需求就俩: 一个是“我想快速看到问题在哪”; 另一个是“我不想花一下午研究日志算发结构”。
我现在的习惯是,新项目直接上 loguru,当做入口日志门面; 老项目就先把 logging 那套理一遍,能抽出来一层封装就抽一层,实在抽不动再考虑桥接。
顺带提一句,别再迷信“print 大法好”了。开发环境下 print("来啦老弟") 的确比 logger 好使,但是一旦上了服务器:
你要真遇到那种“偶发、线上、只在凌晨、某个地区”的 bug,只有 print 的项目,基本等于摸黑找猫。