第三方 Python 库 Loguru 的进阶实战
本文是 Loguru 系列的第二篇。如果你还没读过基础篇,建议先去看看。
基础篇解决了"是什么、怎么用"的问题,这篇要解决的是"怎么用好、怎么在项目中落地"。
系列文章导航:
- 【上篇】第三方 Python 库 Loguru 的基础使用
- 【下篇】第三方 Python 库 Loguru 的进阶实战(本文)
一、回顾:基础篇我们学了什么
在上一篇中,我们掌握了 Loguru 的基础用法:一行导入、日志级别、文件输出、轮转/保留/压缩、自定义格式、异常捕获。
这些功能已经能覆盖日常开发 80% 的需求。但当你开始写正式项目,尤其是 Web 应用、微服务、多模块项目时,你会遇到一些新的问题:
- 怎么在日志里带上用户 ID、请求 ID 这些上下文信息?
- 怎么让不同模块的日志输出到不同文件?
- 怎么和已有的标准
logging配置共存? - 多进程写同一个日志文件会不会出问题?
- 生产环境的最佳配置是什么?
这篇就来解决这些问题。
二、上下文绑定:让日志带上"身份证"
在多用户、多请求的场景下,你可能想在日志里带上用户 ID、请求 ID 等上下文信息。排查问题时,能按请求维度把日志串起来,效率会高很多。
2.1 bind():静态绑定
bind() 会返回一个新的 logger 实例,后续使用这个实例输出的日志都会带上绑定的信息:
python
from loguru import logger
logger.add("app.log", format="{extra[user_id]} {extra[ip]} {message}")
# 绑定上下文,返回新的 logger
user_logger = logger.bind(user_id=1001, ip="192.168.1.100")
user_logger.info("用户登录")
user_logger.info("查看订单")
# 可以再 bind,不影响之前的实例
admin_logger = logger.bind(user_id=9999, role="admin")
admin_logger.success("执行管理员操作")
你也可以内联 bind,一行搞定:
python
logger.bind(user_id=12345).info("快速绑定")
2.2 contextualize():线程级上下文管理
contextualize() 是一个上下文管理器,适合在一个作用域内临时添加上下文信息。它最大的优势是线程隔离------在多线程场景下,每个线程的上下文互不干扰:
python
from loguru import logger
import threading
logger.add("app.log", format="{extra[request_id]} {message}")
def handle_request(request_id):
with logger.contextualize(request_id=request_id):
logger.info("开始处理请求")
do_something()
logger.info("请求处理完毕")
# 离开 with 块后,request_id 自动清除
# 多线程并发,每个线程的 request_id 是隔离的
for i in range(5):
threading.Thread(target=handle_request, args=(f"req-{i}",)).start()
2.3 patch():动态绑定
patch() 通过函数动态生成上下文信息,适合每次日志调用都需要不同值的场景:
python
from loguru import logger
import uuid
logger.add("app.log", format="{extra[req_id]} {message}")
# 每次调用日志方法时,都会执行这个函数生成 req_id
def generate_req_id(record):
record["extra"]["req_id"] = str(uuid.uuid4())[:8]
req_logger = logger.patch(generate_req_id)
# 每条日志的 req_id 都不同
req_logger.info("处理用户请求")
req_logger.error("请求参数错误")
2.4 bind() vs contextualize() vs patch() 怎么选?
| 方法 | 适用场景 | 返回值 | 线程安全 |
|---|---|---|---|
bind() |
长期绑定固定的上下文 | 新的 logger 实例 | 是 |
contextualize() |
临时作用域内的上下文 | 上下文管理器 | 是(线程隔离) |
patch() |
每次调用都需要动态值 | 新的 logger 实例 | 是 |
简单记:固定值用 bind(),临时用 contextualize(),每次不同的用 patch()。
三、日志过滤的高级用法
基础篇简单介绍了 filter 参数,这里深入讲讲。
3.1 用字典按模块过滤
python
from loguru import logger
# my_module 模块只记录 INFO 以上,other_module 只记录 WARNING 以上
logger.add("app.log", filter={"my_module": "INFO", "other_module": "WARNING"})
3.2 用函数做灵活过滤
字典过滤比较简单,实际项目中往往需要更灵活的条件:
python
from loguru import logger
def payment_filter(record):
"""只保留支付相关的日志,或者 ERROR 以上的日志"""
is_payment = "payment" in record["extra"].get("module", "")
is_error = record["level"].no >= logger.level("ERROR").no
return is_payment or is_error
logger.add("payment.log", filter=payment_filter)
# 测试
logger.bind(module="payment").info("支付发起") # 会记录
logger.bind(module="user").info("用户注册") # 不会记录
logger.error("系统错误") # 会记录(ERROR 级别)
3.3 结合 bind 和 filter 实现日志分流
一个常见的需求是:不同模块的日志写到不同文件。用 bind() + filter() 可以轻松实现:
python
from loguru import logger
# 用户模块的日志
logger.add(
"logs/user.log",
filter=lambda r: r["extra"].get("module") == "user",
level="INFO"
)
# 订单模块的日志
logger.add(
"logs/order.log",
filter=lambda r: r["extra"].get("module") == "order",
level="INFO"
)
# 错误日志单独收集
logger.add(
"logs/error.log",
filter=lambda r: r["level"].no >= logger.level("ERROR").no,
level="ERROR"
)
# 使用时绑定模块名
logger.bind(module="user").info("用户登录成功")
logger.bind(module="order").info("订单创建成功")
logger.bind(module="order").error("订单支付失败")
四、结构化日志(JSON 格式)
在微服务架构中,日志经常需要被 ELK、Grafana Loki、Datadog 等系统收集和分析。这时候 JSON 格式的结构化日志就非常重要:
python
from loguru import logger
# serialize=True 会让每条日志变成 JSON 字符串
logger.add("app.json", serialize=True, format="{message}")
logger.info("用户登录", user_id=12345, ip="192.168.1.100")
输出到文件的内容大概是这样的(格式化后):
json
{
"text": "用户登录\n",
"record": {
"elapsed": {"repr": "0:00:01.234567", "seconds": 1.234567},
"exception": null,
"extra": {"user_id": 12345, "ip": "192.168.1.100"},
"file": {"name": "app.py", "path": "/code/app.py"},
"function": "<module>",
"level": {"name": "INFO", "no": 20},
"line": 5,
"message": "用户登录",
"module": "app",
"time": {"repr": "2026-06-09 10:30:15.123456+08:00"}
}
}
JSON 格式的好处是机器可读,方便后续的搜索、过滤、聚合分析。
五、懒求值:避免不必要的性能开销
有些日志信息的获取成本很高,比如要查数据库、调接口。如果日志级别不满足条件(比如生产环境只开 INFO),这些计算就白做了。
python
from loguru import logger
def expensive_query():
"""模拟一个耗时的数据库查询"""
import time
time.sleep(2)
return {"user_count": 10000}
# 不用 lazy:即使 DEBUG 日志不输出,expensive_query() 也会执行
logger.debug("数据库统计: {}", expensive_query())
# 用 lazy=True:lambda 只在日志真正被输出时才执行
logger.opt(lazy=True).debug("数据库统计: {result}", result=lambda: expensive_query())
opt() 方法还有很多其他用途:
python
# 在日志中添加异常堆栈(不用在 except 块里)
logger.opt(exception=True).info("错误信息已添加到日志")
# 启用颜色标记(单条日志级别)
logger.opt(colors=True).info("这是 <blue>蓝色</blue> 文字")
# 输出原始内容,绕过 sink 的格式化
logger.opt(raw=True).info("原始内容,不经过格式化\n")
# 显示 record 信息
logger.opt(record=True).info("当前线程: {record[thread]}")
# 设置调用栈深度(在封装函数中很有用)
logger.opt(depth=1).info("这条日志显示的是调用者的文件和行号")
六、与标准 logging 互操作
如果你的项目已经在用标准 logging,或者你用的第三方库(比如 uvicorn、sqlalchemy、django)用的是标准 logging,Loguru 也能无缝对接。
6.1 把标准 logging 的消息转发到 Loguru
这是最常见的场景------让所有第三方库的日志都走 Loguru:
python
import logging
import inspect
from loguru import logger
class InterceptHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
try:
level: str | int = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = inspect.currentframe(), 0
while frame:
filename = frame.f_code.co_filename
is_logging = filename == logging.__file__
is_frozen = "importlib" in filename and "_bootstrap" in filename
if depth > 0 and not (is_logging or is_frozen):
break
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
# 一行配置,拦截所有标准 logging 的输出
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
这样配置后,uvicorn、sqlalchemy 等库的日志都会通过 Loguru 输出,统一格式、统一管理。
6.2 把 Loguru 的消息转发到标准 logging
反过来也行:
python
import logging
from loguru import logger
class PropagateHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
logging.getLogger(record.name).handle(record)
logger.add(PropagateHandler(), format="{message}")
6.3 把标准 logging 的 Handler 作为 Loguru 的 sink
python
import logging.handlers
from loguru import logger
handler = logging.handlers.SysLogHandler(address=('localhost', 514))
logger.add(handler)
七、异步与多进程安全
7.1 线程安全
Loguru 的所有 sink 默认就是线程安全的,不需要额外处理。多线程环境下放心用。
7.2 多进程安全
如果你用了 multiprocessing,多个进程同时写同一个日志文件可能会出现日志交错或丢失。加上 enqueue=True 就行:
python
from loguru import logger
# enqueue=True 使用队列来保证多进程安全
logger.add("app.log", enqueue=True)
enqueue=True 会在内部使用一个线程安全的队列,所有日志消息先入队,再由单独的线程写入。这也意味着日志写入变成了异步的,不会阻塞主程序。
7.3 异步支持
Loguru 也支持异步日志,sink 可以是一个协程函数:
python
from loguru import logger
import asyncio
async def async_sink(message):
await asyncio.sleep(0.1)
with open("async.log", "a") as f:
f.write(str(message))
logger.add(async_sink)
# 使用完后需要等待所有异步 sink 完成
await logger.complete()
八、在库中使用 Loguru
如果你在写一个 Python 库(给别人用的),Loguru 有一套最佳实践:
python
# my_library/core.py
from loguru import logger
# 库默认禁用日志输出,避免污染用户的日志
logger.disable("my_library")
def do_something():
logger.info("库内部的操作日志") # 默认不会输出
# ...
python
# 用户的代码
from loguru import logger
# 用户想看你库的日志?一行搞定
logger.enable("my_library")
from my_library import core
core.do_something() # 现在能看到库的日志了
关键原则:
- 库中不要调用
logger.add(),这会干扰用户的日志配置 - 用
disable()/enable()来控制日志的开关 disable()的参数是库的模块名,和__name__对应
九、实战案例
9.1 Web 应用的完整日志配置
这是一个比较完整的、可以直接用在生产环境的日志配置方案:
python
from loguru import logger
import sys
def setup_logger():
# 移除默认输出
logger.remove()
# 控制台输出:带颜色,开发友好
logger.add(
sys.stdout,
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
"<level>{message}</level>",
level="DEBUG",
colorize=True
)
# 服务日志文件:INFO 以上,按天轮转
logger.add(
"logs/app_{time:YYYY-MM-DD}.log",
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | "
"{name}:{function}:{line} | {extra[request_id]} | {message}",
level="INFO",
rotation="00:00",
retention="30 days",
compression="gz",
encoding="utf-8",
enqueue=True,
backtrace=False,
diagnose=False,
)
# 错误日志单独文件:ERROR 以上,保留更久
logger.add(
"logs/error_{time:YYYY-MM-DD}.log",
level="ERROR",
rotation="00:00",
retention="90 days",
compression="zip",
encoding="utf-8",
enqueue=True,
backtrace=True,
diagnose=True, # 开发环境可以开,生产环境建议关闭
)
setup_logger()
这个配置实现了三层日志:
- 控制台:DEBUG 以上,带颜色,开发调试用
- 服务日志文件:INFO 以上,按天轮转,保留 30 天,gzip 压缩
- 错误日志文件:ERROR 以上,保留 90 天,带完整的异常追踪信息
9.2 与 FastAPI 集成
python
from fastapi import FastAPI, Request
from loguru import logger
import time
import uuid
app = FastAPI()
# 配置日志(调用上面的 setup_logger)
setup_logger()
@app.middleware("http")
async def log_requests(request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())[:8])
start_time = time.time()
with logger.contextualize(request_id=request_id):
logger.info(f"开始处理 {request.method} {request.url.path}")
response = await call_next(request)
duration = time.time() - start_time
logger.info(
f"完成 {request.method} {request.url.path} "
f"状态码={response.status_code} 耗时={duration:.3f}s"
)
return response
9.3 多模块项目中的日志管理
在实际项目中,代码通常会分成多个模块。推荐的做法是提供一个统一的 logger 工具:
python
# utils/logger.py
from loguru import logger
import sys
# 统一的日志配置
logger.remove()
logger.add(sys.stdout, level="DEBUG", colorize=True,
format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | "
"<cyan>{extra[module]}</cyan> | {message}")
logger.add("logs/app.log", level="INFO", rotation="50 MB", retention="7 days")
def get_logger(module_name: str):
"""获取带模块标识的 logger"""
return logger.bind(module=module_name)
python
# services/user_service.py
from utils.logger import get_logger
logger = get_logger("用户服务")
def login(username: str):
logger.info(f"用户 {username} 尝试登录")
# ... 业务逻辑
logger.success(f"用户 {username} 登录成功")
python
# services/order_service.py
from utils.logger import get_logger
logger = get_logger("订单服务")
def create_order(user_id: int):
logger.info(f"用户 {user_id} 创建订单")
# ... 业务逻辑
这样每条日志都会带上模块名称,排查问题时一目了然。
9.4 用 apprise 实现告警通知
Loguru 可以和 apprise 库配合,在发生错误时自动发送通知(邮件、钉钉、Slack、Discord 等):
python
import apprise
from loguru import logger
# 配置通知渠道
notifier = apprise.Apprise()
notifier.add("discord://webhook_id/webhook_token")
# notifier.add("mailto://user:pass@smtp.example.com?to=alert@example.com")
# ERROR 级别的日志自动发送通知
logger.add(notifier.notify, level="ERROR", filter={"apprise": False})
# 当发生错误时,会自动通知
logger.error("支付服务异常!请立即处理!")
9.5 自定义 Sink:日志写入数据库
sink 不仅支持文件路径,还可以是函数或类实例。比如把日志写入 SQLite:
python
from loguru import logger
import sqlite3
# 初始化数据库
conn = sqlite3.connect("log.db")
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS logs (time TEXT, level TEXT, message TEXT)")
conn.commit()
# 自定义 sink 函数
def db_sink(record):
time = record["time"].strftime("%Y-%m-%d %H:%M:%S")
level = record["level"].name
message = record["message"]
cursor.execute("INSERT INTO logs (time, level, message) VALUES (?, ?, ?)", (time, level, message))
conn.commit()
# 添加自定义 sink
logger.add(db_sink, level="INFO")
# 日志会同时输出到控制台和数据库
logger.info("数据库日志测试")
你甚至可以把日志发送到消息队列(Redis、RabbitMQ)、写入 Elasticsearch 等,sink 的灵活性是 Loguru 的一大优势。
9.6 全局配置:logger.configure()
如果你喜欢一次性把所有配置写完,可以用 logger.configure():
python
import sys
from loguru import logger
logger.configure(
handlers=[
# 控制台输出
{
"sink": sys.stderr,
"level": "INFO",
"format": "<green>{time}</green> | <level>{message}</level>",
},
# 文件输出
{
"sink": "app.log",
"level": "DEBUG",
"format": "{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
"rotation": "1 day",
"retention": "7 days",
"compression": "gz",
},
],
extra={"app_name": "my_app"}, # 全局 extra 字段
)
logger.info("配置完成") # 会带上 extra 中的 app_name
十、性能注意事项
- 懒求值 :对耗时的日志参数使用
opt(lazy=True),避免不必要的计算 - 生产环境级别 :生产环境把级别设为
INFO或更高,减少日志量 - 异步写入 :使用
enqueue=True让日志写入异步化,不阻塞主程序 - 序列化开销 :
serialize=True会有额外的 JSON 序列化开销,非必要不开启 - diagnose 关闭 :生产环境关闭
diagnose=True,避免敏感信息泄露和额外开销
十一、常见问题排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 控制台出现两条重复日志 | 没有 logger.remove() 就直接 add() |
先 logger.remove() 再 add() |
| 中文乱码 | 文件编码问题 | logger.add(..., encoding="utf-8") |
TRACE 级别日志不显示 |
默认不显示 | logger.add(sys.stderr, level="TRACE") |
| 日志轮转不生效 | rotation 参数格式错误 | 检查格式:"500 MB" 带空格,"00:00" 是字符串 |
| 异常日志没有堆栈 | 用了 logger.error() 而不是 logger.exception() |
改用 logger.exception() 或 @logger.catch |
| 多进程日志丢失 | 没有设置 enqueue=True |
logger.add("app.log", enqueue=True) |
| 格式化颜色不生效 | format 里没有颜色标签 | 确保 format 包含 <green> 等标签或 <level> |
十二、总结
这篇进阶篇覆盖了 Loguru 在实际项目中的核心用法:
- 上下文绑定 :
bind()、contextualize()、patch()三种方式,让日志带上业务上下文 - 高级过滤:用函数实现灵活的日志分流
- 结构化日志:JSON 格式,对接 ELK 等日志系统
- 懒求值:避免不必要的性能开销
- 兼容标准 logging:拦截第三方库的日志
- 多进程安全 :
enqueue=True一行搞定 - 库中使用 :
disable()/enable()模式 - 实战配置:Web 应用、FastAPI、多模块项目、告警通知、自定义 Sink
结合基础篇和这篇进阶篇,你已经掌握了 Loguru 从入门到实战的完整知识体系。剩下的就是在实际项目中多用、多练了。
延伸阅读
- Loguru 官方文档 ------ 最权威的参考资料
- Loguru Recipes ------ 各种使用场景的配方
- Loguru GitHub Issues ------ 遇到问题先搜这里
- apprise ------ 多渠道通知库,和 Loguru 配合使用