6个Python日志记录库比较

虽然 Python 在其标准库中提供了强大且功能丰富的日志记录解决方案,但第三方日志记录生态系统提供了一系列引人注目的替代方案。根据您的要求,这些外部库可能更适合您的日志记录需求。

因此,本文将考虑 Python 用于跟踪应用程序和库行为的六大日志记录解决方案。我们将从讨论标准模块开始,然后研究Python 社区创建的logging其他五个日志框架。

1、标准日志模块
Python 与大多数编程语言的区别在于其标准库中包含功能齐全的日志记录框架。该日志记录解决方案有效地满足了库和应用程序开发人员的需求,它包含以下严重级别:DEBUG、INFO、WARNING、 ERROR和CRITICAL。借助默认记录器,您无需任何初步设置即可立即开始记录:

import logging

logging.debug("A debug message")
logging.info("An info message")
logging.warning("A warning message")
logging.error("An error message")
logging.critical("A critical message")

此默认(或根)记录器在该WARNING级别运行,这意味着只有严重性等于或超过的记录调用WARNING才会产生输出:

WARNING:root:A warning message
ERROR:root:An error message
CRITICAL:root:A critical message

这种配置可确保只显示潜在的重要信息,减少日志输出中的噪音。不过,您也可以根据需要自定义日志级别并微调日志记录行为。使用日志记录模块的推荐方法是通过 getLogger() 方法创建自定义日志记录器:

import logging

logger = logging.getLogger(__name__)

. . .

有了自定义日志记录器后,就可以通过日志记录模块提供的处理程序、格式化器和过滤器类自定义其输出。

  • 处理程序决定输出目的地,并可根据日志级别进行自定义。一个日志记录器还可添加多个处理程序,以便同时向不同目的地发送日志信息。
  • 格式器决定日志记录器生成的记录格式。不过,没有预定义的格式,如 JSON、Logfmt 等。您必须结合可用的日志记录属性来创建自己的格式。根日志记录器的默认格式是 %(级别名)s:%(名称)s:%(消息)s。不过,自定义日志记录器默认只使用 %(信息)s。
  • 处理程序和日志记录器对象使用过滤器过滤日志记录。与日志级别相比,过滤器能更好地控制哪些日志记录应被处理或忽略。在日志被发送到最终目的地之前,它们还能以某种方式增强或修改记录。例如,您可以创建一个自定义过滤器,以删除日志中的敏感数据。

下面是一个使用自定义日志记录器向控制台和文件记录日志的示例:

import sys
import logging

logger = logging.getLogger("example")
logger.setLevel(logging.DEBUG)

# Create handlers for logging to the standard output and a file
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
errHandler = logging.FileHandler("error.log")

# Set the log levels on the handlers
stdoutHandler.setLevel(logging.DEBUG)
errHandler.setLevel(logging.ERROR)

# Create a log format using Log Record attributes
fmt = logging.Formatter(
    "%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s"
)

# Set the log format on each handler
stdoutHandler.setFormatter(fmt)
errHandler.setFormatter(fmt)

# Add each handler to the Logger object
logger.addHandler(stdoutHandler)
logger.addHandler(errHandler)

logger.info("Server started listening on port 8080")
logger.warning(
    "Disk space on drive '/var/log' is running low. Consider freeing up space"
)

try:
    raise Exception("Failed to connect to database: 'my_db'")
except Exception as e:
    # exc_info=True ensures that a Traceback is included
    logger.error(e, exc_info=True)

执行上述程序时,控制台会如期打印出以下日志信息:

example: 2023-07-23 14:42:18,599 | INFO | main.py:30 | 187901 >>> Server started listening on port 8080
example: 2023-07-23 14:14:47,578 | WARNING | main.py:28 | 143936 >>> Disk space on drive '/var/log' is running low. Consider freeing up space
example: 2023-07-23 14:14:47,578 | ERROR | main.py:34 | 143936 >>> Failed to connect to database: 'my_db'
Traceback (most recent call last):
  File "/home/ayo/dev/betterstack/demo/python-logging/main.py", line 32, in <module>
    raise Exception("Failed to connect to database: 'my_db'")
Exception: Failed to connect to database: 'my_db'


同时还创建了 error.log 文件,该文件应仅包含 ERROR 日志,因为 errHandler 的最小级别已设置为 ERROR:

example: 2023-07-23 14:14:47,578 | ERROR | main.py:34 | 143936 >>> Failed to connect to database: 'my_db'
Traceback (most recent call last):
  File "/home/ayo/dev/betterstack/demo/python-logging/main.py", line 32, in <module>
    raise Exception("Failed to connect to database: 'my_db'")
Exception: Failed to connect to database: 'my_db'

在撰写本文时,除非执行一些附加代码,否则日志模块无法生成结构化日志。值得庆幸的是,有一种更简单、更好的方法可以获得结构化输出:python-json-logger 库。

pip install python-json-logger

安装后,您可以按以下方式使用它:

import sys
import logging
[b]from pythonjsonlogger import jsonlogger

. . .

# The desired Log Record attributes must be included here, and they can be
# renamed if necessary
fmt = jsonlogger.JsonFormatter(
    "%(name)s %(asctime)s %(levelname)s %(filename)s %(lineno)s %(process)d %(message)s",
    rename_fields={"levelname": "severity", "asctime": "timestamp"},
)[/b]

# Set the log format on each handler
stdoutHandler.setFormatter(fmt)
errHandler.setFormatter(fmt)

. . .

如果用上面突出显示的几行修改前面的示例,执行时将观察到以下输出:

{"name": "example", "filename": "main.py", "lineno": 31, "process": 179775, "message": "Server started listening on port 8080", "severity": "INFO", "timestamp": "2023-07-23 14:39:03,265"}
{"name": "example", "filename": "main.py", "lineno": 32, "process": 179775, "message": "Disk space on drive '/var/log' is running low. Consider freeing up space", "severity": "WARNING", "timestamp": "2023-07-23 14:39:03,265"}
{"name": "example", "filename": "main.py", "lineno": 38, "process": 179775, "message": "Failed to connect to database: 'my_db'", "exc_info": "Traceback (most recent call last):\n  File \"/home/ayo/dev/betterstack/demo/python-logging/main.py\", line 36, in <module>\n    raise Exception(\"Failed to connect to database: 'my_db'\")\nException: Failed to connect to database: 'my_db'", "severity": "ERROR", "timestamp": "2023-07-23 14:39:03,265"}


也可以通过水平方法上的 extra 属性在日志点添加上下文数据,就像这样:

logger.info(
    "Server started listening on port 8080",
    extra={"python_version": 3.10, "os": "linux", "host": "fedora 38"},
)


如果这些额外字段与任何默认属性名称不冲突,就会自动插入日志记录。否则就会出现 KeyError 异常。

{"name": "example", "filename": "main.py", "lineno": 31, "process": 195301, "message": "Server started listening on port 8080", "python_version": 3.1, "os": "linux", "host": "fedora 38", "severity": "INFO", "timestamp": "2023-07-23 14:45:42,472"}


正如您所看到的,内置日志记录模块能够满足各种日志记录需求,并且具有可扩展性。不过,它的初始配置和自定义可能比较麻烦,因为在开始有效记录之前,你必须创建和配置记录器、处理程序和格式化器。

2. Loguru
Loguru是最流行的 Python 第三方日志框架,在撰写本文时拥有超过 15k GitHub star。它的目的是通过预先配置记录器来简化记录过程,并使其能够通过其add()方法轻松定制。使用 Loguru 启动日志记录轻而易举;只需安装包并导入它,然后调用其级别方法之一,如下所示:

pip install loguru

from loguru import logger

logger.trace("Executing program")
logger.debug("Processing data...")
logger.info("Server started successfully.")
logger.success("Data processing completed successfully.")
logger.warning("Invalid configuration detected.")
logger.error("Failed to connect to the database.")
logger.critical("Unexpected system error occurred. Shutting down.")

默认配置将半结构化和彩色输出记录到标准错误。它还默认为其DEBUG最低级别,这解释了为什么TRACE不记录输出。

Loguru 的内部运作可以通过该功能轻松定制,add()该功能处理从格式化日志到设置其目的地的所有事务。例如,您可以使用以下配置记录到标准输出,将默认级别更改为INFO,并将日志格式设置为 JSON:

from loguru import logger
import sys

logger.remove(0) # remove the default handler configuration
logger.add(sys.stdout, level="INFO", serialize=True)

. . .

输出:
{"text": "2023-07-17 15:26:21.597 | INFO     | __main__:<module>:9 - Server started successfully.\n", "record": {"elapsed": {"repr": "0:00:00.006401", "seconds": 0.006401}, "exception": null, "extra": {}, "file": {"name": "main.py", "path": "/home/ayo/dev/betterstack/demo/python-logging/main.py"}, "function": "<module>", "level": {"icon": " ", "name": "INFO", "no": 20}, "line": 9, "message": "Server started successfully.", "module": "main", "name": "__main__", "process": {"id": 3852028, "name": "MainProcess"}, "thread": {"id": 140653618894656, "name": "MainThread"}, "time": {"repr": "2023-07-17 15:26:21.597156+02:00", "timestamp": 1689600381.597156}}}


Loguru 生成的默认 JSON 输出可能非常冗长,但使用自定义函数很容易 序列化日志消息, 如下所示:

from loguru import logger
import sys
import json

def serialize(record):
    subset = {
        "timestamp": record["time"].timestamp(),
        "message": record["message"],
        "level": record["level"].name,
        "file": record["file"].name,
        "context": record["extra"],
    }
    return json.dumps(subset)

def patching(record):
    record["extra"]["serialized"] = serialize(record)

logger.remove(0)

logger = logger.patch(patching)
logger.add(sys.stderr, format="{extra[serialized]}")

logger.bind(user_id="USR-1243", doc_id="DOC-2348").debug("Processing document")

输出:
{"timestamp": 1689601339.628792, "message": "Processing document", "level": "DEBUG", "file": "main.py", "context": {"user_id": "USR-1243", "doc_id": "DOC-2348"}}


Loguru 也完全支持上下文日志记录。您已经看到了 bind()上面的方法,它允许在日志点添加上下文数据。您还可以使用它来创建子记录器来记录共享相同上下文的记录:

child = logger.bind(user_id="USR-1243", doc_id="DOC-2348")
child.debug("Processing document")
child.warning("Invalid configuration detected. Falling back to defaults")
child.success("Document processed successfully")

请注意user_id和doc_id字段如何出现在所有三个记录中:

{"timestamp": 1689601518.884659, "message": "Processing document", "level": "DEBUG", "file": "main.py", "context": {"user_id": "USR-1243", "doc_id": "DOC-2348"}}
{"timestamp": 1689601518.884706, "message": "Invalid configuration detected. Falling back to defaults", "level": "WARNING", "file": "main.py", "context": {"user_id": "USR-1243", "doc_id": "DOC-2348"}}
{"timestamp": 1689601518.884729, "message": "Document processed successfully", "level": "SUCCESS", "file": "main.py", "context": {"user_id": "USR-1243", "doc_id": "DOC-2348"}}

另一方面,它的contextualize()方法简化了向特定范围或上下文内的所有日志记录添加上下文字段的过程。例如,下面的代码片段演示了向由于该请求而创建的所有日志添加唯一的请求 ID 属性:

from loguru import logger
import uuid

def logging_middleware(get_response):
    def middleware(request):
        request_id = str(uuid.uuid4())

        with logger.contextualize(request_id=request_id):
            response = get_response(request)
            response["X-Request-ID"] = request_id
            return response

    return middleware

Loguru 还支持您期望从良好的日志记录框架获得的所有功能,例如通过自动轮换和压缩记录到文件、自定义日志级别、异常处理、一次记录到多个目的地等等。它还 为来自标准模块的用户提供了迁移指南。


3、Structlog
Structlog是一个日志库,专门用于生成 JSON 或 Logfmt 格式的结构化输出。它支持开发环境的彩色和美观增强的控制台输出,但也允许完全自定义日志格式以满足不同的需求。您可以使用以下命令安装 Structlog 包:

pip install structlog

import structlog

logger = structlog.get_logger()

logger.debug("Database query executed in 0.025 seconds")
logger.info(
    "Processing file 'data.csv' completed. 1000 records were imported",
    file="data.csv",
    elapsed_ms=300,
    num_records=1000,
)
logger.warning(
    "Unable to load configuration file 'config.ini'. Using default settings instead",
    file="config.ini",
)

try:
    1 / 0
except ZeroDivisionError as e:
    logger.exception(
        "Division by zero error occurred during calculation. Check the input values",
        exc_info=e,
    )
logger.critical("Application crashed due to an unhandled exception")


4、Eliot
Eliot是一种独特的 Python 日志记录解决方案,其目的不仅是提供程序中发生的事件的记录,而且还输出导致该事件的动作的因果链。您可以按pip如下方式安装 Eliot:

pip install eliot

艾略特的关键概念之一是一个动作action,它代表任何可以成功开始和完成或因异常而失败的任务。当您启动一项操作时,会生成两条日志记录:一条指示操作的开始,另一条指示操作的成功或失败。演示此模型的最佳方式是通过一个示例:

import sys
from eliot import start_action, to_file

to_file(sys.stdout)


def calculate(x, y):
    with start_action(action_type="multiply"):
        return x * y


calculate(10, 5)

这里使用 start_action 函数表示新操作的开始。一旦执行了 calculate() 函数,两份日志就会被发送到 to_file() 配置的目的地:
{"action_status": "started", "timestamp": 1690213156.7701144, "task_uuid": "a9a47808-15a9-439b-8335-b88d50013f75", "action_type": "multiply", "task_level": [1]}
{"action_status": "succeeded", "timestamp": 1690213156.7701554, "task_uuid": "a9a47808-15a9-439b-8335-b88d50013f75", "action_type": "multiply", "task_level": [2]}


Eliot 默认生成结构化的 JSON 输出,其中包括以下记录:

  • task_uuid:生成消息的唯一任务标识符。
  • action_status:动作状态:表示操作的状态。
  • timestamp:消息的 UNIX 时间戳。
  • task_level:任务级别:消息在任务的操作树中的位置。
  • action_type:动作类型:提供的 action_type 参数。

5. Logbook
Logbook将自己描述为 Python 标准库logging模块的一个很酷的替代品,其目的是让日志记录变得有趣。您可以使用以下命令将其安装到您的项目中:

pip install logbook

import sys
import logbook

logger = logbook.Logger(__name__)

handler = logbook.StreamHandler(sys.stdout, level="INFO")
handler.push_application()

logger.info("Successfully connected to the database 'my_db' on host 'ubuntu'")

logger.warning("Detected suspicious activity from IP address: 111.222.333.444")

输出:
[2023-07-24 21:41:50.932575] INFO: __main__: Successfully connected to the database 'my_db' on host 'ubuntu'
[2023-07-24 21:41:50.932623] WARNING: __main__: Detected suspicious activity from IP address: 111.222.333.444

如上所示,logbook.Logger 方法用于创建一个新的日志记录器通道。该日志记录器提供了对 info() 和 warning() 等级别方法的访问,用于写入日志信息。除介于 INFO 和 WARNING 之间的 NOTICE 级别外,还支持日志模块中的所有日志级别。

Logbook 还使用处理程序概念来确定日志的目的地和格式。StreamHandler 类可将日志发送到任何输出流(本例中为标准输出),其他处理程序可将日志发送到文件、Syslog、Redis、Slack 等。


6. Picologging
Mirosoft 的 Picologging库是 Python 日志记录生态系统中相对较新的补充。它定位为标准日志记录模块的高性能直接替代品,其速度显着提高了 4-10 倍,如 GitHub 自述文件中所述。要将其集成到您的项目中,您可以使用以下命令安装它:

pip install picologging

Picologging 与 Python 中的模块共享相同的熟悉的 API logging,并且使用相同的日志记录属性进行格式化:

import sys
import picologging as logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

stdout_handler = logging.StreamHandler(sys.stdout)
fmt = logging.Formatter(
    "%(name)s: %(asctime)s | %(levelname)s | %(process)d >>> %(message)s"
)

stdout_handler.setFormatter(fmt)
logger.addHandler(stdout_handler)

logger.info(
    "Successfully connected to the database '%s' on host '%s'", "my_db", "ubuntu20.04"
)

logger.warning("Detected suspicious activity from IP address: %s", "111.222.333.444")

输出:
__main__: 2023-07-24 05:46:38,-2046715687 | INFO | 795975 >>> Successfully connected to the database 'my_db' on host 'ubuntu20.04'
__main__: 2023-07-24 05:46:38,-2046715687 | WARNING | 795975 >>> Detected suspicious activity from IP address: 111.222.333.444

总结
对 Python 日志记录的主要建议是使用Loguru,因为它具有令人印象深刻的功能和用户友好的 API。然而,熟悉内置 模块至关重要 logging,因为它仍然是一个强大且广泛使用的解决方案。

Structlog 是另一个值得考虑的强大选项,Eliot 也可能是一个不错的选择,只要它缺乏日志级别并不是您的用例的主要问题。另一方面,Picologging 目前还处于早期开发阶段,而 Logbook 缺乏对结构化日志记录的原生支持,因此不太适合在生产环境中进行日志记录。