跳转至

Python 语言进阶 —— 从 logging 到 loguru

认识日志

入门 Python 学习的第一个程序就是 Hello World:

print('Hello World!')

通过使用 print 语句输出 Hello World ,这就是一个最简单的日志记录。在软件开发中,日志记录是至关重要的一环,帮忙开发者、运维者了解程序是否在正常运行。通过分析日志,我们可以了解系统的运行状态,及时发现潜在问题,防患于未然。甚至日志可以记录系统的操作行为,帮助我们更好地理解用户需求,优化产品体验。

日志框架

在 Python 生态系统中,日志记录框架的选择丰富多样,每种框架都有其独特的优势和适用场景。以下介绍几种常用的日志框架,并对它们的优劣进行分析:

框架 优点 缺点 使用场景
logging 1. 无需额外安装 2. 功能强大 3. 灵活度高 1. 配置复杂 2. 功能有限(不支持异步、结构化日志等) 3. 代码冗余 1. 简单的日志记录需求 2. 依赖 Python 标准库的项目
loguru 1. 简洁易用 2. 功能强大(支持异步、结构化日志、彩色输出等) 3. 代码简洁 1. 需要额外安装 2. 灵活性相对较低 1. 需要快速上手的项目 2. 对日志功能要求较高的项目
structlog 1. 专注于结构化日志记录 2. 灵活度高(易于扩展和集成) 1. 学习成本较高 2. 配置复杂 1. 需要结构化日志记录的项目 2. 需要与其他日志框架集成的场景
Sentry 1. 错误追踪和日志记录结合 2. 实时监控和告警功能 3. 集成方便(支持多种语言和框架) 1. 需要额外安装和配置 2. 免费版功能有限,费用较高 1. 需要错误追踪和监控的项目 2. 中大型项目或对稳定性要求高的场景

其中,loggingloguru 在 Python 项目中使用得最为广泛。因此,本文着重介绍这两个库。

logging 模块

Python 语言在标准库中,提供了强大的日志记录标准库:logging。因为 logging 属于 Python 标准库中的一部分,无需安装,所以导入包就可以直接使用。

import logging

logging.debug('Hello World!')

核心概念

logging 模块的设计文档[1][2][3] 中提到 logging 模块的设计基于以下这些核心概念:

  • 日志器 Logger:Logger 是日志记录的主体,负责产生日志消息。每个 Logger 都有一个名称,通常使用模块名作为名称(如 __name__),以便区分不同模块的日志。
  • 处理器 Handler:Handler 负责将日志消息发送到指定的目的地,例如控制台、文件、网络等。一个 Logger 可以添加多个 Handler。
  • 格式化器 Formatter:Formatter 用于定义日志消息的输出格式,例如时间、日志级别、模块名、消息内容等。
  • 过滤器 Filter:Filter 用于对日志消息进行过滤,只有满足条件的日志才会被记录。
  • 日志级别 Log Level:logging 定义了6 种日志级别,从低到高分别是:
    • DEBUG:调试信息,用于开发阶段。
    • INFO:常规信息,用于记录程序运行状态。
    • WARNING:警告信息,表示潜在的问题。
    • ERROR:错误信息,表示程序出现错误。
    • CRITICAL:严重错误信息,表示程序可能无法继续运行。

快速入门

import logging

# 创建 Logger
logger = logging.getLogger(__name__)
# 设置日志级别
logger.setLevel(logging.DEBUG)

# 创建 Handler(输出到控制台)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)

# 创建 Formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 将 Formatter 添加到 Handler
stream_handler.setFormatter(formatter)

# 将 Handler 添加到 Logger
logger.addHandler(stream_handler)

# 记录日志
logger.debug('这是一条调试信息')
logger.info('这是一条常规信息')
logger.warning('这是一条警告信息')
logger.error('这是一条错误信息')
logger.critical('这是一条严重错误信息')

在使用 logging 模块的时候,通过 logging.getLogger(__name__) 为每个模块创建一个独立的日志记录器,这样做的好处有两点:

  • 区分日志来源:日志输出中会包含模块名称,方便定位日志的来源。
  • 模块化配置:可以为不同的模块设置不同的日志级别、Handler 或 Formatter。

