译注:这篇文章译自 Python HOWTOs 中的一篇文章 Loggin Cookbook,作者是 Vinay Sajip。除了 Inserting a BOM into messages sent to a SysLogHandler 和 Speaking logging messages 两小部分个人觉得没有多大意义,其他部分都翻译了。在部分地方有进行了意译和删节。说实话,文章的内容不错,但文笔实在不咋地,不少地方理解起来都很费神。
本文包含了一些 logging 相关的使用方法(recipe)。这些方法在过去被发现很有用。
多模块中使用 logging
多次调用 logging.getLogger(‘someLogger’) 返回对同一个 logger 对象的引用。这种情况不仅出现在单一模块中,而且也会跨模块出现,只要该 logger 对象存在于同一个 Python 解析进程内。对同一模块的引用会出现这种情况;另外,应用程序可以在一个模块中定义和配置一个父 logger,在另一个模块中定义(但不配置)一个子 logger,这样,对所有子 logger 的调用都会向上传递到父 logger。下边是 main 模块:
1 | import logging |
下边是 auxiliary 模块:
1 | import logging |
输出如下所示:
1 | 2005-03-23 23:47:11,663 - spam_application - INFO - |
多线程日志
记录多线程日志很简单就能做到。下边的例子展示了从 main (initial) 和另一个线程中记录日志:
1 | import logging |
当运行的时候,这个程序打印出类似如下的内容:
1 | 0 Thread-1 Hi from myfunc |
就如我们期望的,日志交错输出(译注:线程执行顺序不定)。当然,对更多的线程,这种记录日志的方法也是可行的。
多 handler 和多 formatter
Logger 是普通 Python 对象。(它的)方法 addHandler 对能够添加的 handler 数量没有上下限限制。有时,将所有级别日志记录到文本文件而同时将错误以上级别的日志输出到控制台,对应用程序来说很有效。要达到这个目的,只要简单配置合适的 handler 即可。应用程序中的日志调用代码将可以保持不变。下边例子来自对前文中基于模块配置的日志例子的细微修改:
1 | import logging |
需要注意的是,应用程序并不关心多 handler 的情况(译注:也即单 hander 和多 handler 对应用程序来说是一样的),修改的只是新的 handler fh 的添加和配置。
这种能够创建高或低日志级别的 handler 的能力,对于编写和测试应用程序来说是非常有帮助的。使用 logger.debug 语句用于调试而不是用 print 语句。使用 print 语句,你后续需要删除这些语句或者注释掉它们;而使用 logger.debug 语句,这些语句可以原封不动一直存在于源代码中,且它们会一直保持休眠状态直到你需要用到它们,在那时,你只需要修改 logger 和 handler 的日志级别。
输出日志到多个地方
假定你想要在不同环境中,记录到控制台和文件的日志有不同的消息格式;日志级别大于等于 DEBUG 的记录到文件,大于等于 INFO 级别的输出到控制台。我们同时也假定,记录到文件的日志需要包含时间戳,而输出到控制台的不需要。下边代码展示了我们如何做到这些:
1 | import logging |
当你运行这个代码,在控制台你可以看到:
1 | root : INFO Jackdaws love my big sphinx of quartz. |
在文件中,你可以看到:
1 | 10-22 22:19 root INFO Jackdaws love my big sphinx of quartz. |
就如你所看到的,只有 DEBUG 级别的日志出现在文件中,其余的日志则被同时记录到文件和控制台。
这个例子使用了控制台和文件 handler,但是你可以使用任意数量和任何组合的 handler。
服务器配置样例
下例展示了一个服务器配置样例:
1 | import logging |
下边例子接受一个文件并把该文件发送到服务器:
1 | #!/usr/bin/env python |
跨网络发送和接收日志事件
假定你想跨网络发送日志事件,然后在接收端进行处理。一个简单的做法就是在发送端将一个 SocketHandler 实例添加到 root logger:
1 | import logging, logging.handlers |
在接收端,你可以使用 SocketServer 模块来搭建一个接收器。下边是一个简单的可工作的例子:
1 | import pickle |
首先运行服务端,再运行客户端。在客户端,控制台不会有任何输出;在服务端,你应该能够看到如下输出:
1 | About to start TCP server... |
需要注意的是,在某些场景,使用 pickle 存在一些安全问题。如果这些问题影响到你,你可以使用通过覆写 makePickle() 方法来使用替补序列化方案,同时调整以上代码以适应你的替补方案。
添加上下文信息到你的日志输出中
有时,你想要日志输出中除了包含传递给日志调用的参数,还要包含上下文信息。例如,在一个网络应用程序中,记录客户端具体信息的日志是很有必要的(如客户端的用户名或 IP 地址)。尽管你可以使用额外的参数达到这个目的,但用这种方法来传递信息总不是方便的。虽然通过在每个连接创建 Logger 实例的方法很诱人,但这种方法并不是一个好主意,因为这些实例不会被垃圾管理器回收。在实际操作中,这并不会是个问题,但当 Logger 实例的数量依赖于你想要使用的日志的粒度水平,且它们的数量不受控制时,对它们的管理将很困难。
使用 LoggerAdapter 传递上下文信息
一个跟随日志事件传递上下文信息的简单方法就是使用 LoggerAdapter 类。这个类被设计的看起来像 Logger,所以你可以调用 debug()、info()、warning()、error()、exception()、critical() 和 log() 方法。这些方法跟 Logger 类中相对应的方法有相同功能(signature),所以我们可以交换地使用这两个类的实例。
当你创建了一个 LoggerAdapter 的实例,你将一个 Logger 实例和一个包含上下文信息的字典型对象传递给它。当你调用 LoggerAdapter 实例中的一个方法时,它将这个调用委托给初始化于 LoggerAdapter 构造函数的 Logger 实例,并组织将上下文信息传递给这个委托调用。下边是 LoggerAdapter 的代码片段:
1 | def debug(self, msg, *args, **kwargs): |
LoggerAdapter 中的 process() 方法将上下文信息添加到日志输出中。它接收日志调用传递过来的消息参数和关键词参数,并且返回修改过后的对应参数,以被 Logger 实例使用。这个方法的默认实现中,不处理消息参数,但在关键词参数中插入一个名为 “extra” 的 key。当然了,如果这个关键词参数已经包含了 “extra” key,那它将会被覆写。
使用 “extra” 的优势在于字典型对象会被合并到 LogRecord 实例的 __dict__参数,这样允许知道字典型对象的 key 的 Formatter 实例使用自定义的字符串。如果你需要一个不一样的方法,比如,你想要将上下文信息依附到消息的前边或者后边,你只需要创建一个 LoggerAdapter 的子类并重写 process() 函数,以满足你所需。下边就是一个简单的例子:
1 | class CustomAdapter(logging.LoggerAdapter): |
你可以这样使用这个类:
1 | logger = logging.getLogger(__name__) |
这样,adapter 记录的日志都会将 some_conn_id 依附到消息的前边。
使用对象而不是字典来传递上下文信息
你不需要传递一个真正的字典给 LoggerAdapter ——你可以传递一个实现了 __getitem__和 __iter__ 的类的实例(看起来可以像一个字典一样记录日志)给它。这种方法将会很有用,如果你要动态地生成数值(而字典的值是固定的)。
使用 Filter 来传递上下文信息
你也可以通过一个自定义的 Filter 来添加上下文信息到日志输出。Filter 实例可以修改传递给它们的 LogRecords,包括添加额外的可以使用适当格式输出的属性或自定义的 Formatter。
例如,在一个 Web 应用中,处理过后的请求可以存储在 threadlocal (threading.local) 变量,然后通过 Filter 访问,来添加请求的信息,即 IP 地址、远程用户名到 LogRecord,使用上边 LoggerAdapter 例子的属性名 “ip”、“user”。在那种情况下,同一格式化字符串可以用于获得跟上例类似的输出。下边是样例代码:
1 | import logging |
运行,可以得到如下结果:
1 | 2010-09-06 22:38:15,292 a.b.c DEBUG IP: 123.231.231.123 User: fred A debug message |
多进程输出日志到单一文件
尽管 logging 模块是线程安全的,而且单一进程的多线程输出日志到单一文件也是支持的,但多进程输出日志到单一文件却是不支持的,因为在 Python 中,多进程序列化访问单一文件没有标准的方法。如果你需要多进程输出日志到单一文件,其中一个方法就是让所有的进程将日志输出到 SocketHandler,然后用一个单独的服务器进程从 socket 读取数据并将日志写到文件中(如果你喜欢,你也可以从已存在的进程中派发一个线程来做这件事)。这一节详细描述了这种方法和包含了一个可以工作的 socket 接收器,这个接收器可以作为你开发自己应用程序的开始。
如果你正在使用一个较新的包括 multiprocessing 模块的 Python 版本,你可以写一个使用这个模块中 Lock 类的 handler,以序列化多进程对单一文件的访问。已知的 FileHandler 和子类目前都没有利用 multiprocessing 这个模块,尽管未来它们可能会实现。需要注意的是,multiprocessing 模块并不是在所有平台都实现了可以工作的锁(见 https://bugs.python.org/issue3770 )。
使用文件周转(File Rotation)
有时你想要日志文件只增长到指定的大小,然后就新建新的日志文件并将日志写到该文件。你可能想只保留指定数量的日志文件,并且当这些文件都已经创建,轮转写入日志到这些文件,以保证文件的数量和文件大小在限定范围之内。为了这个,logging 模块提供了 RotatingFileHandler:
1 | import glob |
执行这个代码段会生成 6 个独立的文件,每一个文件都包含了部分日志:
1 | logging_rotatingfile_example.out |
最新的当前文件一直是 logging_rotatingfile_example.out ,并且每当该文件达到大小限制时,它就会被重命名添加后缀 .1。其他的日志文件也会被重命名递增后缀(如 .1 变成 .2),后缀为 .6 的文件则会被删除。
一个基于字典配置的案例
下边是一个日志字典配置的例子——从 Django 项目文档获取得到。这个字典会被传递给 dictConfig() 来使得配置生效:
1 | LOGGING = { |
关于这个配置更多信息,你可以参考 Django 文档的相关章节。
实现结构化日志
大多数的日志消息都设计为方便人类阅读,因此并不方便及其处理,但还是存在一些场景,你想让日志消息结构化输出,以便程序能够处理(不用复杂的正则表达式来过滤日志消息)。使用 logging 模块很容易就可以做到这点。要做到这点有很多种方法,但以下是一个简单的使用 JSON 来序列化事件以便于机器解析的方法:
1 | import json |
运行以上程序,输出:
1 | message 1 >>> {"fnum": 123.456, "num": 123, "bar": "baz", "foo": "bar"} |
需要注意的是,元素输出顺序在不同 Python 版本中可能不一样。
如果你需要更加专业的处理,你可以使用自定义的 JSON 编码器。下边就是一个完整的例子:
1 | from __future__ import unicode_literals |
运行以上代码,输出:
1 | message 1 >>> {"snowman": "\u2603", "set_value": [1, 2, 3]} |
需要注意的是,元素输出顺序在不同 Python 版本中可能不一样。
用 dictConfig() 自定义 handler
有些时候,你可能想要以特定方式自定义 handler。如果你使用 dictConfig(),你可能就可以做到这一点而不用新建子类。作为一个例子,假定你想要设定日志文件的所有者。在 POSIX 兼容的机器,用 os.chown() 就可以容易做到,但标准库 stdlib 中的文件 handler 没有提供內建支持。你可以采用(类似)如下简单函数来自定义创建 handler:
1 | def owned_file_handler(filename, mode='a', encoding=None, owner=None): |
接下来你可以在日志配置文件中指定将会创建的 handler:
1 | LOGGING = { |
在这个例子中,为了展示,我设定了文件的所有者和所有组为 pulse 用户 pulse 组。把他们组合在一起为一个可运行的脚本 chowntest.py:
1 | import logging, logging.config, os, shutil |
要运行这个程序,你可能需要 root 权限:
1 | sudo python3.3 chowntest.py |
需要注意的是,这个例子使用的 Python 版本是 3.3,这也是 shutil.chown() 会出现的原因。这种方法在支持 dictConfig() 的 Python 版本中应该都可以工作——也即,Python 2.7、3.2 以及后续版本。3.3 之前的 Python 版本,你需要实现实际的所有者变更,如 os.chown()。
实际上,这个 handler 创建函数可能在你项目中的某个公共模块中。在这种情况下,不使用如下配置:
1 | '()': owned_file_handler, |
而是可以使用如:
1 | '()': 'ext://project.util.owned_file_handler', |
其中,project.util 可以替换为函数所在的包名。在上边可以运行的代码中,使用 ‘ext://main.owned_file_handler’ 应该可以工作。这里,实际的可调用函数是 dictConfig() 从 ext:// 格式中解析得来。
这个例子同样也之处了你如何使用相同方式实现文件的其他类型的变化——如,设置 POSIX 权限位——使用 os.chmod()。
当然,这种方式也可以拓展到其他类型的 handler 而不是 FileHandler——例如,周转文件 handler、或者其他类型 handler。
用 dictConfig() 自定义 filter
你可以用 dictConfig() 来配置 filter,尽管咋看之下不知道怎么做(所以才有这个教程)。因为 Filter 是标准库中仅有的 filter 类,所以它不大可能迎合太多需求(作为一个基类)。通常你需要定义自己的 Filter 子类并覆写 filter() 函数。要做到这一点,在配置字典中为 filter 定义一个 “()” key,指定用于创建 filter 的回调函数(用一个类来实现是最明显的,但你可以提供任意可返回 Filter 实例的回调函数)。下边是一个完整的例子:
1 | import logging |
这个例子展示了你如何通过传递关键字参数形式的配置文件到回调函数,来创建一个 Filter 实例。运行这个例子,输出如下:
1 | changed: hello |
证明了 filter 如预期般运行。
另外还有一些需要注意的地方:
如果你不能够在配置中直接引用回调函数(例如,回调函数在不同的模块,所以你不能够直接把它导入到配置中),你可以使用在访问外部对象中描述的 ext://… 形式。例如,你可以在上述例子中使用 ‘ext://main.MyFilter’ 而不是 MyFilter。
这种方法除了可用于 filter,也可以用于自定义 handler 和 formatter。请查看用户自定义对象获取 logging 如何支持在配置中使用自定义对象的更多信息,和该文中的用 dictConfig() 自定义 handler 部分。
自定义异常输出格式
有时你可以想自定义异常输出格式——假定你想每一个日志事件输出一行。你可以自定义 formatter 类,就像下例所做的:
1 | import logging |
运行代码,会生成一个只有两行的文件:
1 | 28/01/2015 07:21:23|INFO|Sample message| |
尽管上例很简单,但它指出了如何根据你的需要来格式化异常输出。如果要求更加专业化,traceback 模块将有很大帮助。
缓存日志消息并有条件输出
在有些情况下,你可以想要把日志输出到一个临时地方,然后只在特定条件下输出它们。例如,你可能想要记录一个函数的调试日志,但如果该函数成功执行没有错误,你不想要把调试日志输出到日志文件中;如果函数执行发生错误,你想要所有的调试日志和错误一起输出。
这里有一个例子展示如何使用装饰器做到这一点。这个例子使用了 logging.handlers.MemoryHandler ——这个 handler 允许缓存日志,直到达到特定条件才输出日志(flush)——来将日志消息传递给另一个 handler (目的 handler)处理。默认情况下,当缓冲区满或者当日志事件的级别大于等于设定的临界值,MemoryHandler 会将日志输出。你可以使用这个例子构造一个更加专业化的 MemoryHandler 子类。
这个例子中有一个简单的函数 foo,轮询所有的日志级别,并输出它正在记录的日志级别到 sys.stderr,然后把那个级别的日志信息记录下来。你可以向它传递一个参数,当该参数为真,它会记录 ERROR 和 CRITICAL 级别的日志;否则,它只记录 DEBUG、INFO 和 WARNING 级别的日志。
这个例子用可以执行条件化日志记录的装饰器来装饰函数 foo。这个装饰器接受一个 logger 作为参数,并将一个 memory handler 依附到被装饰函数上。这个装饰器可以接受额外的参数目标 handler、日志输出是的日志级别,以及缓存的容量。这几个参数的默认值是写到 sys.stderr 的 StreamHandler、logging.ERROR 和 100。
下边是代码:
1 | import logging |
执行代码,输出如下:
1 | Calling undecorated foo with False |
就如你所看到的,日志只在当级别大于等于 ERROR 时才会输出。但是,低于该级别的日志也会被记录下来。
你当然也可以使用传统的装饰器语法:
1 |
|
通过配置使用 UTC (GMT) 格式化时间
有时你可能想要用 UTC 来格式化时间,这可以通过使用类 UTCFormatter 来做到:
1 | import logging |
然后在你的代码中用 UTCFormatter 代替 Formatter。如果你想要通过配置来达到这点,你可以像下例一样使用 dictConfig() 函数:
1 | import logging |
运行脚本,输出如下:
1 | 2015-10-17 12:53:29,501 The local time is Sat Oct 17 13:53:29 2015 |
使用上下文管理器有选择性地记录日志
有些时候,临时地改变日志配置但在最后恢复原来的配置是很有用的。对于这点,采用上下文管理器是最显而易见的保存和恢复上下文方法。下边是一个简单的上下文管理器例子,它允许你改变日志级别和添加 handler:
1 | import logging |
如果你指定了日志级别,那 logger 的日志级别就会在 with 代码块中被指定;如果指定了 handler,那该 handler 就会分别在 with 代码块的入口处(entry)和出口处(exit)被添加和移除。你也可以让这个管理器在 with 代码块的出口处关闭 handler——你可以做的更多如果你不需要这个 handler。
为了展示这个管理器怎么工作,我们把以下代码添加到以上代码中:
1 | if __name__ == '__main__': |
刚开始我们设置 logger 的日志级别为 INFO,所以消息 #1 输出而 #2 不输出。然后我们在 with 代码块临时改变日志级别为 DEBUG,所以消息 #3 输出;with 代码块退出时,恢复日志级别为 INFO,所以消息 #4 不输出。接下来,在 with 代码块设置日志级别为 DEBUG,但同时添加一个输出到 sys.stdout 的 handler,所以,消息 #5 输出两次(一次通过 stderr,一次通过 stdout); with 代码块退出后,设置恢复到初始的状态,所以消息 #6 输出(像消息 #1),但消息 #7 不输出(就像消息 #2)。
运行代码,输出如下:
1 | python logctx.py |
如果我们把 stderr 指向 /dev/null,执行代码,我们将得到如下结果:
1 | python logctx.py 2>/dev/null |
同样,我们将 stdout 指向 /dev/null,可以得到:
1 | python logctx.py >/dev/null |