引言
作为一名Python程序员,记录程序运行时的关键信息是一种有益的做法,这有助于深入理解你的代码。这种记录行为被称作日志记录,它是一个非常实用的工具,是编程工具箱中不可或缺的一部分。日志记录能够帮助你捕捉到在开发过程中可能忽略的情况。
这些记录被称作日志,它们就像始终监视应用程序运行的额外眼睛。日志能够记录哪些用户或IP地址访问了应用程序等信息。当程序出错时,日志提供的信息可能比错误堆栈跟踪更加丰富,它能够告诉你错误发生前程序的状态以及出错的具体代码行。
Python的标准库包含了一个日志系统。你只需几行代码,就可以在你的应用程序中实现日志功能。
在本教程中,你将学习以下内容:
- 如何使用Python的日志模块
- 如何配置基本的日志设置
- 如何应用不同的日志级别
- 如何使用格式化工具来美化你的日志消息
- 如何使用处理器来改变日志的存储位置
- 如何通过过滤器来定制日志规则
正确地记录关键数据,可以帮助你调试程序中的错误,分析应用的性能以便规划扩展,或者观察使用模式以制定市场策略。
如果您经常利用Python的 print()
函数来了解程序的执行流程,那么学习日志记录将是您自然而然的进阶选择。本教程将带领您迈出记录日志的第一步,并告诉您如何随着项目的发展逐步完善日志功能。
Python 日志模块
Python 标准库中的日志模块是一个现成且功能强大的工具,它既适合编程新手,也能满足企业团队的需求。
日志模块的核心组件是所谓的日志记录器。你可以将日志记录器想象成一个你代码中的记录员,它负责决定记录哪些信息,信息的详细程度,以及这些信息的存储或发送目的地。
探索 Root Logger
要初步了解日志模块和日志记录器是如何工作的,你可以打开 Python 的标准交互式解释器(REPL),然后输入以下代码:
>>> import logging
>>> logging.warning("Remain calm!")
WARNING:root:Remain calm!
输出信息在每条日志消息前都会标明其严重性等级,以及“root”,这是日志模块为其默认日志记录器指定的名称。这种输出展示了日志的默认格式,您可以对其进行配置,以包含时间戳或其他额外信息。
在上述例子中,您向根日志记录器发送了一条消息,其日志等级被设置为“警告”。日志等级是日志记录中的关键组成部分。默认情况下,存在五个标准等级,用于区分事件的严重程度。每个等级都有对应的函数,您可以使用这些函数来记录相应严重程度的事件。
以下是五个默认日志级别(按严重性递增的顺序排列):
日志模块配备了一个默认的日志记录器,这使得您可以轻松开始记录日志,无需进行复杂的配置。但是,如上表所示的日志函数暴露了一个可能出乎您意料的小特点:
>>> logging.debug("This is a debug message")
>>> logging.info("This is an info message")
>>> logging.warning("This is a warning message")
WARNING:root:This is a warning message
>>> logging.error("This is an error message")
ERROR:root:This is an error message
>>> logging.critical("This is a critical message")
CRITICAL:root:This is a critical message
请注意,debug()
和 info()
级别的日志消息并没有被记录。这是因为日志模块默认情况下只记录 WARNING 及以上级别的日志信息。您可以通过调整日志模块的配置,使其能够记录所有级别的日志事件。
调整日志级别
要配置基本的日志设置并设定日志级别,日志模块包含了一个名为 basicConfig()
的函数。作为 Python 开发者,您可能会觉得这个函数的驼峰命名法有些不寻常,因为它不符合 PEP 8 的命名规范:
在本教程的后续部分,您将学习到 basicConfig()
的常用参数。目前,我们将重点放在 level
参数上,用它来设定根日志记录器的日志级别。
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> logging.debug("This will get logged.")
DEBUG:root:This will get logged.
利用 level
参数,您可以定义希望记录的日志消息的严重性级别。您可以通过指定类中预定义的常量来实现这一点。作为 level
参数,您可以选择使用该常量的直接值、它的数字形式,或者是对应的字符串形式。
设置日志级别将启用定义级别及更高级别的所有日志记录调用。例如,当您将日志级别设置为 DEBUG 时,将记录所有处于或高于 DEBUG 级别的事件。
格式化输出
默认情况下,日志会记录日志级别、记录器名称和日志内容,这为日志记录提供了基础。但您可以通过 basicConfig()
函数中的 format
参数来丰富您的日志,加入更多数据。
format
参数允许您输入一个包含多个预设属性的字符串。您可以把这些属性想象成是您在字符串中设置的格式占位符。format
的默认设置格式大致如下:
>>> import logging
>>> logging.basicConfig(format="%(levelname)s:%(name)s:%(message)s")
>>> logging.warning("Hello, Warning!")
WARNING:root:Hello, Warning!
这种格式被称为 printf 风格字符串格式化。您也可能遇到使用美元符号和大括号(${})的日志格式,这与 Python 的 string.Template() 类相关。如果您熟悉现代 Python 中的字符串格式化方法,那么您可能更习惯于使用大括号({})来进行字符串的格式化。
您可以通过 style
参数来指定格式字符串的样式。style
可选用的值为 "%"、"$" 或 "{"。当您指定了 style
参数后,您的格式字符串就需要与所选样式一致。如果不一致,程序将抛出一个 ValueError 异常。
>>> import logging
>>> logging.basicConfig(format="{levelname}:{name}:{message}", style="{")
>>> logging.warning("Hello, Warning!")
WARNING:root:Hello, Warning!
正如之前提到的,format
参数允许您输入一个包含多个预设属性的字符串。您选择使用的属性将基于您希望从日志中获取的信息类型。
除了日志消息的文本和级别,通常在日志中包含时间戳也是一个好习惯。时间戳能够精确记录下程序发出日志消息的具体时间点。这有助于您监测代码的执行效率或者识别某些错误发生的规律。
要在日志中加入时间戳,您可以在调用 basicConfig()
时,在格式字符串中使用 asctime
属性。默认情况下,asctime
会显示包括毫秒在内的时间。如果您不需要这么详细的时间信息,或者您想要自定义时间戳的格式,那么您需要在 basicConfig()
函数调用中加入 datefmt
参数。
>>> import logging
>>> logging.basicConfig(
... format="{asctime} - {levelname} - {message}",
... style="{",
... datefmt="%Y-%m-%d %H:%M",
... )
>>> logging.error("Something went wrong!")
2024-07-22 09:26 - ERROR - Something went wrong!
在上文的例子中,您的日志信息以时间戳作为开头。在 datefmt
字符串中,您使用特定的指令来设置时间戳的格式,这些指令包括年份(%Y)、月份(%m)、日期(%d)、小时(%H)和分钟(%M)。如果您想查看所有可用的日期指令,以便嵌入到格式字符串中,可以参考 time.strftime()
函数的官方文档。
当您需要记录一系列时间点上的事件,或者您打算将日志信息存储到外部文件中时,了解每条日志消息的具体时间就变得尤为关键。
记录到文件
到目前为止,您的日志信息仅输出到了控制台。如果您打算对日志进行归档,那么将它们保存到一个可以随着时间不断增长的文件中是明智的选择。
要实现日志文件的保存,您可以在配置日志记录器时,通过 basicConfig()
函数的 filename
参数来指定日志文件的路径。这与在 Python 中使用 open()
函数打开文件类似,您需要提供文件的存储路径。同时,指定文件的编码方式和打开模式也是推荐的做法。
>>> import logging
>>> logging.basicConfig(
... filename="app.log",
... encoding="utf-8",
... filemode="a",
... format="{asctime} - {levelname} - {message}",
... style="{",
... datefmt="%Y-%m-%d %H:%M",
... )
>>> logging.warning("Save me!")
使用上面的设置,您的日志信息会被存储到一个名为 app.log
的文件里,而不是输出到控制台。为了确保新日志追加到文件中而不是替换掉已有的内容,您应该将文件模式设置为 a
,代表“追加”。
app.log
文件是一个普通的文本文件,您可以使用任何文本编辑器来查看或编辑它。
2024-07-22 09:55 - WARNING - Save me!
除了对日志记录进行格式化,将日志存储在以日期命名的文件夹中,并调整日志文件的命名也是一个不错的方法。您还可以尝试更具创造性的方式,比如将日志记录格式化为 CSV 文件的格式,这样您就可以保存日志并编写自己的程序来练习如何解析 CSV 文件。
显示变量
在大多数情况下,您希望在日志中包含应用程序的动态信息。您已经看到日志记录函数采用字符串作为参数。通过利用 Python 的 f 字符串,您可以创建包含变量信息的详细调试消息:
>>> import logging
>>> logging.basicConfig(
... format="{asctime} - {levelname} - {message}",
... style="{",
... datefmt="%Y-%m-%d %H:%M",
... level=logging.DEBUG,
... )
>>> name = "Samara"
>>> logging.debug(f"{name=}")
2024-07-22 14:49 - DEBUG - name='Samara'
首先,配置记录器并将调试级别设置为 DEBUG 以显示调试消息。然后,定义一个值为“Samara”的变量名称。使用自记录表达式,您可以通过在变量名称后附加等号 (=) 来在 f 字符串中插入变量名称及其值。
利用日志模块观察变量的当前值是应用调试初期的有效手段。如果您希望更深入地理解代码的运行情况,将异常信息记录到日志中会是一个合理的选择。
捕获堆栈跟踪
日志模块提供了捕获应用程序中完整堆栈跟踪的能力。当设置 exc_info
参数为 True 时,就可以记录异常信息,具体的日志记录函数调用方法如下:
>>> import logging
>>> logging.basicConfig(
... filename="app.log",
... encoding="utf-8",
... filemode="a",
... format="{asctime} - {levelname} - {message}",
... style="{",
... datefmt="%Y-%m-%d %H:%M",
... )
>>> donuts = 5
>>> guests = 0
>>> try:
... donuts_per_guest = donuts / guests
... except ZeroDivisionError:
... logging.error("DonutCalculationError", exc_info=True)
...
由于您正在登录 app.log 文件,因此您可以跟踪文件中的堆栈跟踪:
2024-07-22 15:04 - ERROR - DonutCalculationError
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
如果未将 exc_info
设置为 True,那么上述程序的输出将不会显示任何异常信息,在现实世界的应用中,这些异常可能远比一个除以零的错误(ZeroDivisionError
)要复杂。
鉴于记录错误是一种常规操作,日志模块提供了一个便捷函数,可以减少您的代码输入量。如果您是在异常处理程序中进行日志记录——您将在后文中了解到更多细节——那么您可以使用 logging.exception()
函数。此函数会以错误(ERROR)级别记录一条消息,并将异常详情附加到这条消息上。
以下示例展示了如何使用 logging.exception()
函数来获得与之前相同的日志输出结果:
>>> try:
... donuts_per_guest = donuts / guests
... except ZeroDivisionError:
... logging.exception("DonutCalculationError")
...
使用 logging.exception()
函数的效果等同于使用 logging.error()
函数并设置 exc_info
参数为 True。因为这个函数总是会记录下异常信息,所以您应当仅在捕获到异常时调用它。
调用 logging.exception()
会生成一条错误级别的日志条目。如果您不希望以错误级别记录,您可以使用日志模块中其他级别的函数,比如 debug()
、info()
、warning()
、error()
或 critical()
,并在调用时将 exc_info
参数设置为 True,以便包含异常信息。