Python|日志记录详解(1)

引言

作为一名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,以便包含异常信息。

未完待续,欢迎关注!

动动您发财的小手点个赞吧!欢迎转发!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容