loguru 0.7 日志:一个 logger 走到底,和三个默认行为
标准库 logging 要配 logger、handler、formatter、filter 四层对象,写对一次之后很稳,但每开一个新项目都要再抄一遍配置;轮转、压缩、JSON 序列化还要额外装 handler。loguru 把这些收进一个全局 logger 和 add():logger.add("app.log", rotation="10 MB", retention="7 days") 一行就把文件、轮转、清理配完。代价是它和标准库的 logging 是两套东西,项目里已经有一堆 logging.getLogger() 的时候,接进来要写适配层。
安装与实际版本
python -m venv D:\ai\cache\temp\py-loguru
D:\ai\cache\temp\py-loguru\Scripts\python.exe -m pip install loguru
Name: loguru
Version: 0.7.3
Location: D:\ai\cache\temp\py-loguru\Lib\site-packages
最小可运行示例
默认就有一个输出到 stderr 的 handler,级别是 DEBUG:
import sys
from loguru import logger
logger.debug("这条 DEBUG 会出来")
logger.info("带变量的写法 {}", "和 str.format 一样")
logger.warning("警告")
换成文件 sink,顺带把轮转和清理配好:
import os
import tempfile
from loguru import logger
workdir = os.path.join(tempfile.gettempdir(), "logdemo")
os.makedirs(workdir, exist_ok=True)
logfile = os.path.join(workdir, "app.log")
logger.remove() # 去掉默认的 stderr sink
logger.add(
logfile,
level="INFO",
rotation="2 KB",
retention=3,
encoding="utf-8",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{line} | {message}",
)
for i in range(60):
logger.info(f"第 {i:02d} 条日志,填充内容 {'x' * 60}")
logger.complete()
print("目录里的文件:", sorted(os.listdir(workdir)))
print("app.log 的大小:", os.path.getsize(logfile), "字节")
目录里的文件: ['app.2026-11-10_14-08-55_778311.log', 'app.2026-11-10_14-08-55_781670.log', 'app.2026-11-10_14-08-55_782692.log', 'app.log']
app.log 的大小: 1160 字节
60 条日志撑出 3 个归档文件,当前 app.log 停在 1160 字节,没超过 2 KB 的阈值。归档文件名是 app.<时间戳>.log,retention=3 表示同类文件最多留 3 个。
结构化日志用 serialize=True,每行一个 JSON:
jsonfile = os.path.join(workdir, "app.jsonl")
logger.add(jsonfile, serialize=True, level="INFO")
logger.bind(user="u-17", action="checkout").info("下单成功")
logger.complete()
with open(jsonfile, encoding="utf-8") as fh:
lines = fh.read().strip().splitlines()
print("行数:", len(lines))
print("最后一行:", lines[-1])
行数: 1
最后一行: {"text": "2026-11-10 14:08:55.785 | INFO | __main__:<module>:52 - 下单成功\n", "record": {"elapsed": {"repr": "0:00:00.040262", "seconds": 0.040262}, "exception": null, "extra": {"user": "u-17", "action": "checkout"}, "file": {"name": "loguru_demo.py", "path": "D:\\ai\\cache\\temp\\work\\loguru_demo.py"}, "function": "<module>", "level": {"icon": "ℹ️", "name": "INFO", "no": 20}, "line": 52, "message": "下单成功", "module": "loguru_demo", "name": "__main__", "process": {"id": 22592, "name": "MainProcess"}, "thread": {"id": 42184, "name": "MainThread"}, "time": {"repr": "2026-11-10 14:08:55.785878+08:00", "timestamp": 1794290935.785878}}}
bind() 传进去的 user 和 action 落在 record.extra 里,采集端按字段取就能用,不用解析 text。time 带时区偏移,process/thread 也都在里面。
常用 API 与参数
| 名称 | 作用 | 要点 |
|---|---|---|
logger.add(sink, ...) | 注册输出 | sink 可以是路径、文件对象、标准流、函数、协程 |
rotation= | 轮转 | 支持 "10 MB"、"1 week"、"12:00" |
retention= | 清理旧文件 | 按数量或时长,如 3、"10 days" |
compression= | 归档压缩 | "zip"、"gz",在关闭文件时执行 |
format= | 格式串 | 花括号字段:{time}、{level}、{name}、{line}、{extra[x]} |
serialize=True | 输出 JSON 行 | 整条 record 一起序列化 |
logger.bind() | 绑定上下文 | 关键字参数进 extra,可链式再 bind |
logger.contextualize() | 临时上下文 | 关键字参数在 with 块内生效,适合请求级字段 |
logger.catch | 捕获异常 | 装饰器或上下文管理器,异常记录后不再抛出 |
logger.remove(handler_id) | 移除 sink | remove() 不带参数清空全部,含默认的 stderr |
三个默认行为
第一个是默认 sink 指向 stderr,不是 stdout。文档写的是"pre-configured and outputs to stderr"。这在交互式终端里没差别,但用 subprocess 抓 stdout、或者 python app.py > out.txt 时,日志不会进那个文件。本机跑的时候能直接看到后果:重定向到同一个流之后,stderr 的内容最后才一起刷出来,顺序和代码里的调用顺序不一致。
14:08:55 | INFO | 这条走 stderr
14:08:55 | WARNING | WARNING 会出现
14:08:55 | INFO | 开始处理订单
14:08:55 | INFO | req-8f21 | 开始处理订单
对应的 stdout 只有 (上面这行是 stderr 输出,stdout 只有本行) 这一句。要么把 sink 显式指到 sys.stdout,要么采集端两个流都收。
第二个是与标准库 logging 互不相通。两个都配好之后,各写各的:
std INFO 标准库日志
标准库的 handler 与 loguru 互不相通
第三方库大多用标准库 logging,它们的日志不会出现在 loguru 的 sink 里。要互通得自己写桥:把 logging 的记录转给 loguru 用 InterceptHandler,反方向用 PropagateHandler。文档给了这两个类的完整写法,都是一二十行。
第三个是 @logger.catch 默认带诊断信息。它不只是打印回溯,还会把栈帧里的变量值画出来:
14:08:55 | ERROR | An error has been caught in function '<module>', process 'MainProcess' (22592), thread 'MainThread' (42184):
Traceback (most recent call last):
> File "D:\ai\cache\temp\work\loguru_demo.py", line 69, in <module>
print("返回值:", divide(1, 0))
└ <function divide at 0x0000018644B776A0>
File "D:\ai\cache\temp\work\loguru_demo.py", line 66, in divide
return a / b
│ └ 0
└ 1
ZeroDivisionError: division by zero
└ 0、└ 1 就是实参的值。开发期很好用,生产环境就可能把密码、token 打进日志。文档自己也标了提醒:diagnose=True 是默认值,可能泄露敏感数据。上生产前把 diagnose=False、backtrace=False 配上。
另外,被 @logger.catch 捕获后函数返回 None,异常不再向上抛:
返回值: None
程序继续往下走
主流程里这么用会把错误吞掉,需要让异常继续传播时加 reraise=True。
小结
日志库的分歧点不在写法,在"谁来收集"。loguru 把格式、轮转、序列化收进 add(),代价是与标准库生态隔开:用它的项目里,第三方库的日志要靠适配层才看得见。
三个默认值决定上线后的行为:sink 在 stderr、诊断信息默认开、多进程默认不安全(文档要求 enqueue=True 才保证多进程写入完整)。前两个改配置就行,第三个要改 add() 的参数,而且不加的话症状是日志偶发缺行或者串行错乱,很难归因。
数据来源与检索时间
检索时间 2026-11-10。版本、文件列表、JSON 记录与回溯文本为本机实测。
| 来源 | URL | 对应数据 |
|---|---|---|
| PyPI《loguru》项目页 | https://pypi.org/project/loguru/ | 包名、安装方式与当前版本 0.7.3 |
| loguru 官方文档《Overview》 | https://loguru.readthedocs.io/en/stable/overview.html | "预配置并输出到 stderr"的默认行为;add() 统一管理 sink/格式/级别/过滤;rotation/retention/compression 的取值;serialize=True 与 bind() 的结构化日志;@logger.catch 捕获异常;diagnose=True 为默认且可能泄露敏感数据的提醒;多进程需 enqueue=True;InterceptHandler/PropagateHandler 与标准库互通的写法 |
| 本机运行记录 | D:\ai\cache\temp\work\loguru_demo.py | 0.7.3;默认 stderr 与级别过滤;bind 的 extra 输出;2 KB 轮转产生的 4 个文件与 1160 字节;app.jsonl 的完整 record;@logger.catch 的回溯与返回值;标准库日志并排输出的结果 |
核验说明
- 虚拟环境
D:\ai\cache\temp\py-loguru,Python 3.11.9,pip install loguru装到 0.7.3。 - 真跑的示例:默认 sink、级别过滤、
bind与format里的{extra[...]}、文件 sink 配rotation="2 KB"+retention=3、serialize=True的 JSON 行、@logger.catch捕获除零、{name}:{function}:{line}格式、与标准库logging并排输出。 - 没测的部分:
compression(zip/gz)、contextualize、logger.patch、自定义级别、enqueue=True的多进程写入、InterceptHandler的实际对接、logger.parse()。这些文档里有完整示例,但没有在本机验证,文章里没有给性能或行为结论。 - 多进程那条是从文档结论转述的(文档明确写 "thread-safe by default... not multiprocess-safe, but you can enqueue"),本机只跑了单进程,没有复现丢日志的现象。