本文翻译自 opensource,原文章地址为:A guide to logging in Python,由于本人各方面能力有限,如翻译有纰漏,请同学们不吝纠正。


作为一个开发人员,有一点很蛋疼的事情就是我们需要弄清楚为什么我们编写的一个程序为什么不能正常工作,因为我们不知道它真正运行的时候程序内部正在怎么运行。这就导致有时你甚至不敢保证开发的系统能够按照当初设计的那样工作。

当程序在生产环境中运行的时候,它们就变成一个需要跟踪和监视的黑盒子。我们要想知道黑盒子里面正在干什么,最简单也是最重要的方法之一就是 日志。通过 日志,我们在开发软件时指示程序在系统运行时给出有助于开发人员和系统管理员的信息。

就像我们在编写代码时为以后的开发人员编写注释文档一样,在开发一个新程序时,我们也应该考虑为后续的开发者和系统管理员提供充足的日志。日志作为应用程序在运行时状态时关键的系统文档的一部分,在使用日志调试软件时,请为将来维护系统的开发人员和系统管理员考虑,编写好这份文档。

本文将介绍 Python 的 logging 日志模块,它被设计用于适应更复杂用例。这不是一篇作为开发人员的文档,而是作为指导,介绍如何构建 Python 日志记录模块,希望能够对有兴趣的同学深入日志模块有所帮助。

为什么要使用日志模块

可能有同学会争辩,为什么不是简单的 print 语句呢?使用日志模块有很多优势,包括:

其中最后一点,我们怎么输出日志和我们要输出什么例子是软件中不同的部分。例如,logging 模块允许框架或库的开发人员添加日志,并让系统管理员或负责运行时配置的人员决定以后应该记录的内容。

记录模块中有什么

logging 模块精美分离它的每一个部件的责任(按照 Apache 的 Log4j API 的做法)。我们来看看一个日志行如何在模块的代码周围传播,并探索其不同的部分。

Logger

Logger 是开发人员通常进行交互的对象,它们是我们使用 log 的主要 API。给定一个 logger 的实例,我们可以分类和指定要记录的消息,但是不需要考虑它们将如何或何处被写出。

例如,当我们写 logger.info("Stock was sold at %s", price) 时,我们考虑到以下模式:

我们要求写出一行日志,然后 logger 在内部执行了一些内置的代码,最后这行日志就被添加到了 console 或者 文件 中,但是,内部发生了什么我们现在还不太清楚。

Log records

Log records 是 logging 模块中用于传递所有必需信息的软件包,它们包含有关请求 log 的函数/传递的字符串/参数/调用堆栈信息等。这些都是被 log 处理的对象。我们每次调用我们的 logger 时,都会创建这些对象的实例,但是如何将这些对象序列化成流的?那就的看看 Handlers 了!

Handlers

handlers 可以将日志记录发送到任何输出,他们收集日志记录并通过内置的函数处理它们。例如,一个FileHandler 将获取一个日志记录并将其添加到一个文件中。

标准库 logging 模块已经包含多个内置的处理程序,如:

我们现在有一个更贴近现实的模式了:

但大多数处理程序都只能处理简单的字符串(SMTPHandler,FileHandler等),因此您可能会想知道如何将一些结构化的 LogRecords 转换为易于序列化的字节...

Formatters

下面让我来介绍一些 FormattersFormatters 负责将富含元数据的 LogRecord 序列化成字符串,如果什么格式都不提供,将会有默认的 Formatter 可以使用。logging 库提供的通用格式化程序类可以采用模板和样式作为输入,里面可以使用 LogRecord 对象中的所有属性来声明占位符。

例如:'%(asctime)s%(levelname)s%(name)s:%(message)s' 将生成日志,如 2017-07-19 15:31:13,942 INFO parent.child:Hello EuroPython

所有默认属性都可以在日志记录文档中找到。

现在我们知道了格式化器,我们的模型又将发生改变了:

Filters

我们的 logging 工具包中的最后一个对象是 filters。filter 允许对哪些日志进行更细粒度的控制。多个 filter 可以连接到 logger 和 handler,对于要写出的日志,所有过滤器应允许记录通过。用户可以使用过滤方法将自己的过滤器声明为对象,该过滤方法将记录作为输入并返回 True / False 作为输出。

考虑到这一点,这里是当前的日志记录工作流程:

Logger 层次结构

现在,可能模块隐藏的复杂性和配置的数量会给您留下深刻的印象,但还有更多的考虑:Logger 的层次结构。

我们可以通过 logging.getLogger( 创建一个 logger。作为参数传递给getLogger 的字符串可以通过使用点分隔元素来定义层次结构。

例如,logging.getLogger(“parent.child”) 将创建一个名为 parent 的父记录器的记录器 child。记录器是由日志记录模块管理的全局对象,因此可以在项目中随时随地检索。

logger 实例也称为通道。层次结构允许开发人员定义通道及其层次。在日志记录传递到 logger 中的所有handler 之后,父进程将被递归调用,直到达到顶级记录器(定义为空字符串)或记录器已配置 propagate = False。我们可以在更新的图表中看到它:

请注意,父 logger 不被调用,只有其处理程序。这意味着 logger 类中的 filter 和其他代码将不会在父级上执行。这是向 logger 添加 filter 时的常见缺陷。

重新绘制工作流程

我们已经检查了责任分工,以及如何对日志过滤进行微调。但还有另外两个属性我们还没有提到:

  1. logger 可以被禁用,从而不允许从它们发出任何记录。
  2. 可以在 logger 和 handler 中配置有效级别。

作为一个例子,当一个 logger 已经被配置了 INFO 级别,因此仅仅 INFO 及以上级别将被传递。同样的规则适用于处理程序。考虑到这一切,记录文档中的最终流程图如下所示:

如何使用日志记录

现在我们已经看过了日志记录模块的部件和设计,现在是时候来研究开发人员如何与它进行交互。这是一个代码示例:

这将使用模块 name 创建一个 logger。它将根据项目结构创建渠道和层次结构,因为 Python 模块与点连接。logger 变量引用记录器 "module",将 "projectA"作为父项,以 "root" 作为其父项。在第5行,我们将看到如何执行调用以发出日志。我们可以使用调试,信息,错误或关键的方法之一使用适当的级别进行日志记录。

记录消息时,除了模板参数之外,我们可以传递具有特定含义的关键字参数。最有趣的当属 exc_infostack_info。这些将分别添加有关当前异常和堆栈帧的信息。为方便起见,记录器对象中可以使用方法异常,这与使用 exc_info = True 的调用错误相同。

这些是如何使用记录器模块的基础知识。ʘ‿ʘ。但也值得一提的是下面这些通常被认为是坏习惯的用法。

1. 贪婪的字符串格式化

应尽可能避免使用 loggger.info("string template {}".format(argument)),而应该用 logger.info("string template %s", argument),这是一个更好的做法,因为实际的字符串插值将仅在日志被发出时使用。当我们在高于 INFO 级别时输出这个这个日志时,将会浪费一次渲染,因为这在事实上将不会被写出,从而多调用了一次解释器。

2. 捕获和格式化异常

很多时候,我们想在 catch 块中记录有关异常的信息,并且可能会直观地使用:

但是这段代码给我们输出的日志行将类似于:Something bad happened: "secret_key."。 这不是很有用,如果我们使用如前所述的 exc_info,它将产生以下内容:

这就不仅仅包含异常的确切来源,而且还包含类型。

配置 logger

将 logging 组装进我们的程序中很容易,但是我们需要配置日志记录堆栈,并指定日志的发布方式。有多种方法可以用来配置日志记录堆栈。

BasicConfig

这是配置日志记录的最简单的方法。只要做 logging.basicConfig(level="INFO") ,就会设置一个基本的 StreamHandler,它将记录 INFO 和以上级别的所有内容到控制台。有自定义此基本配置的参数。其中一些是:

注意,basicConfig 只能在运行时第一次调用。如果您已经配置了根 logger ,调用 basicConfig 将不起作用。

DictConfig

所有元素的配置和如何连接它们都可以指定为字典。该字典应具有不同的部分用于 logger ,handler,formatter 和一些基本的全局参数。

这里有一个例子:

最后一句调用时,dictConfig 将禁用所有现有的记录器,除非 disable_existing_loggers 设置为 false。这通常是必要的,因为在调用dictConfig 之前,许多模块声明将在导入时被实例化的全局 logger。您可以看到可用于 dictConfig 方法的模式。通常,此配置存储在 YAML 文件中,并从中配置。许多开发人员经常喜欢使用 fileConfig,因为它可以更好地支持定制。

扩展日志记录

由于 logging 的设计方式,扩展日志模块很容易。我们来看一些例子:

JSON 日志

如果我们想要,我们可以通过创建自定义格式化程序来将日志记录转换为 JSON 编码的字符串来记录 JSON 日志:

添加上下文

在格式化器上,我们可以指定任何日志记录属性。我们可以以多种方式注入属性。在这个例子中,我们滥用过滤器来丰富记录。

这有效地为通过 logger 的所有记录添加了一个属性。格式化程序将在日志行中包含它。

请注意,这会影响应用程序中的所有日志记录,包括您可能正在使用的库或其他框架,并且您正在发布日志。它可用于记录所有日志行上的唯一请求ID以跟踪请求或添加额外的上下文信息。

从Python 3.2开始,您可以使用 setLogRecordFactory 捕获创建所有日志记录并注入额外的信息。该额外的属性和 LoggerAdapter 类也可能是感兴趣的。

缓冲日志

有时我们想要在发生错误时访问调试日志。这是可行的,通过创建一个缓冲的处理程序,将在发生错误后记录最后一个调试消息。请参阅以下代码作为非策略的示例:

Reference

  1. A guide to logging in Python