zoukankan      html  css  js  c++  java
  • 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    = '33[1;31m'
     5     __COLOR_GREEN  = '33[1;32m'
     6     __COLOR_YELLOW = '33[1;33m'
     7     __COLOR_BLUE   = '33[1;34m'
     8     __COLOR_PURPLE = '33[1;35m'
     9     __COLOR_CYAN   = '33[1;36m'
    10     __COLOR_GRAY   = '33[1;37m'
    11     __COLOR_WHITE  = '33[1;38m'
    12     __COLOR_RESET  = '33[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类

    后续可以做的扩展:颜色可以作为单独的抽象类,各个平台的颜色,如 CmdColor 作为其子类实现具体的颜色方法,这样可以增强健壮性和可扩展性

    由于 win 平台和 *nix 平台对于输出处理不同,因此在目前的版本中,如果在win平台调用,则直接禁用了颜色的输出。

    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
    BasicFormatter 主要部分

    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
    CmdColoredFormatter 的实现

    3.4 Logger 类:

    通过前面各个类的准备工作,Logger 类就可以初具雏形了。

    1. 几个参数的相关解释:

    1. 参数列表: __LOG_ARGS

    __LOG_ARGS 作为参数列表,主要用途进行参数检查,同时便于 debug 时了解本类的相关参数。这是因为代码中使用了 setattr 进行动态属性配置,因此代码中没有明确的属性初始化过程。
    2. 参数 set: __log_arg_set 参数查重,主要是相比于 list 提高效率
    3. __lock :线程锁,用于基于 loggername 的单例模式
    4. __name2logger :通过 loggername 映射到相应实例

    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)
    handler 相关实现

    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]
    get_logger 的实现

    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()
    set_logger 的实现

    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

  • 相关阅读:
    酱茄WordPress社区论坛圈子小程序为解决用户活跃变现而生
    太顶了!爆肝3.5W字长文Java 集合!(建议收藏)
    美团二面:内存耗尽后Redis会发生什么?
    UE4_C++自定义log
    python3进制转换
    UE4蓝图Blueprint->组件->TreeView/ListView
    C++,win编程
    2020-11-11
    b站视频详情数据抓取,自动打包并发送到指定邮箱(单个或者群发)
    BiLiBiLi爬虫
  • 原文地址:https://www.cnblogs.com/luruiyuan/p/7600931.html
Copyright © 2011-2022 走看看