第三方 Python 库 Loguru 的进阶实战

第三方 Python 库 Loguru 的进阶实战

本文是 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,或者你用的第三方库(比如 uvicornsqlalchemydjango)用的是标准 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)

这样配置后,uvicornsqlalchemy 等库的日志都会通过 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()

这个配置实现了三层日志:

  1. 控制台:DEBUG 以上,带颜色,开发调试用
  2. 服务日志文件:INFO 以上,按天轮转,保留 30 天,gzip 压缩
  3. 错误日志文件: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

十、性能注意事项

  1. 懒求值 :对耗时的日志参数使用 opt(lazy=True),避免不必要的计算
  2. 生产环境级别 :生产环境把级别设为 INFO 或更高,减少日志量
  3. 异步写入 :使用 enqueue=True 让日志写入异步化,不阻塞主程序
  4. 序列化开销serialize=True 会有额外的 JSON 序列化开销,非必要不开启
  5. 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 在实际项目中的核心用法:

  1. 上下文绑定bind()contextualize()patch() 三种方式,让日志带上业务上下文
  2. 高级过滤:用函数实现灵活的日志分流
  3. 结构化日志:JSON 格式,对接 ELK 等日志系统
  4. 懒求值:避免不必要的性能开销
  5. 兼容标准 logging:拦截第三方库的日志
  6. 多进程安全enqueue=True 一行搞定
  7. 库中使用disable() / enable() 模式
  8. 实战配置:Web 应用、FastAPI、多模块项目、告警通知、自定义 Sink

结合基础篇和这篇进阶篇,你已经掌握了 Loguru 从入门到实战的完整知识体系。剩下的就是在实际项目中多用、多练了。


延伸阅读

相关推荐
码云骑士2 小时前
32-慢查询排查全流程(下)-索引优化实战与最左前缀原则
python
闵孚龙2 小时前
《PyTorch 深度修炼》Dataset 和 DataLoader:数据如何喂给模型
人工智能·pytorch·python
goldenrolan2 小时前
A公司物料替代测试系统 v1.7:从需求到 exe/apk 的 AI 辅助全链路实践
android·自动化测试·软件测试·python·ai
菜板春3 小时前
jupyter入门-手册-特征探索
python·jupyter
Metaphor6923 小时前
使用 Python 将 PDF 转换为 HTML
python·pdf·html
极光代码工作室3 小时前
基于数据仓库的电商数据分析平台
大数据·hadoop·python·spark·数据可视化
开发小能手-roy3 小时前
StringBuilder vs StringBuffer:2024年还需要线程安全字符串吗?
开发语言·python·安全
AC赳赳老秦4 小时前
用 OpenClaw 搭建服务器故障应急响应系统,自动处理 80% 常见运维故障
android·运维·服务器·python·rxjava·deepseek·openclaw
2601_954706494 小时前
云手机技术详解+Python实战调用|2026高稳云手机平台推荐
开发语言·python·智能手机