如果直接使用 logging.getLogger()而不传递名称,会返回根日志记录器(Root Logger):

  • 根日志记录器是全局的,所有模块共享同一个日志记录器。
  • 这样会导致日志输出难以区分来源,且配置可能会相互覆盖。

也就是说通过 logging.getLogger(__name__),可以避免全局冲突。

上述案例中,创建一个输出到控制台的 StreamHandler 对象,用于将日志定向输出到控制台。还创建一个 Formatter 对象对输出到控制的日志信息进行格式化。最后,将 Handler 添加到日志记录器中,完成一个自定义的日志输出的设计。

文件配置

logging 模块支持多种配置方式,除了上述使用的代码方式配置日志记录器(Logger)、处理器(Handler)、格式化器(Formatter)等组件以外,还支持使用文件的方式进行配置。文件格式支持 JSON、YAML 或字典的形式配置日志。通过文件配置的方式非常适合复杂的日志配置需求,因为它可以将配置与代码分离,使配置更易于管理和维护。

下表是这三种方式的优缺点对比:

格式 优点 缺点
JSON 1. 结构化数据,易于解析 2. 适合复杂配置 1. 可读性较差 2. 需要额外文件
YAML 1. 可读性高 2. 适合配置文件 3. 支持注释 1. 需要安装 pyyaml 库 2. 对缩进敏感,容易出错
字典 1. 无需额外文件 2. 适合直接在代码中定义配置 1. 配置与代码耦合 2. 不适合复杂配置

JSON 格式

JSON 配置文件通常包含以下部分:

  • version:配置版本,必须为 1
  • formatters:定义日志的格式化器。
  • handlers:定义日志的处理器(如输出到控制台、文件等)。
  • loggers:定义日志记录器及其配置。
  • root:根日志记录器的配置(可选)。

以下是一个完整的 JSON 配置文件示例:

{
  "version": 1,
  "disable_existing_loggers": false,
  "formatters": {
    "simple": {
      "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    },
    "json": {
      "format": {
        "timestamp": "%(asctime)s",
        "name": "%(name)s",
        "level": "%(levelname)s",
        "message": "%(message)s"
      },
      "class": "pythonjsonlogger.jsonlogger.JsonFormatter"
    }
  },
  "handlers": {
    "console": {
      "class": "logging.StreamHandler",
      "level": "DEBUG",
      "formatter": "simple",
      "stream": "ext://sys.stdout"
    },
    "file": {
      "class": "logging.handlers.RotatingFileHandler",
      "level": "INFO",
      "formatter": "json",
      "filename": "app.log",
      "maxBytes": 10485760,
      "backupCount": 5,
      "encoding": "utf8"
    }
  },
  "loggers": {
    "my_module": {
      "level": "DEBUG",
      "handlers": [
        "console",
        "file"
      ],
      "propagate": false
    }
  },
  "root": {
    "level": "WARNING",
    "handlers": [
      "console"
    ]
  }
}

上述配置的字段含义如下:

  1. formatters 定义日志的格式化器:

  2. simple:使用简单的文本格式。

  3. json:使用 JSON 格式输出日志,需要安装 python-json-logger 库。
  4. handlers 定义日志的处理器:

  5. console:将日志输出到控制台。

  6. file:将日志输出到文件,并支持文件轮转(RotatingFileHandler)。
  7. loggers 定义模块级别的日志记录器:

  8. my_module:为 my_module 模块配置日志记录器,输出到控制台和文件。

  9. root 定义根日志记录器的配置。所有未明确配置的日志记录器会继承根日志记录器的配置。

在代码中,通过下面的方式加载配置文件:

import logging.config
import json

# 假如文件的名字为 logging_config.json,加载 JSON 配置文件
with open('logging_config.json', 'r') as f:
    config = json.load(f)

logging.config.dictConfig(config)

# 获取日志记录器
logger = logging.getLogger('my_module')

# 记录日志
logger.debug('This is a debug message')
logger.info('This is an info message')
logger.warning('This is a warning message')
logger.error('This is an error message')

YAML 格式

YAML 是一种易读的数据序列化格式,适合用于配置文件。以下是将上面 JSON 配置修改成 YAML 格式:

version: 1
disable_existing_loggers: false
formatters:
  simple:
    format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
  json:
    format:
      timestamp: "%(asctime)s"
      name: "%(name)s"
      level: "%(levelname)s"
      message: "%(message)s"
    class: "pythonjsonlogger.jsonlogger.JsonFormatter"
handlers:
  console:
    class: "logging.StreamHandler"
    level: "DEBUG"
    formatter: "simple"
    stream: "ext://sys.stdout"
  file:
    class: "logging.handlers.RotatingFileHandler"
    level: "INFO"
    formatter: "json"
    filename: "app.log"
    maxBytes: 10485760
    backupCount: 5
    encoding: "utf8"
loggers:
  my_module:
    level: "DEBUG"
    handlers: [ "console", "file" ]
    propagate: false
root:
  level: "WARNING"
  handlers: [ "console" ]

此时,需要通过如下的方式加载配置文件:

import logging.config
import yaml

# 加载 YAML 配置文件
with open('logging_config.yaml', 'r') as f:
    config = yaml.safe_load(f)

logging.config.dictConfig(config)

# 获取日志记录器
logger = logging.getLogger('my_module')

# 记录日志
logger.debug('This is a debug message')
logger.info('This is an info message')

字典格式

如果不想使用外部文件,可以直接在代码中使用 Python 字典定义配置:

import logging.config

config = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "simple": {
            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
        },
        "json": {
            "format": {
                "timestamp": "%(asctime)s",
                "name": "%(name)s",
                "level": "%(levelname)s",
                "message": "%(message)s"
            },
            "class": "pythonjsonlogger.jsonlogger.JsonFormatter"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "DEBUG",
            "formatter": "simple",
            "stream": "ext://sys.stdout"
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "level": "INFO",
            "formatter": "json",
            "filename": "app.log",
            "maxBytes": 10485760,
            "backupCount": 5,
            "encoding": "utf8"
        }
    },
    "loggers": {
        "my_module": {
            "level": "DEBUG",
            "handlers": ["console", "file"],
            "propagate": False
        }
    },
    "root": {
        "level": "WARNING",
        "handlers": ["console"]
    }
}

# 加载配置
logging.config.dictConfig(config)

# 获取日志记录器
logger = logging.getLogger('my_module')

# 记录日志
logger.debug('This is a debug message')
logger.info('This is an info message')

高阶功能

日志轮转

当日志文件过大时,可以通过文件轮转(Rotating)将日志分割成多个文件。logging提供了两种文件轮转处理器:

  • RotatingFileHandler:基于文件大小轮转。
  • TimedRotatingFileHandler:基于时间轮转。

基于文件大小的轮转:

import logging
from logging.handlers import RotatingFileHandler

# 创建日志记录器
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)

# 创建 RotatingFileHandler
handler = RotatingFileHandler(
    filename='app.log',  # 日志文件名
    maxBytes=10 * 1024 * 1024,  # 每个日志文件最大 10MB
    backupCount=5,  # 保留 5 个备份文件
    encoding='utf8'
)
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))

# 添加处理器
logger.addHandler(handler)

# 记录日志
for i in range(10000):
    logger.info(f'This is log message {i}')

基于时间的轮转:

import logging
from logging.handlers import TimedRotatingFileHandler

# 创建日志记录器
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)

# 创建 TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
    filename='app.log',  # 日志文件名
    when='midnight',  # 每天午夜轮转
    interval=1,  # 轮转间隔
    backupCount=7,  # 保留 7 个备份文件
    encoding='utf8'
)
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))

# 添加处理器
logger.addHandler(handler)

# 记录日志
logger.info('This is a log message')

日志转发到网络

logging支持将日志发送到远程服务器,例如通过 HTTP 或 Socket。

使用 HTTP 发送到服务器:

import logging
import logging.handlers

# 创建日志记录器
logger = logging.getLogger('remote_logger')
logger.setLevel(logging.DEBUG)

# 创建 SocketHandler
handler = logging.handlers.SocketHandler('localhost', 9020)  # 发送到本地 9020 端口
logger.addHandler(handler)

# 记录日志
logger.info('This is a log message sent to a remote server')

使用 Socket 发送到服务器:

import logging
import logging.handlers

# 创建日志记录器
logger = logging.getLogger('remote_logger')
logger.setLevel(logging.DEBUG)

# 创建 HTTPHandler
handler = logging.handlers.HTTPHandler(
    'localhost:5000',  # 服务器地址
    '/log',  # 日志接收路径
    method='POST'
)
logger.addHandler(handler)

# 记录日志
logger.info('This is a log message sent via HTTP')

日志过滤

logging 模块的日志过滤功能允许根据特定的条件控制哪些日志应该被记录。通过过滤器,可以实现对日志更加精细的日志控制,例如:

  • 过滤特定级别的日志。
  • 过滤包含特定关键词的日志。
  • 过滤来自特定模块或日志记录器的日志。

logging 模块中的日志过滤器需要继承 logging.Filter 类,重写 filter(record) 方法。record 是一个日志对象,包含了日志的所有信息,比如日志级别、消息和模块名等。

重写的 filter(record) 方法返回 True 时,日志会被记录。返回 False 时,日志会被忽略。

一个过滤特定级别日志的过滤器实现:

import logging


# 自定义过滤器
class LevelFilter(logging.Filter):
    def __init__(self, level):
        self.level = level

    def filter(self, record):
        return record.levelno == self.level  # 只允许指定级别的日志通过


# 创建日志记录器
logger = logging.getLogger('level_filter_logger')
logger.setLevel(logging.DEBUG)

# 创建 Handler
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))

# 添加过滤器
handler.addFilter(LevelFilter(logging.ERROR))  # 只记录 ERROR 级别的日志

# 添加处理器
logger.addHandler(handler)

# 记录日志
logger.debug('This is a debug message')  # 不会被记录
logger.info('This is an info message')  # 不会被记录
logger.error('This is an error message')  # 会被记录

一个过滤不包含某些关键词的日志过滤器实现:

import logging


# 自定义过滤器
class KeywordFilter(logging.Filter):
    def __init__(self, keyword):
        self.keyword = keyword

    def filter(self, record):
        return self.keyword in record.getMessage()  # 只允许包含关键词的日志通过


# 创建日志记录器
logger = logging.getLogger('keyword_filter_logger')
logger.setLevel(logging.DEBUG)

# 创建 Handler
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))

# 添加过滤器
handler.addFilter(KeywordFilter('important'))  # 只记录包含 "important" 的日志

# 添加处理器
logger.addHandler(handler)

# 记录日志
logger.info('This is a normal message')  # 不会被记录
logger.info('This is an important message')  # 会被记录

异步日志记录

在高并发场景下,同步日志记录可能会影响性能。可以通过QueueHandlerQueueListener实现异步日志记录:

import logging
import logging.handlers
import queue
import threading

# 创建日志队列
log_queue = queue.Queue()

# 创建 QueueHandler
queue_handler = logging.handlers.QueueHandler(log_queue)

# 创建日志记录器
logger = logging.getLogger('async_logger')
logger.setLevel(logging.DEBUG)
logger.addHandler(queue_handler)

# 创建 QueueListener
file_handler = logging.FileHandler('async.log')
listener = logging.handlers.QueueListener(log_queue, file_handler)

# 启动监听器
listener.start()

# 记录日志
logger.info('This is an async log message')

# 停止监听器
listener.stop()

动态修改日志配置

在运行时动态修改日志配置,例如根据环境变量调整日志级别:

import logging
import os

# 创建日志记录器
logger = logging.getLogger('dynamic_logger')
logger.setLevel(logging.DEBUG)

# 根据环境变量设置日志级别
log_level = os.getenv('LOG_LEVEL', 'INFO').upper()
logger.setLevel(getattr(logging, log_level))

# 记录日志
logger.debug('This is a debug message')
logger.info('This is an info message')

loguru 库

介绍

loguru 是一个 Python 日志记录 第三方开源库,相比与 Python 原生的 logging 模块,loguru 提供了更简单、更强大、更优雅的日志记录方式。它是对 Python 标准库logging 的替代方案,具有开箱即用、功能丰富、配置灵活等特点。

官方文档 中,对 loguru 的介绍十分有趣:

  1. Loguru 的主要理念是存在且仅存在一个日志记录器。
  2. 没有繁琐的 Handler、Formatter 或 Filter:一个函数即可掌控全局。

真是醉翁之意不在酒,虽然 loguru 没有直接写明和谁进行对比,但是学习过 Python 日志框架的人都知道官网在说什么。读完官方文档对 loguru 的介绍,可以总结出四个字:简单好用!

安装库

pip install loguru

推荐在虚拟环境中安装依赖库文件,避免对全局环境产生污染。

快速入门

开箱即用是 loguru 最大的特点,所有的核心配置全都被分装到了 logger 对象中,全部使用 add 的方式向 loguru 添加配置:

from loguru import logger

# 开箱即用,不需要过多的配置
logger.debug("That's it, beautiful and simple logging!")

# 通过 add 方法添加配置
logger.add(sys.stderr, format="{time} {level} {message}", filter="my_module", level="INFO")

如果希望将日志信息转发到文件,也是通过 add 方法进行:

logger.add("file_{time}.log")

如果需要轮换日志、删除旧日志或者是在关闭时压缩日志文件,可以这样做:

# 文件过大时自动轮换
logger.add("file_1.log", rotation="500 MB")
# 每天中午 12 点创建新文件
logger.add("file_2.log", rotation="12:00")
# 文件过期后轮换
logger.add("file_3.log", rotation="1 week")
# 一段时间后清理旧日志
logger.add("file_X.log", retention="10 days")
# 压缩日志以节省空间
logger.add("file_Y.log", compression="zip")

看到了吧,一切关于日志的操作在 loguru 中都会变得简单!

格式化日志

Loguru 的日志方法支持类似 str.format() 的格式化方式,既可以使用位置参数,也可以使用关键字参数:

logger.info("If you're using Python {}, prefer {feature} of course!", 3.6, feature="f-strings")

这种方式比传统的 % 格式化更灵活、更易读,也符合 Python 的现代格式化风格(如 f-strings)。

日志格式

Loguru 的默认日志格式如下:

< level > {time: YYYY - MM - DD HH: mm: ss.SSS} | < level > {level} | < cyan > {name} < / cyan >: < cyan > {
    function} < / cyan >: < cyan > {line} < / cyan > - < level > {message} < / level >

其中:

  • {time}:日志记录的时间,默认格式为 YYYY-MM-DD HH:mm:ss.SSS
  • {level}:日志级别(如 INFO、ERROR 等)。
  • {name}:日志记录器的名称(通常是模块名称)。
  • {function}:记录日志的函数名称。
  • {line}:记录日志的代码行号。
  • {message}:日志消息内容。

Loguru 提供了非常灵活的日志格式配置,允许通过format参数自定义日志格式。以下是一个简单的自定义格式示例:

logger.add(sys.stderr, format="{time} | {level} | {message}")

输出的日志如下:

2023-10-10 12:00:00 | INFO | This is a log message

Loguru 支持自定义时间格式,使用{time}字段时可以指定格式:

logger.add(sys.stderr, format="{time:YYYY/MM/DD at HH:mm:ss} | {level} | {message}")

此外,Loguru 支持许多内置字段,例如:

  • {file}:记录日志的文件名。
  • {module}:记录日志的模块名称。
  • {process}:进程 ID。
  • {thread}:线程 ID。
  • {elapsed}:从程序启动到记录日志的时间间隔。
logger.add(sys.stderr, format="{time} | {level} | {file}:{line} | {message}")

输出的日志如下:

2023-10-10 12:00:00 | INFO | example.py:10 | This is a log message

若 Loguru 的内置字段不满足要求,还可以使用 bind() 方法绑定额外的上下文信息,然后通过 {extra} 字段显示这些信息。

logger.add(sys.stderr, format="{time} | {level} | {message} | {extra}")

context_logger = logger.bind(user_id=123, request_id="abc123")
context_logger.info("User logged in")

输出的日志如下:

2023-10-10 12:00:00 | INFO | User logged in | {'user_id': 123, 'request_id': 'abc123'}

Loguru 还支持根据日志级别或其他条件动态调整日志格式:

# 错误日志显示为红色。其他日志显示为默认格式。
def formatter(record):
    if record["level"].name == "ERROR":
        return "<red>{time} | {level} | {message}</red>\n"
    return "{time} | {level} | {message}\n"

logger.add(sys.stderr, format=formatter)

catch 装饰器

Loguru 支持使用装饰器的方式捕获方法可能出现的异常:

from loguru import logger


@logger.catch
def risky_function():
    return 1 / 0  # 这会引发一个异常


risky_function()

当异常发生时,Loguru 会捕获异常并记录详细的错误信息,包括堆栈跟踪。

日志颜色

Loguru 会根据日志的级别(如 INFO、WARNING、ERROR 等)自动为日志消息添加颜色。例如,错误日志可能是红色的,而信息日志可能是绿色的。同时,Loguru 也支持自定义日志样式:

from loguru import logger

# 自定义日志格式,使用标记标签定义颜色和样式
logger.add(sys.stderr, format="<green>{time}</green> <level>{message}</level>")

logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")

Loguru 支持的样式包括:

  • <bold>:加粗文本
  • <dim>:暗淡文本
  • <italic>:斜体文本
  • <underline>:下划线文本
  • <strike>:删除线文本
  • <black><red><green><yellow><blue><magenta><cyan><white>:文本颜色
  • <bg-black><bg-red><bg-green><bg-yellow><bg-blue><bg-magenta><bg-cyan><bg-white>:背景颜色
logger.add(sys.stderr, format="<red><bold>{time}</bold></red> <blue>{message}</blue>")

线程安全

默认情况下,Loguru 是线程安全的,但不是多线程安全的。在多线程环境中,可以将日志信息添加到队列中,确保捕获到的日志信息的完整性。

from loguru import logger

# 添加一个文件接收器,并启用队列以确保多进程安全或异步日志记录
logger.add("output.log", enqueue=True)

enqueue=True:将日志消息放入队列中,确保线程安全、多进程安全以及异步日志记录。日志消息会被后台线程处理,不会阻塞主程序的执行。

序列化

使用 serialize=True参数,Loguru 会将日志消息转换为 JSON 格式。转换后的格式保留了日志的完整结构化信息,便于后续处理和分析。

from loguru import logger

# 添加一个文件接收器,并启用序列化
logger.add("output.log", serialize=True)

# 记录一些日志
logger.info("This is a serialized log message")
logger.error("An error occurred", details={"code": 500, "message": "Internal Server Error"})

日志文件output.log中的内容将是 JSON 格式,例如:

{
  "text": "2023-10-10 12:00:00 | INFO     | __main__:<module>:1 - This is a serialized log message\n",
  "record": {
    "elapsed": {
      "repr": "0:00:00.000123",
      "seconds": 0.000123
    },
    "exception": null,
    "extra": {},
    "file": {
      "name": "example.py",
      "path": "/path/to/example.py"
    },
    "function": "<module>",
    "level": {
      "icon": "ℹ️",
      "name": "INFO",
      "no": 20
    },
    "line": 1,
    "message": "This is a serialized log message",
    "module": "example",
    "name": "__main__",
    "process": {
      "id": 1234,
      "name": "MainProcess"
    },
    "thread": {
      "id": 12345,
      "name": "MainThread"
    },
    "time": {
      "repr": "2023-10-10 12:00:00",
      "timestamp": 1696939200.0
    }
  }
}
{
  "text": "2023-10-10 12:00:01 | ERROR    | __main__:<module>:2 - An error occurred\n",
  "record": {
    "elapsed": {
      "repr": "0:00:01.000456",
      "seconds": 1.000456
    },
    "exception": null,
    "extra": {
      "details": {
        "code": 500,
        "message": "Internal Server Error"
      }
    },
    "file": {
      "name": "example.py",
      "path": "/path/to/example.py"
    },
    "function": "<module>",
    "level": {
      "icon": "❌",
      "name": "ERROR",
      "no": 40
    },
    "line": 2,
    "message": "An error occurred",
    "module": "example",
    "name": "__main__",
    "process": {
      "id": 1234,
      "name": "MainProcess"
    },
    "thread": {
      "id": 12345,
      "name": "MainThread"
    },
    "time": {
      "repr": "2023-10-10 12:00:01",
      "timestamp": 1696939201.0
    }
  }
}

惰性求值

Loguru 的惰性求值(Lazy Evaluation)是一种优化日志记录性能的技术。通过惰性求值,Loguru 可以确保只有在日志实际需要记录时,才会执行这些昂贵的操作,从而避免不必要的性能开销。

惰性求值的核心思想有两点:

  1. 延迟执行:在日志记录时,如果某些操作(如调用复杂函数或生成大量数据)非常耗时,但这些日志可能在生产环境中不会被记录(例如日志级别较高时),惰性求值可以延迟这些操作的执行。只有在日志级别允许记录时,才会真正执行这些操作。
  2. 性能优化:避免在不必要的情况下执行昂贵的操作,从而提高程序的运行效率。

Loguru 提供了opt(lazy=True)方法来实现惰性求值。通过将日志参数封装为可调用对象(如函数或 lambda 表达式),Loguru 会在需要记录日志时才执行这些操作。

from loguru import logger

# 一个耗时的函数
def expensive_operation():
    print("Expensive operation is running...")
    return "Expensive data"

# 普通日志记录(即使日志级别高于 INFO,expensive_operation() 也会被执行)
logger.info("Data: {}", expensive_operation())

# 使用 opt(lazy=True) 实现惰性求值(只有在日志级别允许时才会执行 expensive_operation())
logger.opt(lazy=True).info("Data: {}", expensive_operation)

如果日志级别高于 INFO(如 WARNING 或 ERROR)时,expensive_operation() 不会被执行,日志也不会记录。如果日志级别为 INFO 或更低,输出的结果如下:

Expensive
operation is running...
2023 - 10 - 10
12: 00:00 | INFO | Data: Expensive
data

惰性求值特别适用于需要记录复杂、耗时操作结果的场景,同时避免在生产环境中执行不必要的操作,比如:

  1. 调试场景:在开发环境中记录详细的调试信息,但在生产环境中避免执行这些操作。
  2. 复杂计算:记录某些需要复杂计算的结果,但只在需要时执行计算。
  3. 大数据生成:记录大量数据时,避免在不必要的情况下生成数据。

状态管理

Loguru 提供了disable()enable()方法,用于控制日志记录器的启用和禁用状态。在项目开发和调试过程中,状态的管理十分重要,可以灵活的配置日志的输出,使调试环境和生产环境日志的输出有所区别。

  • disable()方法用于禁用指定模块或日志记录器的日志输出。禁用后,日志函数(如logger.info())将不会执行任何操作。
  • enable()方法用于启用指定模块或日志记录器的日志输出。启用后,日志函数将正常执行并输出日志。
from loguru import logger

# 禁用当前模块的日志记录器
logger.disable(__name__)
# 这条日志不会输出
logger.info("This log message will not be displayed")

# 启用指定模块的日志记录器
logger.enable("my_library")
# 这条日志会正常输出
logger.info("This log message will be displayed")

在某些情况下,还可以根据条件动态启用或禁用日志记录器:

from loguru import logger

# 根据条件启用或禁用日志记录器
if debug_mode:
    logger.enable("my_module")
else:
    logger.disable("my_module")

# 日志输出取决于 debug_mode 的值
logger.info("This log message depends on debug_mode")