日志记录手册 — Python 文档
记录食谱
- 作者
- 维奈沙吉普
此页面包含许多与日志记录相关的方法,这些方法在过去很有用。
在多个模块中使用日志记录
多次调用 logging.getLogger('someLogger')
返回对同一个记录器对象的引用。 这不仅在同一个模块内如此,而且在跨模块时也是如此,只要它在同一个 Python 解释器进程中。 对同一对象的引用也是如此; 此外,应用程序代码可以在一个模块中定义和配置父记录器,并在单独的模块中创建(但不配置)子记录器,所有对子记录器的调用都将传递给父记录器。 这是一个主要模块:
这是辅助模块:
输出如下所示:
从多个线程记录
从多个线程进行日志记录不需要特别的努力。 以下示例显示了来自主(初始)线程和另一个线程的日志记录:
运行时,脚本应打印如下内容:
这显示了日志输出,正如人们所期望的那样。 当然,这种方法适用于比此处显示的更多的线程。
多个处理程序和格式化程序
记录器是普通的 Python 对象。 addHandler() 方法对于您可以添加的处理程序数量没有最小或最大配额。 有时,应用程序将所有严重性的所有消息记录到文本文件中,同时将错误或更高级别的错误记录到控制台是有益的。 要设置它,只需配置适当的处理程序。 应用程序代码中的日志调用将保持不变。 下面是对之前简单的基于模块的配置示例的轻微修改:
请注意,“应用程序”代码不关心多个处理程序。 改变的只是添加和配置了一个名为 fh 的新处理程序。
在编写和测试应用程序时,创建具有更高或更低严重性过滤器的新处理程序的能力非常有用。 与其使用许多 print
语句进行调试,不如使用 logger.debug
:与 print 语句不同,稍后您必须将其删除或注释掉,logger.debug 语句可以在源代码中保持完整并保持休眠状态,直到您再次需要它们。 那时,唯一需要发生的变化是修改记录器和/或处理程序的严重性级别以进行调试。
记录到多个目的地
假设您想在不同的情况下使用不同的消息格式登录到控制台和文件。 假设您要将 DEBUG 及更高级别的消息记录到文件中,并将 INFO 及更高级别的消息记录到控制台。 我们还假设文件应该包含时间戳,但控制台消息不应该。 以下是您如何实现这一目标:
当你运行它时,在控制台上你会看到
在文件中你会看到类似的东西
如您所见,DEBUG 消息仅显示在文件中。 其他消息被发送到两个目的地。
此示例使用控制台和文件处理程序,但您可以使用您选择的任意数量和组合的处理程序。
配置服务器示例
下面是一个使用日志配置服务器的模块示例:
这是一个脚本,它接受一个文件名并将该文件发送到服务器,正确地以二进制编码的长度开头,作为新的日志配置:
通过网络发送和接收日志事件
假设您想通过网络发送日志事件,并在接收端处理它们。 一个简单的方法是将 SocketHandler
实例附加到发送端的根记录器:
在接收端,您可以使用 SocketServer 模块设置接收器。 这是一个基本的工作示例:
首先运行服务器,然后是客户端。 在客户端,控制台上没有打印任何内容; 在服务器端,您应该看到如下内容:
请注意,在某些情况下,pickle 存在一些安全问题。 如果这些影响您,您可以通过覆盖 makePickle() 方法并在那里实现您的替代方案,以及调整上述脚本以使用您的替代序列化方案来使用替代序列化方案。
将上下文信息添加到日志输出
有时,除了传递给日志调用的参数之外,您还希望日志输出包含上下文信息。 例如,在网络应用程序中,可能需要在日志中记录客户端特定的信息(例如 远程客户端的用户名或 IP 地址)。 尽管您可以使用 extra 参数来实现这一点,但以这种方式传递信息并不总是很方便。 虽然在每个连接的基础上创建 Logger 实例可能很诱人,但这不是一个好主意,因为这些实例不会被垃圾收集。 虽然这在实践中不是问题,但当 Logger 实例的数量取决于您要在记录应用程序时使用的粒度级别时,如果 的数量可能难以管理Logger 实例实际上是无界的。
使用 LoggerAdapters 传递上下文信息
传递上下文信息和日志事件信息一起输出的一种简单方法是使用 LoggerAdapter 类。 这个类被设计成一个Logger,这样你就可以调用debug()、info()、warning() , error(), exception(), critical() 和 log()。 这些方法与 Logger 中的对应方法具有相同的签名,因此您可以互换使用这两种类型的实例。
当你创建一个 LoggerAdapter 的实例时,你传递给它一个 Logger 实例和一个包含上下文信息的类似 dict 的对象。 当您在 LoggerAdapter 的实例上调用其中一种日志记录方法时,它会将调用委托给传递给其构造函数的 Logger 的底层实例,并安排在委托调用。 这是 LoggerAdapter 的代码片段:
LoggerAdapter 的 process() 方法是将上下文信息添加到日志输出的地方。 它传递了日志记录调用的消息和关键字参数,并将它们的(可能)修改版本传回以在对底层记录器的调用中使用。 此方法的默认实现不处理消息,而是在关键字参数中插入一个“额外”键,其值是传递给构造函数的类似 dict 的对象。 当然,如果您在对适配器的调用中传递了“额外”关键字参数,它将被静默覆盖。
使用 'extra' 的好处是类似 dict 的对象中的值被合并到 LogRecord 实例的 __dict__ 中,允许您将自定义字符串与您的 Formatter 实例一起使用,这些实例知道类似 dict 的对象的键。 如果您需要不同的方法,例如 如果您想在消息字符串中添加或附加上下文信息,您只需要子类化 LoggerAdapter 并覆盖 process() 来执行您需要的操作。 这是一个简单的例子:
你可以这样使用:
然后,您记录到适配器的任何事件都将在日志消息前面加上 some_conn_id
的值。
使用 dicts 以外的对象传递上下文信息
您不需要将实际的 dict 传递给 LoggerAdapter - 您可以传递一个实现 __getitem__
和 __iter__
的类的实例,使其看起来像一个 dict到日志记录。 如果您想动态生成值(而 dict 中的值将是常量),这将很有用。
使用过滤器传递上下文信息
您还可以使用用户定义的 过滤器 将上下文信息添加到日志输出。 Filter
实例可以修改传递给它们的 LogRecords
,包括添加额外的属性,然后可以使用合适的格式字符串输出,或者如果需要自定义 Formatter。
例如,在 Web 应用程序中,正在处理的请求(或至少是其中有趣的部分)可以存储在线程本地 (threading.local) 变量中,然后从 [ X202X] 将来自请求的信息(例如远程 IP 地址和远程用户的用户名)添加到 LogRecord
,使用 [ X382X] 上面的例子。 在这种情况下,可以使用相同的格式字符串来获得与上述类似的输出。 这是一个示例脚本:
它在运行时会产生如下结果:
从多个进程记录到单个文件
尽管日志记录是线程安全的,并且支持从单个进程中的多个线程记录到单个文件 是 ,但从 多个进程 记录到单个文件是 不是 ] 支持,因为没有标准方法可以在 Python 中跨多个进程序列化对单个文件的访问。 如果您需要从多个进程登录到单个文件,一种方法是让所有进程登录到 SocketHandler,并有一个单独的进程来实现从套接字读取的套接字服务器并记录到文件中。 (如果您愿意,可以在现有进程之一中指定一个线程来执行此功能。) 本节 更详细地记录了这种方法,并包括一个可用作起点的工作套接字接收器以便您适应自己的应用程序。
如果您使用的是包含 multiprocessing 模块的最新版本的 Python,您可以编写自己的处理程序,该处理程序使用该模块中的 Lock 类来序列化您的进程对文件的访问. 现有的 FileHandler 和子类目前没有使用 multiprocessing,尽管它们将来可能会这样做。 请注意,目前,multiprocessing 模块并未在所有平台上提供工作锁功能(参见 https://bugs.python.org/issue3770)。
使用文件轮换
有时您想让日志文件增长到特定大小,然后打开一个新文件并记录到该文件。 您可能希望保留一定数量的这些文件,并在创建了这么多文件后,旋转这些文件,以便文件数量和文件大小都保持有界。 对于这种使用模式,日志包提供了一个 RotatingFileHandler:
结果应该是 6 个单独的文件,每个文件都包含应用程序的日志历史记录的一部分:
最新的文件始终是 logging_rotatingfile_example.out
,每次达到大小限制时,都会使用后缀 .1
重命名。 每个现有备份文件都被重命名以增加后缀(.1
变为 .2
等),并删除 .6
文件。
显然,这个例子将日志长度设置得太小作为一个极端的例子。 您可能希望将 maxBytes 设置为适当的值。
基于字典的配置示例
下面是一个日志配置字典的例子——它取自 Django 项目 上的 文档。 这个字典被传递给 dictConfig() 以使配置生效:
有关此配置的更多信息,您可以查看 Django 文档的 相关部分 。
将 BOM 插入发送到 SysLogHandler 的消息中
RFC 5424 要求将 Unicode 消息作为具有以下结构的一组字节发送到系统日志守护程序:一个可选的纯 ASCII 组件,后跟一个 UTF-8 字节顺序标记 (BOM),后跟使用 UTF-8 编码的 Unicode。 (请参阅规范 的 相关部分。)
在 Python 2.6 和 2.7 中,代码被添加到 SysLogHandler 以将 BOM 插入消息中,但不幸的是,它的实现不正确,BOM 出现在消息的开头,因此不允许任何纯 -出现在它之前的 ASCII 组件。
由于此行为被破坏,错误的 BOM 插入代码将从 Python 2.7.4 及更高版本中删除。 但是,它不会被替换,并且如果您想生成符合 RFC 5424 的消息,其中包括 BOM、它之前的可选纯 ASCII 序列和它之后的任意 Unicode(使用 UTF-8 编码),那么您需要执行下列的:
将 Formatter 实例附加到您的 SysLogHandler 实例,使用格式字符串,例如:
Unicode 代码点
u'\ufeff'
,当使用 UTF-8 编码时,将被编码为 UTF-8 BOM——字节串'\xef\xbb\xbf'
。用你喜欢的任何占位符替换 ASCII 部分,但要确保替换后出现在那里的数据始终是 ASCII(这样,它在 UTF-8 编码后将保持不变)。
用你喜欢的任何占位符替换 Unicode 部分; 如果替换后出现的数据包含 ASCII 范围之外的字符,那很好——它将使用 UTF-8 编码。
如果格式化的消息是 Unicode,则 将 由 SysLogHandler
使用 UTF-8 编码进行编码。 如果您遵循上述规则,您应该能够生成符合 RFC 5424 的消息。 如果不这样做,日志记录可能不会抱怨,但您的消息将不符合 RFC 5424,并且您的 syslog 守护程序可能会抱怨。
实现结构化日志
尽管大多数日志消息是供人类阅读的,因此不容易机器解析,但在某些情况下,您可能希望以结构化格式输出消息,该格式的 是 能够被程序解析(没有需要复杂的正则表达式来解析日志消息)。 使用 logging 包可以直接实现这一点。 有多种方法可以实现这一点,但以下是一种简单的方法,它使用 JSON 以机器可解析的方式序列化事件:
如果运行上面的脚本,它会打印:
请注意,项目的顺序可能会根据所使用的 Python 版本而有所不同。
如果您需要更专业的处理,您可以使用自定义 JSON 编码器,如以下完整示例所示:
当上面的脚本运行时,它会打印:
请注意,项目的顺序可能会根据所使用的 Python 版本而有所不同。
使用 dictConfig() 自定义处理程序
有时您想以特定方式自定义日志处理程序,如果您使用 dictConfig() ,您可能无需子类化就可以做到这一点。 例如,考虑您可能想要设置日志文件的所有权。 在 POSIX 上,这可以使用 os.chown() 轻松完成,但 stdlib 中的文件处理程序不提供内置支持。 您可以使用普通函数自定义处理程序创建,例如:
然后,您可以在传递给 dictConfig() 的日志记录配置中指定通过调用此函数来创建日志记录处理程序:
在此示例中,我使用 pulse
用户和组设置所有权,仅用于说明目的。 把它组合成一个工作脚本,chowntest.py
:
要运行它,您可能需要以 root
的身份运行:
$ sudo python3.3 chowntest.py
$ cat chowntest.log
2013-11-05 09:34:51,128 DEBUG mylogger A debug message
$ ls -l chowntest.log
-rw-r--r-- 1 pulse pulse 55 2013-11-05 09:34 chowntest.log
请注意,此示例使用 Python 3.3,因为这是 shutil.chown()
出现的地方。 这种方法应该适用于任何支持 dictConfig() 的 Python 版本——即 Python 2.7、3.2 或更高版本。 对于 3.3 之前的版本,您需要使用例如实现实际的所有权更改 os.chown()。
实际上,处理程序创建函数可能位于项目中某处的实用程序模块中。 而不是配置中的行:
你可以使用例如:
其中 project.util
可以替换为函数所在的包的实际名称。 在上面的工作脚本中,使用 'ext://__main__.owned_file_handler'
应该可以工作。 在这里,实际的可调用对象由 ext://
规范中的 dictConfig() 解析。
希望这个例子也指出了如何实现其他类型的文件更改的方法 - 例如 设置特定的 POSIX 权限位 - 以相同的方式,使用 os.chmod()。
当然,该方法还可以扩展到除 FileHandler 之外的处理程序类型——例如,旋转文件处理程序之一,或完全不同类型的处理程序。
使用 dictConfig() 配置过滤器
你 可以 使用 dictConfig() 配置过滤器,尽管乍一看如何去做可能并不明显(因此这个秘诀)。 由于 Filter 是标准库中唯一包含的过滤器类,并且不太可能满足许多要求(它仅作为基类存在),您通常需要定义自己的 Filter 子类具有覆盖的 filter() 方法。 为此,请在过滤器的配置字典中指定 ()
键,指定将用于创建过滤器的可调用对象(类是最明显的,但您可以提供任何返回 [ X227X]过滤器实例)。 这是一个完整的例子:
此示例展示了如何以关键字参数的形式将配置数据传递给构造实例的可调用对象。 运行时,上面的脚本将打印:
这表明过滤器按配置工作。
需要注意的几点:
- 如果您不能在配置中直接引用可调用对象(例如 如果它位于不同的模块中,并且您无法直接将其导入到配置字典所在的位置),则可以使用
ext://...
形式,如 访问外部对象 中所述。 例如,您可以在上面的示例中使用文本'ext://__main__.MyFilter'
而不是MyFilter
。 - 与过滤器一样,此技术还可用于配置自定义处理程序和格式化程序。 有关日志记录如何支持在其配置中使用用户定义对象的更多信息,请参阅 用户定义对象 ,并参阅上面的其他食谱配方 使用 dictConfig() 自定义处理程序。
自定义异常格式
有时您可能想要进行自定义异常格式设置 - 出于参数的考虑,假设您希望每个记录的事件仅一行,即使存在异常信息也是如此。 您可以使用自定义格式化程序类来执行此操作,如以下示例所示:
运行时,这会生成一个正好有两行的文件:
虽然上述处理很简单,但它指出了如何根据您的喜好格式化异常信息。 traceback 模块可能有助于满足更专业的需求。
说日志消息
在某些情况下,可能需要以可听而不是可见的格式呈现日志消息。 如果您的系统中有可用的文本转语音 (TTS) 功能,即使它没有 Python 绑定,这也很容易做到。 大多数 TTS 系统都有一个可以运行的命令行程序,可以使用 subprocess 从处理程序调用它。 这里假设 TTS 命令行程序不会期望与用户交互或需要很长时间才能完成,并且记录消息的频率不会高到用消息淹没用户,并且可以接受一次说出一条消息而不是并发,下面的示例实现在处理下一条消息之前等待一条消息被说出,这可能会导致其他处理程序保持等待。 这是一个展示该方法的简短示例,它假定 espeak
TTS 包可用:
运行时,这个脚本应该用女声说“你好”,然后说“再见”。
当然,上述方法可以适用于其他 TTS 系统,甚至可以通过从命令行运行的外部程序处理消息的其他系统。
缓冲日志消息并有条件地输出它们
在某些情况下,您可能希望将消息记录在临时区域中,并且仅在发生某种情况时才输出它们。 例如,您可能希望开始记录某个函数中的调试事件,并且如果该函数在没有错误的情况下完成,您不希望收集到的调试信息使日志变得混乱,但是如果出现错误,您希望所有的调试信息要输出的信息以及错误。
这是一个示例,它展示了如何使用装饰器为您希望日志记录以这种方式运行的函数执行此操作。 它利用了 logging.handlers.MemoryHandler,它允许缓冲记录的事件直到某些条件发生,此时缓冲的事件是 flushed
- 传递给另一个处理程序([X212X ] 处理程序)进行处理。 默认情况下,MemoryHandler
在其缓冲区被填满或看到级别大于或等于指定阈值的事件时刷新。 如果您想要自定义刷新行为,您可以将此配方与更专业的 MemoryHandler
子类一起使用。
示例脚本有一个简单的函数 foo
,它只是循环遍历所有日志记录级别,写入 sys.stderr
以说明它将在哪个级别登录,然后在该级别实际记录一条消息等级。 您可以将参数传递给 foo
,如果为真,将在 ERROR 和 CRITICAL 级别记录 - 否则,它仅在 DEBUG、INFO 和 WARNING 级别记录。
该脚本只是安排使用装饰器装饰 foo
,该装饰器将执行所需的条件日志记录。 装饰器将记录器作为参数,并在调用被装饰函数的持续时间内附加内存处理程序。 装饰器还可以使用目标处理程序、应发生刷新的级别以及缓冲区的容量进行额外参数化。 这些默认为 StreamHandler,分别写入 sys.stderr
、logging.ERROR
和 100
。
这是脚本:
运行此脚本时,应观察到以下输出:
如您所见,实际日志输出仅在记录严重性为 ERROR 或更高的事件时才会发生,但在这种情况下,还会记录任何先前具有较低严重性的事件。
你当然可以使用传统的装饰方式:
通过配置使用 UTC (GMT) 格式化时间
有时您想使用 UTC 格式化时间,这可以使用诸如 UTCFormatter 之类的类来完成,如下所示:
然后您可以在代码中使用 UTCFormatter
而不是 Formatter。 如果你想通过配置来做到这一点,你可以使用 dictConfig() API 和以下完整示例所示的方法:
当这个脚本运行时,它应该打印如下内容:
显示如何将时间格式化为本地时间和 UTC,每个处理程序一个。
使用上下文管理器进行选择性日志记录
有时临时更改日志配置并在执行某些操作后将其恢复会很有用。 为此,上下文管理器是保存和恢复日志上下文的最明显方式。 这是此类上下文管理器的一个简单示例,它允许您有选择地更改日志记录级别并纯粹在上下文管理器的范围内添加日志记录处理程序:
如果您指定级别值,则记录器的级别将设置为上下文管理器覆盖的 with 块范围内的该值。 如果您指定了一个处理程序,它会在进入块时添加到记录器中,并在退出块时移除。 您还可以要求经理在块退出时为您关闭处理程序 - 如果您不再需要处理程序,您可以这样做。
为了说明它是如何工作的,我们可以在上面添加以下代码块:
我们最初将记录器的级别设置为 INFO
,因此消息 #1 出现而消息 #2 没有。 然后我们在下面的 with
块中临时将级别更改为 DEBUG
,因此出现消息 #3。 块退出后,记录器的级别恢复为 INFO
,因此不会出现消息 #4。 在下一个 with
块中,我们再次将级别设置为 DEBUG
,但也添加了一个写入 sys.stdout
的处理程序。 因此,消息 #5 在控制台上出现两次(一次通过 stderr
,一次通过 stdout
)。 with
语句完成后,状态与之前一样,因此消息 #6 出现(如消息 #1)而消息 #7 不出现(就像消息 #2)。
如果我们运行生成的脚本,结果如下:
$ python logctx.py
1. This should appear just once on stderr.
3. This should appear once on stderr.
5. This should appear twice - once on stderr and once on stdout.
5. This should appear twice - once on stderr and once on stdout.
6. This should appear just once on stderr.
如果我们再次运行它,但将 stderr
管道传输到 /dev/null
,我们会看到以下内容,这是写入 stdout
的唯一消息:
$ python logctx.py 2>/dev/null
5. This should appear twice - once on stderr and once on stdout.
再一次,将 stdout
传递到 /dev/null
,我们得到:
$ python logctx.py >/dev/null
1. This should appear just once on stderr.
3. This should appear once on stderr.
5. This should appear twice - once on stderr and once on stdout.
6. This should appear just once on stderr.
在这种情况下,打印到 stdout
的消息 #5 没有出现,正如预期的那样。
当然,这里描述的方法可以推广,例如临时附加日志过滤器。 请注意,上述代码适用于 Python 2 和 Python 3。