loguru 完全指南
最后更新:2026-04-22 标准库 logging 需要手动创建 Logger、Handler、Formatter 三层对象,配置繁琐。loguru 用一个全局 logger 对象统一所有操作,开箱即用。 loguru 只有一个全局 logger 对象(loguru.logger),所有输出目标(终端、文件、自定义 sink)通过 logger.add() 注册为 sink,每个 sink 独立配置级别、格式、轮转策略,logger.remove() 移除 sink。 默认 sink 是 sys.stderr,id 为 0。 默认输出格式: logg
最后更新:2026-04-22
官方文档:https://loguru.readthedocs.io/en/stable/
适用版本:loguru 0.7+(2026-05-08 核实)
1. 基础概念
为什么使用 loguru
标准库 logging 需要手动创建 Logger、Handler、Formatter 三层对象,配置繁琐。loguru 用一个全局 logger 对象统一所有操作,开箱即用。
| 对比维度 | logging | loguru |
|---|---|---|
| 初始配置 | 需要 basicConfig 或手动组装 Handler | from loguru import logger 即可使用 |
| 文件轮转 | 需要 RotatingFileHandler/TimedRotatingFileHandler | logger.add() 的 rotation 参数 |
| 异常追踪 | logging.exception() 打印 traceback |
logger.exception() 或 @logger.catch |
| 颜色支持 | 需要第三方库 | 终端自动着色 |
| 结构化日志 | 需要额外格式化 | serialize=True 直接输出 JSON |
| 多进程安全 | 需要 QueueHandler + QueueListener | enqueue=True |
核心设计
loguru 只有一个全局 logger 对象(loguru.logger),所有输出目标(终端、文件、自定义 sink)通过 logger.add() 注册为 sink,每个 sink 独立配置级别、格式、轮转策略,logger.remove() 移除 sink。
默认 sink 是 sys.stderr,id 为 0。
2. 快速开始
from loguru import logger
# 开箱即用,默认输出到 stderr(带颜色)
logger.debug("调试信息")
logger.info("服务已启动")
logger.warning("磁盘空间不足")
logger.error("数据库连接失败")
logger.critical("进程崩溃")
# 记录异常(自动附带 traceback)
try:
1 / 0
except ZeroDivisionError:
logger.exception("捕获到异常") # 等价于 logger.error + exc_info
默认输出格式:
2026-04-22 10:23:45.123 | DEBUG | __main__:<module>:5 - 调试信息
3. logger.add() — 注册 sink
logger.add() 是 loguru 最核心的 API,用于添加输出目标。
函数签名
logger.add(sink, *, level="DEBUG", format=<默认格式>, filter=None,
colorize=None, serialize=False, backtrace=True, diagnose=True,
enqueue=False, catch=True,
rotation=None, retention=None, compression=None,
delay=False, watch=False, mode="a", buffering=1, encoding="utf8",
errors="backslashreplace")
参数说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sink |
str / Path / 文件对象 / 可调用 / logging.Handler | 必填 | 输出目标。字符串/Path 视为文件路径;可调用则接收格式化后的字符串;logging.Handler 兼容标准库 |
level |
str / int | "DEBUG" |
该 sink 的最低输出级别 |
format |
str / 可调用 | 内置默认格式 | 日志格式模板,支持 {time} {level} {message} 等占位符;也可传入返回字符串的函数 |
filter |
str / 可调用 / dict | None |
过滤规则。字符串为模块名前缀过滤;可调用接收 record 返回 bool;dict 按模块名映射级别 |
colorize |
bool / None | None |
终端着色。None 时自动检测是否为 tty |
serialize |
bool | False |
True 时输出 JSON 字符串(结构化日志) |
backtrace |
bool | True |
异常时显示完整调用栈(超出当前帧之外的链式调用) |
diagnose |
bool | True |
异常时在 traceback 中显示变量值(生产环境建议关闭,防止敏感数据泄露) |
enqueue |
bool | False |
将日志写入队列后异步输出,实现多进程/多线程安全 |
catch |
bool | True |
sink 内部抛出异常时是否静默处理(避免日志错误影响主程序) |
rotation |
str / int / datetime.time / datetime.timedelta / 可调用 | None |
仅文件 sink 有效。触发轮转的条件,见下方说明 |
retention |
str / int / datetime.timedelta / 可调用 | None |
仅文件 sink 有效。保留旧日志文件的数量或时长 |
compression |
str / 可调用 | None |
仅文件 sink 有效。轮转后压缩旧文件,支持 "gz" "bz2" "xz" "zip" 等 |
delay |
bool | False |
True 时延迟到第一条日志写入时才创建文件 |
watch |
bool | False |
True 时监控文件是否被外部删除/移动,自动重建 |
mode |
str | "a" |
文件打开模式 |
buffering |
int | 1 |
缓冲策略,1 为行缓冲 |
encoding |
str | "utf8" |
文件编码 |
errors |
str | "backslashreplace" |
编码错误处理策略 |
返回值
logger.add() 返回一个整数 id,可传给 logger.remove(id) 来移除该 sink。
示例
from loguru import logger
# 移除默认 stderr sink
logger.remove(0)
# 添加终端 sink(仅 INFO 及以上)
logger.add(
sys.stderr,
level="INFO",
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
colorize=True,
)
# 添加文件 sink
file_id = logger.add(
"logs/app.log",
level="DEBUG",
rotation="100 MB", # 超过 100 MB 轮转
retention="30 days", # 保留 30 天
compression="gz", # 压缩旧文件
enqueue=True, # 异步写入(多进程安全)
backtrace=True,
diagnose=False, # 生产环境关闭变量展示
)
4. 格式化(format 占位符)
内置占位符
| 占位符 | 说明 | 示例 |
|---|---|---|
{time} |
当前时间(ISO 格式) | 2026-04-22T10:23:45.123456+0800 |
{time:格式} |
自定义时间格式(使用 strftime 指令) | {time:YYYY-MM-DD HH:mm:ss} |
{level} |
日志级别名称 | DEBUG |
{level.name} |
同上 | DEBUG |
{level.no} |
级别数值 | 10 |
{level.icon} |
级别图标(可在 add_level 中自定义) | 🐛 |
{message} |
日志内容 | 服务已启动 |
{name} |
logger 名称(模块的 __name__) |
myapp.service |
{function} |
调用函数名 | start_server |
{line} |
调用行号 | 42 |
{file} |
文件对象 | - |
{file.name} |
文件名 | service.py |
{file.path} |
文件完整路径 | /app/service.py |
{module} |
模块名(不含包前缀) | service |
{thread} |
线程对象 | - |
{thread.id} |
线程 ID | 140234567890 |
{thread.name} |
线程名 | MainThread |
{process} |
进程对象 | - |
{process.id} |
进程 ID | 12345 |
{process.name} |
进程名 | MainProcess |
{extra} |
通过 bind() 绑定的上下文字典 |
{"request_id": "abc"} |
{exception} |
异常信息(有异常时才有内容) | - |
颜色标签
format 字符串中可以嵌入颜色标签(仅终端 sink 有效):
<red>红色</red>
<green>绿色</green>
<yellow>黄色</yellow>
<blue>蓝色</blue>
<magenta>洋红</magenta>
<cyan>青色</cyan>
<white>白色</white>
<bold>粗体</bold>
<underline>下划线</underline>
<level>使用当前级别的颜色</level>
自定义 format 函数
def formatter(record):
# record 是包含所有字段的字典
fmt = "{time:HH:mm:ss} | {level} | {message}\n"
if record["exception"]:
fmt += "{exception}\n"
return fmt
logger.add(sys.stderr, format=formatter)
5. 日志级别
内置级别
| 级别名 | 数值 | 对应方法 |
|---|---|---|
TRACE |
5 | logger.trace() |
DEBUG |
10 | logger.debug() |
INFO |
20 | logger.info() |
SUCCESS |
25 | logger.success() |
WARNING |
30 | logger.warning() |
ERROR |
40 | logger.error() |
CRITICAL |
50 | logger.critical() |
loguru 比标准库多了 TRACE(比 DEBUG 更低)和 SUCCESS(介于 INFO 和 WARNING 之间)。
自定义级别
# add_level(name, no, color="", icon="")
logger.level("AUDIT", no=35, color="<yellow><bold>", icon="@")
# 使用自定义级别
logger.log("AUDIT", "用户 {} 登录", user_id)
logger.level() 参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
str | 必填 | 级别名称(全大写为惯例) |
no |
int | 必填 | 级别数值,决定过滤优先级 |
color |
str | "" |
颜色标签字符串 |
icon |
str | "" |
在 {level.icon} 占位符中显示的字符 |
6. 文件轮转(rotation)
rotation 参数决定何时创建新日志文件:
| rotation 值 | 含义 |
|---|---|
"500 MB" |
文件超过 500 MB 时轮转(支持 KB/MB/GB) |
"1 week" |
每周轮转(支持 second/minute/hour/day/week/month/year) |
"00:00" |
每天 00:00 轮转(datetime.time 格式字符串) |
datetime.timedelta(days=1) |
每 24 小时轮转 |
datetime.time(hour=0) |
每天午夜轮转 |
可调用 (message, file) -> bool |
自定义轮转逻辑,返回 True 则轮转 |
文件名支持 {time} 占位符,轮转后自动加时间戳区分:
logger.add("logs/app_{time:YYYY-MM-DD}.log", rotation="00:00")
# 生成:app_2026-04-21.log, app_2026-04-22.log ...
7. 文件保留(retention)
retention 参数决定旧日志文件保留多久:
| retention 值 | 含义 |
|---|---|
10 |
保留最近 10 个轮转文件 |
"1 month" |
保留最近 1 个月内的文件 |
datetime.timedelta(weeks=2) |
保留最近 2 周的文件 |
可调用 (files: list[Path]) -> None |
自定义清理逻辑 |
logger.add(
"logs/app.log",
rotation="1 day",
retention=7, # 最多保留 7 个历史文件
compression="gz",
)
8. 过滤器(filter)
字符串过滤(模块名前缀)
# 只输出来自 myapp 模块及其子模块的日志
logger.add("myapp.log", filter="myapp")
函数过滤
def only_errors(record):
return record["level"].no >= 40
logger.add("errors.log", filter=only_errors)
filter 函数接收 record 字典,常用字段:
| 字段 | 说明 |
|---|---|
record["level"].no |
级别数值 |
record["level"].name |
级别名称 |
record["name"] |
模块名(__name__) |
record["message"] |
日志消息字符串 |
record["extra"] |
bind() 绑定的上下文字典 |
record["exception"] |
异常信息(无异常时为 None) |
dict 过滤(按模块名映射级别)
logger.add(
sys.stderr,
filter={
"myapp": "DEBUG", # myapp 及子模块输出 DEBUG 以上
"myapp.db": "WARNING", # myapp.db 只输出 WARNING 以上(更严格)
"third_party": False, # 屏蔽该模块
"": "INFO", # 其他模块输出 INFO 以上
}
)
9. 上下文绑定(bind / contextualize)
bind() — 静态绑定
bind() 创建一个绑定了额外字段的新 logger 副本,原 logger 不受影响:
request_logger = logger.bind(request_id="abc-123", user_id=42)
request_logger.info("处理请求")
# 日志 extra 字段中包含 request_id 和 user_id
# 可在 format 中引用
logger.add(sys.stderr, format="{time} | {extra[request_id]} | {message}")
contextualize() — 上下文管理器绑定
适合在一个代码块内临时绑定上下文(如 FastAPI 请求处理中):
with logger.contextualize(request_id="abc-123"):
logger.info("进入请求处理")
process_request()
logger.info("请求处理完成")
# 退出 with 块后,request_id 自动清除
在异步场景下,contextualize() 基于 contextvars,天然隔离不同协程的上下文:
async def handle_request(request_id: str):
with logger.contextualize(request_id=request_id):
logger.info("开始处理")
await do_work()
logger.info("处理完成")
10. 异常捕获
logger.exception()
在 except 块中调用,自动附加当前异常的 traceback:
try:
result = risky_operation()
except Exception:
logger.exception("操作失败") # 相当于 logger.error + 自动获取 exc_info
@logger.catch — 装饰器
自动捕获并记录函数内未处理的异常:
@logger.catch
def my_function():
return 1 / 0 # 异常会被记录,然后重新抛出
# 也支持异步函数
@logger.catch
async def async_function():
...
logger.catch() 可作为装饰器也可作为上下文管理器:
with logger.catch():
risky_operation()
logger.catch() 参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
exception |
异常类 / 元组 | Exception |
捕获的异常类型 |
level |
str / int | "ERROR" |
记录异常时使用的级别 |
reraise |
bool | False |
记录后是否重新抛出异常 |
onerror |
可调用 | None |
异常发生时额外调用的回调函数 |
exclude |
异常类 / 元组 | None |
不捕获的异常类型(会直接传播) |
default |
any | None |
异常时函数的返回值(仅装饰器模式) |
message |
str | "An error has been caught..." |
日志消息前缀 |
11. 结构化日志(序列化)
serialize=True 时,每条日志输出为一行 JSON,适合对接 ELK、Loki 等日志收集系统:
logger.add("logs/structured.log", serialize=True)
logger.bind(user_id=42).info("用户登录")
输出:
{"text": "2026-04-22 10:23:45.123 | INFO | __main__:<module>:3 - 用户登录\n", "record": {"elapsed": {"repr": "...", "seconds": 0.1}, "exception": null, "extra": {"user_id": 42}, "file": {"name": "example.py", "path": "/app/example.py"}, "function": "<module>", "level": {"icon": "ℹ️", "name": "INFO", "no": 20}, "line": 3, "message": "用户登录", "module": "example", "name": "__main__", "process": {"id": 1234, "name": "MainProcess"}, "thread": {"id": 140234, "name": "MainThread"}, "time": {"repr": "2026-04-22 10:23:45.123456+08:00", "timestamp": 1745283825.123}}}
12. 多进程安全
使用 enqueue=True 时,日志写入先进入线程安全队列,由后台线程统一写入文件,避免多进程并发写入导致日志交错:
logger.add("logs/app.log", enqueue=True)
在 multiprocessing 场景下,子进程不继承父进程的 logger 配置。推荐模式:
from loguru import logger
import multiprocessing
def worker(queue):
# 子进程中重新配置 logger,将日志发送到队列
logger.remove()
logger.add(queue, serialize=True)
logger.info("子进程工作中")
if __name__ == "__main__":
queue = multiprocessing.Queue()
logger.add(queue, serialize=True) # 主进程监听队列
p = multiprocessing.Process(target=worker, args=(queue,))
p.start()
p.join()
13. 与标准库 logging 集成
已有项目大量使用标准库 logging(如 FastAPI、SQLAlchemy、uvicorn 的内部日志),可以用 InterceptHandler 将所有标准库日志重定向到 loguru:
import logging
from loguru import logger
class InterceptHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
# 将标准库 level 映射到 loguru level
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
# 找到正确的调用栈帧,使文件名/行号显示准确
frame, depth = logging.currentframe(), 0
while frame and (depth == 0 or 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()
)
# 接管所有标准库日志
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
# 或者只接管特定库
for name in ("uvicorn", "uvicorn.error", "uvicorn.access", "sqlalchemy.engine"):
logging.getLogger(name).handlers = [InterceptHandler()]
logging.getLogger(name).propagate = False
14. 在 FastAPI 中使用
# config/logging.py
import sys
import logging
from loguru import logger
class InterceptHandler(logging.Handler):
def emit(self, record):
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = logging.currentframe(), 0
while frame and (depth == 0 or 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())
def setup_logging(log_level: str = "INFO", json_logs: bool = False):
# 移除默认 sink
logger.remove()
# 终端输出
logger.add(
sys.stderr,
level=log_level,
colorize=True,
backtrace=True,
diagnose=not json_logs,
)
# 文件输出(生产环境)
if json_logs:
logger.add(
"logs/app.log",
level=log_level,
serialize=True,
rotation="1 day",
retention="30 days",
compression="gz",
enqueue=True,
diagnose=False,
)
# 接管 uvicorn/fastapi 标准库日志
for name in logging.root.manager.loggerDict:
logging.getLogger(name).handlers = [InterceptHandler()]
logging.getLogger(name).propagate = False
logging.root.handlers = [InterceptHandler()]
# main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from loguru import logger
from config.logging import setup_logging
@asynccontextmanager
async def lifespan(app: FastAPI):
setup_logging(log_level="INFO", json_logs=True)
logger.info("应用启动")
yield
logger.info("应用关闭")
app = FastAPI(lifespan=lifespan)
@app.middleware("http")
async def log_requests(request: Request, call_next):
with logger.contextualize(
path=request.url.path,
method=request.method,
client=request.client.host if request.client else None,
):
logger.info("收到请求")
response = await call_next(request)
logger.info("请求完成,状态码 {}", response.status_code)
return response
15. logger.opt() — 高级选项
logger.opt() 返回一个临时修改了选项的 logger,常用于框架内部:
logger.opt(depth=1).info("消息") # 调整调用栈深度,使文件名/行号指向调用者
logger.opt(exception=True).error("出错") # 强制附加当前异常信息
logger.opt(lazy=True).debug("耗时操作: {}", lambda: compute()) # 懒求值,级别不满足时不执行 lambda
logger.opt(colors=True).info("<red>带颜色</red>的消息") # 强制启用颜色标签解析
logger.opt(raw=True).info("原始字符串\n") # 绕过格式化直接输出
logger.opt(record=True).info("record={record}") # 将 record 字典注入消息
opt() 参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
exception |
bool / 异常元组 | False |
是否附加异常信息 |
record |
bool | False |
是否将 record 字典注入格式化字符串 |
lazy |
bool | False |
消息参数是否懒求值(参数为无参 lambda) |
colors |
bool | False |
是否解析颜色标签(即使 sink 不支持颜色也会剥离标签) |
raw |
bool | False |
绕过 format 模板,直接输出原始字符串 |
capture |
bool | True |
是否捕获当前局部变量(用于 diagnose) |
depth |
int | 0 |
调整调用栈查找深度(封装 logger 时使用) |
16. 最佳实践
项目统一配置入口
将 logger.remove() 和所有 logger.add() 集中在一个初始化函数中,在应用启动时调用一次。各模块只 from loguru import logger 直接使用,不做任何配置。
生产环境关闭 diagnose
diagnose=True 会在 traceback 中展示局部变量值,可能泄露密码、Token 等敏感信息。生产环境必须设置 diagnose=False。
文件 sink 始终开启 enqueue
即使当前是单进程,enqueue=True 也能避免 IO 阻塞影响主线程性能,代价极小。
用 bind() 传递业务上下文
不要在消息字符串中硬编码业务 ID,用 bind() 或 contextualize() 绑定,便于日志聚合系统按字段过滤:
# 不推荐
logger.info(f"处理订单 {order_id} 失败")
# 推荐
logger.bind(order_id=order_id).error("处理订单失败")
区分开发与生产配置
import os
def setup_logging():
logger.remove()
if os.getenv("ENV") == "production":
logger.add("logs/app.log", serialize=True, level="INFO",
rotation="1 day", retention="14 days",
enqueue=True, diagnose=False)
else:
logger.add(sys.stderr, level="DEBUG", colorize=True, diagnose=True)
17. 常见陷阱
格式化字符串用 {} 而非 %s 或 f-string
loguru 使用 str.format() 风格的懒格式化,直到日志被实际处理时才求值,减少无效格式化开销:
# 不推荐:f-string 立即求值,即使日志级别不满足也会执行字符串拼接
logger.debug(f"结果:{expensive_function()}")
# 推荐:只有级别满足时才调用 str.format()
logger.debug("结果:{}", expensive_function())
# 更进一步:用 opt(lazy=True) 完全避免求值
logger.opt(lazy=True).debug("结果:{}", lambda: expensive_function())
不要在子进程中直接使用父进程配置的 logger
fork 后子进程继承了父进程的 logger 状态(包括文件句柄),多进程同时写同一文件会导致日志交错。解决方案:使用 enqueue=True,或在子进程初始化时重新调用 logger.remove() + logger.add()。
rotation 与文件名模板配合
如果文件名不含 {time} 但同时设置了 rotation,轮转后旧文件会被自动添加时间戳重命名。若文件名已含 {time} 则新文件按时间命名,无需额外处理。
remove() 传参而非不传参
logger.remove() 不传参数会移除所有 sink(包括默认 stderr)。通常应传入 logger.add() 返回的 id:
stderr_id = logger.add(sys.stderr)
# ...
logger.remove(stderr_id) # 只移除这一个 sink
# 移除默认 sink
logger.remove(0) # 默认 stderr 的 id 是 0
Windows 多进程与 enqueue
在 Windows 上使用 multiprocessing + enqueue=True 时,需确保 logger 配置在 if __name__ == "__main__": 块内,否则子进程导入时会重复执行配置代码。
最佳实践
应用启动时移除默认 sink 再添加自定义配置:loguru 默认向 stderr 输出,生产环境应先 logger.remove() 清除默认配置再添加文件和结构化 sink,避免日志重复输出。
用 serialize=True 输出结构化 JSON:生产环境日志要便于机器解析,serialize=True 将每条日志序列化为 JSON 行,直接接入 ELK、Grafana Loki 等日志系统:
logger.add("app.log", serialize=True, rotation="100 MB")
按日志级别分文件输出:错误日志单独文件便于告警和排查:
logger.add("app.log", level="INFO", rotation="1 day")
logger.add("error.log", level="ERROR", rotation="1 week", retention="30 days")
在 FastAPI/异步应用中用 enqueue=True:异步环境下 file IO 是同步阻塞,enqueue=True 将日志写入队列,由独立线程处理,不阻塞事件循环。
用 logger.contextualize() 绑定请求上下文:在中间件中绑定 request_id、user_id 等字段,该请求所有日志自动携带这些信息,无需每次手动传参。
常见陷阱
陷阱:多次调用 logger.add() 导致日志重复输出
现象: 每条日志输出两次或更多次。
原因: 每次导入模块或函数被多次调用时重复 logger.add(),而 loguru 的 sink 是累积的。
解决: 在应用启动入口集中配置一次,可用 logger.remove() + 单次 logger.add();或用 if not logger._core.handlers: 守卫。
陷阱:loguru 与 logging 第三方库日志割裂
现象: FastAPI、SQLAlchemy 等库的日志通过 logging 模块输出,与 loguru 的格式不一致,难以集中管理。
原因: 第三方库使用标准 logging 模块,loguru 默认不接收它们的日志。
解决: 用 loguru.logger 的 PropagateHandler 将标准 logging 的日志转发到 loguru:
import logging
from loguru import logger
class InterceptHandler(logging.Handler):
def emit(self, record):
level = logger.level(record.levelname).name
logger.opt(depth=6, exception=record.exc_info).log(level, record.getMessage())
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
陷阱:rotation 参数字符串格式错误静默失效
现象: 设置 rotation="500 mb" 后日志文件不轮转。
原因: loguru 的 rotation 大小写敏感,接受 "500 MB" 但不接受 "500 mb"(小写)。
解决: 始终用大写单位("100 MB"、"1 GB"),或用时间格式("1 day"、"00:00")。