18 KiB
如何开始使用 Python 进行日志记录
原文:https://www.pythoncentral.io/how-to-start-logging-in-python/
Python 提供了对日志记录的内置支持,为程序员提供了对其应用程序的关键可见性,而没有太多麻烦。
要理解 Python 日志模块,需要了解语言中相应的日志 API 以及使用它们的步骤。在使用这些 API 进行无故障日志记录时,学习最佳实践也同样重要。
本指南向您介绍了许多与 Python 日志相关的概念。虽然 Python 从 2.3 版起就提供了内置的日志支持,但本文是基于 Python 3.8 版的。它假设读者了解与通用和面向对象编程相关的概念和结构。
什么是 Python 日志记录?
标准 Python 库有一个内置的日志模块,允许用户轻松地记录来自库和应用程序的事件。配置好记录器后,只要代码运行,它就作为 Python 解释器的一部分运行。简单来说;它是全球性的。
Python 还允许用户使用外部配置文件来配置日志子系统。您可以在 Python 库中找到日志配置格式的规范。
内置日志库遵循模块化方法,具有以下组件:
- 记录器: 这些暴露了应用程序使用的接口
- 处理程序: 这些处理程序将记录器生成的日志记录发送到正确的目的地
- 过滤器: 他们过滤记录并决定输出哪一个
- 格式化程序: 这些定义了最终日志条目的布局。
几个 logger 对象被组织成一棵树,代表系统的许多部分和安装在其上的各种第三方库。当用户向这些记录器中的一个发送消息时,消息由它们的格式化程序输出到所有的处理程序。
消息沿记录器树向上移动,直到到达配置了 propagate=False 的记录器或根记录器。
Python 日志记录是如何工作的?
下面是 Python 用户使用日志库时触发的所有任务的分类:
客户端运行日志记录语句并发出日志请求。这些语句通常调用日志库 API 中的方法,提供日志级别和日志数据作为参数。
日志级别参数定义了请求的重要性。日志数据通常是日志消息字符串,但也记录一些其他数据。logger 对象通常公开日志 API。
然后,日志记录库创建一个代表日志请求的日志记录,并捕获相关数据,以便在请求穿过该库时能够对其进行处理。
按照程序库的指示过滤日志请求/记录。在过滤期间,将日志记录级别请求与阈值日志记录级别进行比较。然后记录通过用户提供的过滤器。
最后,处理程序检查过滤后的记录,通常通过将数据写入文件来存储数据。但是,可以设置处理程序将日志数据通过电子邮件发送到特定的地址。
某些日志库中的处理程序可能会根据日志级别和用户指定的处理程序过滤器对日志记录进行第二次过滤。需要时,处理程序还依赖用户提供的格式化程序将日志数据格式化为日志条目。
Python 中的日志模块
Python 标准库有以下模块为日志提供支持:
- 日志模块: 具有主要的面向客户端的 API。
- logging . config 模块: 它有允许客户端配置的 API。
- logging . handlers 模块: 它提供了以不同方式处理和存储日志记录的各种处理程序。
这些模块统称为 Python 日志库,它们将前面章节中讨论的概念具体化为类、模块级方法或常量。
Python 中的日志记录级别
Python 中的日志库支持五个日志级别:关键、错误、警告、信息和调试。
它们由同名的常量表示,比如:logging。临界,记录。错误,记录。警告,记录中。这些常量的值分别为 50、40、30、20 和 10。
运行期间,日志级别的值表示该级别的含义。通过使用大于零且不等于预定义的日志记录级别值的值,客户端还可以使用其他日志记录级别。
当日志记录级别的名称可用时,它们在日志条目中按其名称显示。每个预先存在的日志记录级别都与相应的常数同名。例如,伐木。警告和 30 个级别在日志条目中显示为“警告”。
另一方面,默认情况下,自定义日志记录级别没有任何名称。因此,Python 在条目中打印一个值为 n 的自定义级别(作为“级别 n”)。
但是,当涉及多个自定义日志记录级别时,日志条目可能会变得混乱,因为条目之间使用了相同的默认名称。
为了防止这个问题,客户端可以使用 logging.addLevelName 函数通过传递 level 和 LevelName 参数来命名它们的自定义日志记录级别。
换句话说,运行 logging.addLevelName(43,' CUSTOM1 ')会将级别 43 记录为日志条目中的' CUSTOM1 '。
官方文档概述了 Python 日志库中内置的五个日志级别的 社区范围的适用性规则 。这些准则表明:
- 使用调试级别记录诊断问题的详细信息。
- 使用信息级别确认应用程序是否按预期运行。
- 使用警告级别来报告意外行为,这些行为可能预示着未来的问题,但目前不会影响应用程序的正常运行。
- 使用错误等级报告应用程序运行中的严重问题。
- 使用临界级别报告错误,表明程序可能无法继续运行。
Python 记录器的功能
伐木。记录器对象(简称为“记录器”)充当用户和库之间的接口。这些对象便于发出日志请求,还提供了查询和修改方法状态的方法。
通常,程序员使用 logging.getLogger(name)工厂函数来创建记录器。使用函数,客户端可以按名称访问和管理记录器。不需要存储和传递对记录器的引用!
函数中传递的参数一般是用点分隔的层次名称(如 a.b.c)。以这种方式传递值允许库维护记录器层次结构。
当记录器被创建时,库确保在每个层级都有一个记录器。它还确保所有记录器都链接到它们的父记录器和子记录器。
这也是阈值日志记录级别的概念出现的地方。每个日志记录器都有一个阈值级别来帮助确定是否需要处理请求。
如果请求的日志记录级别的值等于或高于阈值级别,则记录器处理日志请求。
客户端可以使用 Logger.getEffectiveLevel()和 Logger.setLevel(level)函数来检索和更改记录器的阈值日志记录级别。
工厂功能将记录器的阈值水平设置为与相同的值
父。Python 日志记录方法
每个日志记录器都有以下方法来方便发出日志请求:
- Logger.critical(msg,*args,**kwargs)
- Logger.error(msg,*args,**kwargs)
- 记录器. debug(msg、*args、**kwargs)
- 记录器. info(msg、*args、**kwargs)
- Logger.warn(msg,*args,**kwargs)
用户可以使用这些方法发出具有预定义日志记录级别的请求。除了这些方法,记录器还提供了另外两种方法:
- **Logger.log(level,msg,*args,kwargs): 它以明确指定的日志记录级别发出日志请求。在使用自定义日志记录级别时,这很有帮助。
- **Logger.exception(msg,*args,kwargs): 它发出带有错误级别的请求,并在日志条目中捕获当前异常。因此,客户端应该只从异常处理程序中使用此方法。
这些方法中的“msg”和“args”参数一起在日志条目中创建日志消息。
此外,上述所有方法都支持“exc_info”参数,允许客户端将异常信息添加到日志条目中。还支持“stack_info”和“stacklevel”参数,允许用户向日志条目添加调用堆栈详细信息。
关键字参数“extra”使客户端能够传递相关的过滤器、处理程序和格式化程序值。
当这些方法运行时,它们执行上一节提到的所有任务,然后执行以下任务:
- 在比较了阈值和日志记录级别之后,记录器决定处理日志请求,它创建一个 LogRecord 对象。对象表示请求的下游处理中的请求。设置 LogRecord 对象是为了捕获方法的参数、调用堆栈细节和异常。它还可以将额外参数中的值和键捕获为字段。
- 每个处理程序处理一个日志请求,它的前身记录器的处理程序在记录器层次结构中适当地处理请求。记录器控制 handlers.propagate 字段的这一方面,默认情况下是这样的。
过滤器不仅仅是一种与日志记录级别交互的手段。它们允许对请求进行细粒度过滤,并忽略特定类中的请求。用户可以使用 Logger.addFilter(filter)和 Logger.removeFilter(filter)方法分别在记录器中添加和删除筛选器。
Python 中的日志过滤器
任何接受日志作为参数并返回零(拒绝)或非零值(接受)以接受记录的可调用函数都是过滤器。具有签名过滤器方法的对象(记录:日志记录)- > int 也可以是一个过滤器。
日志记录的子类。可以覆盖日志记录的 Filter(name)方法。Filter.filter(record)方法也可以成为筛选器。
但是,这种类型的过滤器会将记录器生成的记录设置为与子过滤器同名,而不会覆盖过滤器方法。
如果过滤器名称为空,则过滤器接纳所有记录。另一方面,当方法被重写时,它返回零以拒绝记录,或者返回非零以接受记录。
Python 中的日志处理程序
伐木。处理程序对象负责处理 Python 中的日志记录。这些组件负责记录日志请求。处理程序的最终处理通常包括通过将记录写入系统日志或其他文件来存储记录。
然而,最终处理也可能涉及将其发送给其他实体或通过电子邮件将记录数据发送到特定的电子邮件地址。
与记录器一样,处理程序也有一个阈值日志记录级别。客户端可以使用 Handler.setLevel(level)方法设置它,使用 Handler.addFilter(filter)和 Hander.removeFilter(filter)方法支持过滤器。
处理程序使用过滤器和阈值日志记录级别来过滤要处理的记录。额外的过滤允许对记录进行上下文控制,这意味着通知处理程序将只处理来自损坏或关键模块的请求。
当处理程序处理日志记录时,记录被格式化器格式化成日志条目。客户端可以使用 Handler.setFormatter(formatter)方法来设置格式化程序。不带格式化程序的处理程序可以与 Python 标准库中的默认格式化程序一起使用。
logging.handler 模块有十几个不同的处理程序,可以在几种情况下使用。因此,在大多数情况下,客户端可以快速配置这些处理程序。
然而,有些情况下需要使用自定义处理程序。在这些情况下,开发人员可以使用 Handler.emit(record)方法扩展处理程序类或他们选择的预定义处理程序类。
Python 中的日志格式化程序
伐木。格式化程序对象对于处理程序来说是无价的,因为它们将日志记录格式化为基于字符串的日志条目。然而,重要的是要记住格式化程序不能控制日志消息的创建。
格式化程序将日志记录中的数据或字段与用户指定的格式字符串相结合。但是与处理程序不同,Python 日志库只有一个基本的格式化程序来记录级别、日志记录者的名字和消息。
因此,如果客户需要一个超越简单用例的应用格式化程序,他们将需要构建新的日志记录。具有所需格式字符串的格式化程序对象。
格式化程序支持格式字符串的 printf、str.format()和 str.template 样式。此外,格式字符串可以是 LogRecord 对象的任何字段,包括基于额外参数的键的字段。
在格式化日志记录之前,格式化程序使用 LogRecord.getMessage()结合日志记录方法的 msg 和 args 参数,使用%运算符来构造日志消息。
格式化程序最终使用指定的格式字符串将日志消息与日志记录数据结合起来,以创建条目。
Python 中的日志模块
当客户端起诉日志库时,该库建立一个根日志记录器来维护日志记录器的层次结构。所有记录器的根记录器的默认阈值记录级别是 logging.WARNING.
该模块提供 Logger 类的所有日志记录方法,作为具有相同名称和签名的模块级方法。客户端可以使用这些方法发出日志请求,而无需创建日志记录器,最终由根日志记录器处理所有请求。
也就是说,如果根日志记录器在服务方法中的日志请求时没有任何处理程序,那么日志记录库会添加一个日志记录。使用 sys.stderr 流作为根记录器的处理程序的 StreamHandler 实例。这个实例被称为最后的处理程序。
给没有处理程序的记录器的日志请求被日志库定向到最后一个处理程序。客户端可以使用 logging.lastResort 属性访问处理程序。
Python 日志记录的良好实践
下面我们为您重点介绍了登录 Python 的最佳实践。然而,需要注意的是,没有灵丹妙药。在使用任何这些实践之前,最好考虑它们在您的程序中的适用性,并考虑它们的适当性。
#1 使用 getlogger 函数
logging.getLogger()函数使库能够管理记录器名称到记录器实例的映射,并维护记录器的层次结构。映射和层次因此提供了以下好处:
- 通过按名称检索记录器,客户可以使用该功能从程序的不同部分访问记录器。
- 运行时创建有限的记录器。
- 日志请求可以在日志记录器层次结构中传播。
- 未指定时,阈值日志记录级别可以从以前的日志记录程序中推断出来。
- 库的配置可在运行时使用记录器名称进行更新。
#2 使用预定义的日志记录级别
库中预定义的日志记录级别涵盖了几乎所有的日志记录场景。此外,大多数开发人员都熟悉预定义的日志记录级别,因为跨编程语言的日志记录库具有相似的级别。
使用预定义的级别可以简化配置、部署和维护。除非必要,否则最好使用预定义的级别。
#3 创建模块级记录器
客户端可以为每个类和每个模块创建日志程序。为每个类创建记录器允许详细的配置;然而,它加载了几个记录器的程序。
另一方面,为每个模块创建记录器可以使记录器的总数保持较小。因此,只有当细粒度配置是必须的时候,使用类级记录器才是理想的。
#4 命名模块级记录器与模块相同
记录器的名称是字符串值,不包含在 Python 名称空间中。因此,它们不会与模块名冲突。
使用相应模块级记录器的模块名使得在代码中识别它们变得轻而易举。以这种方式命名日志程序使用了点符号,使得引用日志程序变得容易。
#5 使用日志记录注入本地上下文信息。记录器适配器方法
将上下文信息插入日志记录就像使用日志记录一样简单。LoggerAdapter 方法客户端也可以使用它来修改日志请求中的消息和数据。
日志库不管理这些适配器,这意味着不能使用通用名称访问它们。因此,开发人员根据需要使用它们将上下文信息插入到类或模块中。
#6 避免使用过滤器插入全局上下文信息
将全局上下文信息插入日志记录就像使用过滤器修改提供给过滤器的记录参数一样简单。
您可以编写这样一个过滤器,将详细信息插入到传入的日志记录中:
def version_injecting_filter(logRecord):
logRecord.version = '3'
return True
然而,这种方法有两个缺点。首先,如果过滤器从日志记录中获取数据,那么将数据注入到记录中的过滤器必须在使用注入数据的过滤器之前运行。过滤器添加到记录器的顺序变得至关重要。
其次,过滤器扩展了日志记录,滥用支持来过滤它们。
还有另一种方法将上下文信息注入到日志记录器中——您可以使用 logging.setLogRecordFactory()函数初始化日志库。假设注入的信息是全局的,当工厂函数创建日志记录时,客户端可以将它注入到日志记录中。
使用 logging.setLogRecordFactory()函数是确保数据在程序中的每个日志组件中都可用的一个很好的方法。然而,这种方法也有缺点:
客户必须确保程序不同部分的工厂功能能够很好地相互配合。而客户端可以链接日志记录工厂函数,使程序变得不必要的复杂。
#7 将通用处理程序放在记录器的高层
如果两个记录器有相同的处理程序,并且其中一个是另一个的后代,那么最好将处理程序附加到上级记录器。客户端可以依靠日志库将请求从下层日志记录器传播到上层日志记录器的处理程序。
此外,如果 propagate 属性没有被修改,将处理程序放在上层记录器中可以防止重复消息的出现。
#8 使用 logging.disable()函数调节记录输出
如果请求的记录级别等于或高于记录器的有效记录级别,它将处理该请求。但是有效的日志记录级别由阈值日志记录级别和库范围的日志记录级别之间的较高者来确定。
客户端可以使用 logging.disable(level)方法来设置库范围的日志记录级别。默认情况下,此级别为 0;因此,它处理每个日志记录级别的请求。
使用该函数是提高整个程序的日志记录级别和抑制程序的日志记录输出的一个很好的方法。
#9 缓存对记录器的引用考虑周到
缓存对记录器的引用并使用缓存的引用来访问它们有助于避免重复调用 logging.getLogger()函数来获取相同的记录器,从而提高程序的效率。
也就是说,如果检索不是冗余的,这些调用函数的消除会导致丢失日志请求。更新对记录器的引用会有所帮助,但是使用 logging.getLogger()函数是避免这个问题的最好方法。