python 简单日志框架 自定义logger
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python 简单日志框架 自定义logger相关的知识,希望对你有一定的参考价值。
转载请注明:
仰望高端玩家的小清新 http://www.cnblogs.com/luruiyuan/
通常我们在构建 python 系统时,往往需要一个简单的 logging 框架。python 自带的 logging 框架的确十分完善,但是本身过于复杂,因此需要自行封装来满足我们的高(zhuang)端(b)需求
1. 常用的格式化字符串:
这是我比较常用的格式化字符串,不同的人可能有不同的习惯
1 # 第一种,月日年的输出 2 DEFAULT_DATE_FMT = ‘%a, %p %b %d %Y %H:%M:%S‘ 3 # Wed, Sep 27 2017 18:56:40 4 5 #第二种,年月日 6 DEFAULT_DATE_FMT = ‘%Y-%m-%d %a, %p %H:%M:%S‘ 7 # Wed, 2017-09-27 18:59:33
2. logging 框架的简单基本用法:
1 # 简单的logging配置 2 import logging 3 4 logging.basicConfig(level=logging.DEBUG, 5 format=‘[%(asctime)s %(filename)s [line:%(lineno)d]] %(levelname)s %(message)s‘, 6 datefmt=‘%a, %d %b %Y %H:%M:%S‘, 7 filename=‘myapp.log‘, 8 filemode=‘w‘)
这样的好处是,在一些情况下可以简单配置log之后输出,但是其格式中的样式是难以变化的
3. 封装自己的 logger 框架
毫无疑问,为了方便代码的维护和重构,职责单一原则必不可少。目前的 v0.1 版本的 UML 图如下:
3.1 颜色:
CmdColor 类主要用于存储命令行控制台的字体转义字符串,并且保证颜色名称到颜色转义字符串的映射,其中包括一些常用的颜色
其中代码如下:
本类作为颜色的映射,主要实现了获取所有颜色,以及查重的set,以及名称到字符串的映射
1 class CmdColor(): 2 ‘‘‘ Cmd color escape strings ‘‘‘ 3 # color escape strings 4 __COLOR_RED = ‘\\033[1;31m‘ 5 __COLOR_GREEN = ‘\\033[1;32m‘ 6 __COLOR_YELLOW = ‘\\033[1;33m‘ 7 __COLOR_BLUE = ‘\\033[1;34m‘ 8 __COLOR_PURPLE = ‘\\033[1;35m‘ 9 __COLOR_CYAN = ‘\\033[1;36m‘ 10 __COLOR_GRAY = ‘\\033[1;37m‘ 11 __COLOR_WHITE = ‘\\033[1;38m‘ 12 __COLOR_RESET = ‘\\033[1;0m‘ 13 14 # color names to escape strings 15 __COLOR_2_STR = { 16 ‘red‘ : __COLOR_RED, 17 ‘green‘ : __COLOR_GREEN, 18 ‘yellow‘: __COLOR_YELLOW, 19 ‘blue‘ : __COLOR_BLUE, 20 ‘purple‘: __COLOR_PURPLE, 21 ‘cyan‘ : __COLOR_CYAN, 22 ‘gray‘ : __COLOR_GRAY, 23 ‘white‘ : __COLOR_WHITE, 24 ‘reset‘ : __COLOR_RESET, 25 } 26 27 __COLORS = __COLOR_2_STR.keys() 28 __COLOR_SET = set(__COLORS) 29 30 @classmethod 31 def get_color_by_str(cls, color_str): 32 if not isinstance(color_str, str): 33 raise TypeError("color string must str, but type: ‘%s‘ passed in." % type(color_str)) 34 color = color_str.lower() 35 if color not in cls.__COLOR_SET: 36 raise ValueError("no such color: ‘%s‘" % color) 37 return cls.__COLOR_2_STR[color] 38 39 @classmethod 40 def get_all_colors(cls): 41 ‘‘‘ return a list that contains all the color names ‘‘‘ 42 return cls.__COLORS 43 44 @classmethod 45 def get_color_set(cls): 46 ‘‘‘ return a set contains the name of all the colors‘‘‘ 47 return cls.__COLOR_SET
后续可以做的扩展:颜色可以作为单独的抽象类,各个平台的颜色,如 CmdColor 作为其子类实现具体的颜色方法,这样可以增强健壮性和可扩展性
3.2 logging 的格式:
同样,为了保证 logging 打印的数据格式一致,通过 BasicFormatter 类将 logging 模块的元数据处理为一致的格式,可以保证在彩色和黑白的情况下数据的格式一致性,更重要的是这一抽象也保证了这一格式在日后被其他 handler 复用时的格式一致性。
其中的 format 和 formatTime 方法覆盖了父类 logging.Formatter 中的同名方法,这样通过继承机制很好的模拟了多态,这样我们的公用格式就可以得到复用
3.2.1 修正无法显示毫秒的问题
这里还有一个细节需要注意:
在 logging.Formatter 中的 formatTime 在没有传入时间格式字符串时需要的是会显示毫秒,但是一旦传递了该参数,就无法精确到秒以下的单位。这是由于 logging.Formatter 直接使用了 time.strftime 函数来格式化时间,而该函数参照了 ISO8601 标准,这一标准并未规定比秒更小的时间单位该如何表示,问题由此产生。
但是,注意到在默认不传参情况下 formatTime 会显示毫秒,因此我们只需要知道这里毫秒数是如何产生的即可
logging.Formatter.formatTime 的关键代码如下:
1 ct = self.converter(record.created) 2 if datefmt: 3 s = time.strftime(datefmt, ct) 4 else: 5 t = time.strftime(self.default_time_format, ct) 6 s = self.default_msec_format % (t, record.msecs) 7 return s
我们不难发现,最关键的部分是 record.msecs,因此我们可以知道,我们只需要通过该参数,即可获得秒以下的时间单位。通过测试,我发现这是一个小数,既然如此,剩下的就不用我说了吧~
综上,我们可以得到该类的主要代码:
1 class BasicFormatter(Formatter): 2 3 def __init__(self, fmt=None, datefmt=None): 4 super(BasicFormatter, self).__init__(fmt, datefmt) 5 self.default_level_fmt = ‘[%(levelname)s]‘ 6 7 def formatTime(self, record, datefmt=None): 8 ‘‘‘ @override logging.Formatter.formatTime 9 default case: microseconds is added 10 otherwise: add microseconds mannually‘‘‘ 11 asctime = Formatter.formatTime(self, record, datefmt=datefmt) 12 return asctime if datefmt is None or datefmt == ‘‘ else self.default_msec_format % (asctime, record.msecs) 13 14 def format(self, record): 15 ‘‘‘ @override logging.Formatter.format 16 generate a consistent format‘‘‘ 17 msg = Formatter.format(self, record) 18 pos1 = self._fmt.find(self.default_level_fmt) # return -1 if not find 19 pos2 = pos1 + len(self.default_level_fmt) 20 if pos1 > -1: 21 last_ch = self.default_level_fmt[-1] 22 repeat = self._get_repeat_times(msg, last_ch, 0, pos2) 23 pos1 = self._get_index(msg, last_ch, repeat) 24 return ‘%-10s%s‘ % (msg[:pos1], msg[pos1+1:]) 25 else: 26 return msg
3.3 具体的 CmdColoredFormatter 格式类:
这个类已经不再是抽象了,而是在 BasicFormatter 的基础上对 logging 中的信息进一步美化——上色的过程
这个类只负责上色,不涉及 logging 中的时间处理,因此我们只需覆盖 format 方法即可,颜色的处理已经主要聚合在 CmdColor 类中,因此本类较为简单
本类的代码如下:
1 class CmdColoredFormatter(BasicFormatter): 2 ‘‘‘ Cmd Colored Formatter Class‘‘‘ 3 4 # levels list and set 5 __LEVELS = [‘NOTSET‘, ‘DEBUG‘, ‘INFO‘, ‘WARNING‘, ‘ERROR‘, ‘CRITICAL‘] 6 __LEVEL_SET = set(__LEVELS) 7 8 def __init__(self, fmt=None, datefmt=None, **level_colors): 9 super(CmdColoredFormatter, self).__init__(fmt, datefmt) 10 self.LOG_COLORS = {} # a dict, used to convert log level to color 11 self.init_log_colors() 12 self.set_level_colors(**level_colors) 13 14 def init_log_colors(self): 15 ‘‘‘ initialize log config ‘‘‘ 16 for lev in CmdColoredFormatter.__LEVELS: 17 self.LOG_COLORS[lev] = ‘%s‘ 18 19 def set_level_colors(self, **kwargs): 20 ‘‘‘ set each level different colors ‘‘‘ 21 lev_set = CmdColoredFormatter.__LEVEL_SET 22 color_set = CmdColor.get_color_set() 23 24 # check log level and set colors 25 for lev, color in kwargs.items(): 26 lev, color = lev.upper(), color.lower() 27 if lev not in lev_set: 28 raise KeyError("log level ‘%s‘ does not exist" % lev) 29 if color not in color_set: 30 raise ValueError("log color ‘%s‘ does not exist" % color) 31 self.LOG_COLORS[lev] = ‘‘.join([CmdColor.get_color_by_str(color), ‘%s‘, CmdColor.get_color_by_str(‘reset‘)]) 32 33 def format(self, record): 34 ‘‘‘ @override BasicFormatter.format‘‘‘ 35 msg = super(CmdColoredFormatter, self).format(record) 36 # msg = BasicFormatter.format(self, record) # 本行和上一行等价 37 return self.LOG_COLORS.get(record.levelname, ‘%s‘) % msg
3.4 Logger 类:
通过前面各个类的准备工作,Logger 类就可以初具雏形了。
1. 几个参数的相关解释:
1. 参数列表: __LOG_ARGS
2. 初始化:除了固定的几个参数,其余参数的初始化通过 kwargs 传入的 dict 在 set_logger 方法中动态初始化
这里有一些小 trick 可以简化我们的代码,并且具有良好的可扩展新
1 # 在某个函数定义内调用,可获得函数的所有参数,以 dict 为形式 2 # 每次调用时返回一个新的 dict,注意,参数 self 或者 cls 也会包含在内 3 # 需要用 pop() 方法去除 4 arg_dict = locals() 5 6 # 获取对象中某个属性或方法,不存在时返回 default 中的内容 7 getattr(obj, name, default=None) 8 # 动态设置对象中的属性值或者函数指针 9 setattr(obj, name, value)
3. 添加 handler:
目前还没有用到更复杂的 http 和 socket 的 handler , 因此这里暂时没有封装相应的方法,后续可以封装成一个简单工厂,等用到再说。
目前只用到了 fileHandler 和 streamHandler ,因此只能输出到控制台以及文件。
1 def __add_filehandler(self): 2 ‘‘‘ Add a file handler to logger ‘‘‘ 3 # Filehandler 4 if self.backup_count == 0: 5 self.filehandler = logging.FileHandler(self.filename, self.filemode) 6 # RotatingFileHandler 7 elif self.when is None: 8 self.filehandler = logging.handlers.RotatingFileHandler(self.filename, 9 self.filemode, self.limit, self.backup_count) 10 # TimedRotatingFileHandler 11 else: 12 self.filehandler = logging.handlers.TimedRotatingFileHandler(self.filename, 13 self.when, 1, self.backup_count) 14 15 formatter = BasicFormatter(self.filefmt, self.filedatefmt) 16 self.filehandler.setFormatter(formatter) 17 self.logger.addHandler(self.filehandler) 18 19 def __add_streamhandler(self): 20 ‘‘‘ Add a stream handler to logger ‘‘‘ 21 self.streamhandler = logging.StreamHandler() 22 self.streamhandler.setLevel(self.cmdlevel) 23 formatter = CmdColoredFormatter(self.cmdfmt, self.cmddatefmt, 24 **self.cmd_color_dict) if self.colorful else BasicFormatter(self.cmdfmt, self.cmddatefmt) 25 self.streamhandler.setFormatter(formatter) 26 self.logger.addHandler(self.streamhandler)
4. 基于 loggername 的单例模式:
使用过 logging 的都知道,相同的 loggername 获取的 logging 模块的实例是相同的,因此自行封装的 logger 框架也应该遵循类似的模式,即基于 loggername 的类单例模式。
这里只需要注意 3 点:1. 线程并发安全性——加锁 2. loggername 到相应 instance 的映射 3. Logger 类本身允许多例,但是同一个 loggername 只允许单例
但是要注意,__init__ 本身只能返回 None ,因而拿不到对象引用,每个类在创建实例的时候,实际上是由类调用了 __new__ 方法返回对象引用,这个引用再作为 self 参数传入 __init__ 中初始化该对象,因此实现中的 __new__ 是一个容易忽略的细节。
相应实现如下:
1 @classmethod 2 def get_logger(cls, **kwargs): 3 loggername = kwargs[‘loggername‘] 4 cls.__lock.acquire() # lock current thread 5 if loggername in cls.__name2logger: 6 cls.__name2logger[loggername].set_logger(**kwargs) 7 else: 8 log_obj = object.__new__(cls) 9 cls.__init__(log_obj, **kwargs) 10 cls.__name2logger[loggername] = log_obj 11 cls.__lock.release() # release lock 12 return cls.__name2logger[loggername]
5. set_logger: 通过一个方法设置所有的相关参数
这里体现出了 setattr 的用处,通过这样的方法能够动态的添加 / 修改相关的对象属性
通过对象的属性重新加载
其实现如下:
1 def set_logger(self, **kwargs): 2 ‘‘‘ Configure logger with dict settings ‘‘‘ 3 for k, v in kwargs.items(): 4 if k not in Logger.__log_arg_set: 5 raise KeyError("config argument ‘%s‘ does not exist" % k) 6 setattr(self, k, v) # add instance attributes 7 8 if self.cmd_color_dict is None: 9 self.cmd_color_dict = {‘debug‘: ‘green‘, ‘warning‘:‘yellow‘, ‘error‘:‘red‘, ‘critical‘:‘purple‘} 10 if isinstance(self.cmdlevel, str): 11 self.cmdlevel = getattr(logging, self.cmdlevel.upper(), logging.DEBUG) 12 if isinstance(self.filelevel, str): 13 self.filelevel = getattr(logging, self.filelevel.upper(), logging.INFO) 14 15 self.__init_logger() 16 self.__import_log_func() 17 if self.cmdlog: 18 self.__add_streamhandler() 19 if self.filelog: 20 self.__add_filehandler()
6. 其他:
在实现基于 loggername 的单例模式时,有一些基于反射的想法,虽然失败了,但是也是对反射方式的一种尝试
以下这个装饰器就是我第一次时试图加在 __init__ 上的装饰器,但是由于 __init__ 强制返回 None 而无法拿到对象引用而失败,但是实际上如果用在 __new__ 上即可。
这里展示了从函数外通过反射获取传入函数参数的方法:
与 locals() 对应,inspect.signature(func_name).parameters 可以从函数外通过反射的方式获取到传入函数的参数和值,返回值为:
OrdereDict,例如一个函数 func(a,b),调用为 func(1, 2)
则返回一个 OrdereDict {‘a‘: ‘a=1‘, b: ‘b=2‘}
相应的实现如下:
1 import inspect 2 3 # 基于 loggername 的单例装饰器 4 def singletonLoggerByName(cls): 5 __name2logger = {} 6 def getValueByArg(orderedDict, arg): 7 return str(orderedDict[arg]).partition(‘=‘)[-1] 8 9 def wrapper(self, logger_init, **kwargs): 10 default_values = inspect.signature(logger_init).parameters 11 name = kwargs.get(‘loggername‘, getValueByArg(default_values, ‘loggername‘)) 12 print(‘name not in __name2logger: %r‘ % (name not in __name2logger)) 13 if name not in __name2logger: 14 logger_init(self, **kwargs) 15 __name2logger[name] = self 16 print(__name2logger[name]) 17 return __name2logger[name] # 装饰器用于 __init__ 是不行的,因为 python 中 __init__ 只能返回 None, 这样单例模式中后续的引用无法绑定到第一次的实例上 18 return wrapper
7.效果图:
完整代码详见:log/logger.py
参考资料:大佬的博客
今天就到这里啦~lalala
以上是关于python 简单日志框架 自定义logger的主要内容,如果未能解决你的问题,请参考以下文章
Django 中使用 logging 配置 logger 自定义日志输出
Java日志框架 -- LOG4J(Log4j入门案例日志级别Log4j组件(LoggersAppendersLayouts)配置文件内置日志记录自定义Logger)
Java日志框架 -- LOG4J(Log4j入门案例日志级别Log4j组件(LoggersAppendersLayouts)配置文件内置日志记录自定义Logger)