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類

 

后續可以做的擴展:顏色可以作為單獨的抽象類,各個平台的顏色,如 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM