Jacky Liu's Blog

在多线程 Python 程序中实现多目标不同缩进格式的 logging

---- 带有动态缩进格式的自定义 logging 机制的输出效果:

* 设计目标:

        ---- 使用 Python 自带的 logging 模块可以很方便地让程序输出 logging 信息,而当程序比较复杂,尤其是使用了多线程以后,如果 logging 信息本身的格式也能反映出这些程序结构,分析起来就会比较方便:
       
        ---- 比如:
        我的程序中有个下载模块 Downloader, 在运行时负责为程序的其它部分提供指定内容的下载服务,算是顶级模块。这个模块的直属成员函数所输出的 logging 信息应该使用 0 级缩进格式。
        Downloader 下面有若干个 DownloadServer(服务器)对象,每个服务器对象负责处理特定的一批下载任务,这些 DownloadServer 对象输出的信息应该使用 1 级缩进格式。
        在执行下载任务的时候,每个 DownloadServer 对象下面又有一批下载任务对象,这些具体的任务对象输出的信息应该使用 2 级缩进格式。

        ---- 又比如:
        程序的总体运行过程是一个个任务对象的执行过程,这些对象根据用户实时的输入而建立并执行,在执行完毕后销毁,留下执行结果。这些顶级的 Task 对象在运行时拥有自己的主控线程,并且有自己专属的 log 文件。Task 对象大部分时间在自己的线程里运行,但是当中间需要下载一些数据的时候,它会通知 Downloader 模块建立相应的下载任务对象,并且在下载过程中,切换到属于 Downloader 模块的线程里运行。而下载的过程中所产生的 logging 信息,就需要同时写入到 Downloader 模块的 log 文件和 Task 对象专属的 log 文件里,并且可能要在相同内容的基础上采用不同的缩进格式,因为下载任务对于 Downloader 模块和对于 Task 对象来说,可能具有不一样的逻辑等级。

        ---- 要实现这些功能,就需要通过自定义类型,对标准的 logging 模块的特性做一些扩展


* Python 标准的 logging 机制

        ---- Python 标准的 logging 机制基本上由三种不同等级的对象构成:

            [1] Logger 对象,主要向用户提供 logging 的界面函数: Logger.debug(), Logger.error(), Logger.warning() ... 这些函数的参数就是要记录的字串,用户通过调用这些函数来输出 logging 信息。

            [2] Handler 对象,是 Logger 对象的成员,主要用来指定 logging 的目标(一般是个 log 文件),一个 Handler 指定一个目标。用 Logger.addHandler() 可以向 Logger 对象中添加 Handler。如果一个 logging 信息需要写入多个不同的目标,那么就要向相关的 Logger 对象中添加多个 Handler。

            [3] Formatter 对象,是 Handler 对象的成员,内部包含字符串模板,用来控制写入相关 Handler 指定目标的消息的格式。一个 Handler 只包含一个 Formatter。

        ---- logging 机制的使用可以很灵活。对较小的程序来说,可以整个程序使用一个 Logger 和一个 Handler。对于较复杂的程序来说,可以每个模块拥有自己的 Logger 和 Handler,一个动态建立的任务也可以拥有自己的 Logger 和 Handler。当任务执行到某阶段需要切换到 A 模块的线程里运行时,可以把自身的 Handler 加入 A 模块的 Logger,这样执行过程中产生的信息会同时写入 A 模块的 log 文件和任务自身专属的 log 文件。在 A 模块中执行结束后,可以把 Handler 从 A 模块的 Logger 中移走。下一阶段在 B 模块的线程中运行时,也可以做同样处理。


        ---- 另外需要专门提到的是,所有的 logging 界面函数(debug(), warning(), error() ...)都可以接受一个 extra 参数,类型是 dict。这个参数可以在相同 log 内容的基础上,向不同的 Handler 提供不同的附加信息。比如,现在已经建立了下面这样的 logging 结构:
           
            Logger_A:
             |
            ├─────    Handler_A:
             |                         |
             |                        └─────    Formatter_A: "%(aaa)s %(message)s"
             |   
            └─────    Handler_B:
                                       |
                                      └─────    Formatter_B: "%(bbb)s %(message)s"

        注意,两个 Handler 下面的 Formatter 使用了不同的格式模板,Formatter_A 里面包含域 "aaa",而 Formatter_B 里面包含域 "bbb"。

        如果用户这样调用 Logger_A 的界面函数:

            Logger_A.debug('blah blah blah ...', extra={'aaa':'xxxxxxx', 'bbb':'yyyyyyy'})

        那么,写入 Handler_A 所指定目标的消息会是这样:
           
            'xxxxxxx blah blah blah ...'

        而写入 Handler_B 指定目标的消息会是这样:
           
            'yyyyyyy blah blah blah ...'

        ---- 使用上面所说的这种机制,就可以在相同的 logging 内容基础上使用不同的缩进格式。



* 增强的 logging 机制的设计

        ---- 下面是在 Python 标准的 Logger 和 Handler 对象的基础上所定义的增强的 IndentLogger 和 IndentHandler。
 

# -*- coding: utf-8 -*-

import logging
import logging.handlers


class IndentHandler:

	def __init__(self, file, idtname):	# 如果本类的多个实例要加入一个 IndentLogger 里,那么这些实例的 idtname 不能冲突。

		self._handler= logging.handlers.RotatingFileHandler(filename=file, mode='a', encoding='utf-8')
		
		self._idtname= idtname	# indent name, 作为 format string 内的 field,同时也是 extra 参数里的 key。
		self._idtstr= ""	# indent string,由 '\t' 组成,反映了写入此 Handler 相关目标的消息的缩进等级
		
		self._format= "%(asctime)s %(name)-12s %(levelname)-8s>> %(" + self._idtname + ")s%(message)s"
		#	self._format= "%(asctime)s %(levelname)-8s>> %(" + self._idtname + ")s%(message)s"
		self._formatter= logging.Formatter(self._format)
		
		self._handler.setFormatter(self._formatter)
		
	def set_indent_level(self, ilevel):
		'''
		将本对象的缩进等级重设一下。
		'''
		self._idtstr= '\t' * ilevel

class IndentLogger:
	'''
	接受 IndentHandler 实例作为成员,IndentHandler 实例包含了写入相关目标的消息的缩进信息。
	'''

	def __init__(self, name, level):

		self._logger= logging.getLogger(name)
		self._logger.setLevel(level)

		self._ihandlers= []	# 所有在本实例注册过的 IndentHandler 实例组成的 list

	def addIndentHandler(self, ihandler):

		self._ihandlers.append(ihandler)
		self._logger.addHandler(ihandler._handler)

	def removeIndentHandler(self, ihandler):

		self._ihandlers.remove(ihandler)
		self._logger.removeHandler(ihandler._handler)



	def critical(self, message, *pargs):
		extra= {h._idtname: h._idtstr for h in self._ihandlers}
		for arg in pargs:	# arg[0] 是 IndentHandler 对象,arg[1] 是针对此对象在这个消息中使用的缩进等级
			extra[arg[0]._idtname]= '\t' * arg[1]	# 注意,不改变 self._ihandlers[n]._idtstr 的值

		self._logger.critical(message, extra=extra)

	# 注意,其余的界面函数与 critical() 形式完全一样,只是名字不同。

 

        ---- 这里主要有下面几个考虑:

        [1] IndentHandler._idtstr 只是一个默认的缩进等级,在调用界面函数未指定 *pargs 的情况下会使用,一般是供顶级模块的直属成员用的。而 IndentHandler.set_indent_level() 是供初始化时用的,平时不需要动态设定缩进等级。

        [2] 界面函数的 *pargs 参数形式是这样:
       
            ((ihandler_A, ilevel_A), (ihandler_B, ilevel_B), ...)

        其中 ihandler 是 IndentHandler 对象,ilevel 是 int 类型的缩进等级,顶级模块是 0 级。含义是:针对这个 IndentLogger 下面的 IndentHandler 对象 ihandler_A 使用缩进等级 ilevel_A,针对 ihandler_B 使用缩进等级 ilevel_B ...

        pargs 里需要指定 IndentHandler 对象是因为 IndentLogger 里面可能包含多个 IndentHandler,而设计 pargs 参数本身主要是为了使用起来方便。因为一个顶级模块下面所有不同等级的成员都要使用同一个 IndentLogger,而在执行过程中动态调整缩进等级(通过 IndentHandler.set_indent_level() 函数)不如让这些成员自带缩进等级信息,然后在输出 logging 信息时通过 pargs 参数传递给 IndentLogger。


 




Host by is-Programmer.com | Power by Chito 1.3.3 beta | © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee