转载请注明:

仰望高端玩家的小清新 http://www.cnblogs.com/luruiyuan/

通常我们在构建 python 系统时,往往需要一个简单的 logging 框架。python 自带的 logging 框架的确十分完善,但是本身过于复杂,因此需要自行封装来满足我们的高(zhuang)端(b)需求

1. 常用的格式化字符串:

这是我比较常用的格式化字符串,不同的人可能有不同的习惯

 # 第一种,月日年的输出
DEFAULT_DATE_FMT = '%a, %p %b %d %Y %H:%M:%S'
# Wed, Sep 27 2017 18:56:40 #第二种,年月日
DEFAULT_DATE_FMT = '%Y-%m-%d %a, %p %H:%M:%S'
# Wed, 2017-09-27 18:59:33

2. logging 框架的简单基本用法:

 # 简单的logging配置
import logging logging.basicConfig(level=logging.DEBUG,
format='[%(asctime)s %(filename)s [line:%(lineno)d]] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename='myapp.log',
filemode='w')

这样的好处是,在一些情况下可以简单配置log之后输出,但是其格式中的样式是难以变化的

3. 封装自己的 logger 框架

毫无疑问,为了方便代码的维护和重构,职责单一原则必不可少。目前的 v0.1 版本的 UML 图如下:

3.1 颜色:

CmdColor 类主要用于存储命令行控制台的字体转义字符串,并且保证颜色名称到颜色转义字符串的映射,其中包括一些常用的颜色

其中代码如下:

本类作为颜色的映射,主要实现了获取所有颜色,以及查重的set,以及名称到字符串的映射

 class CmdColor():
''' Cmd color escape strings '''
# color escape strings
__COLOR_RED = '\033[1;31m'
__COLOR_GREEN = '\033[1;32m'
__COLOR_YELLOW = '\033[1;33m'
__COLOR_BLUE = '\033[1;34m'
__COLOR_PURPLE = '\033[1;35m'
__COLOR_CYAN = '\033[1;36m'
__COLOR_GRAY = '\033[1;37m'
__COLOR_WHITE = '\033[1;38m'
__COLOR_RESET = '\033[1;0m' # color names to escape strings
__COLOR_2_STR = {
'red' : __COLOR_RED,
'green' : __COLOR_GREEN,
'yellow': __COLOR_YELLOW,
'blue' : __COLOR_BLUE,
'purple': __COLOR_PURPLE,
'cyan' : __COLOR_CYAN,
'gray' : __COLOR_GRAY,
'white' : __COLOR_WHITE,
'reset' : __COLOR_RESET,
} __COLORS = __COLOR_2_STR.keys()
__COLOR_SET = set(__COLORS) @classmethod
def get_color_by_str(cls, color_str):
if not isinstance(color_str, str):
raise TypeError("color string must str, but type: '%s' passed in." % type(color_str))
color = color_str.lower()
if color not in cls.__COLOR_SET:
raise ValueError("no such color: '%s'" % color)
return cls.__COLOR_2_STR[color] @classmethod
def get_all_colors(cls):
''' return a list that contains all the color names '''
return cls.__COLORS @classmethod
def get_color_set(cls):
''' return a set contains the name of all the colors'''
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 的关键代码如下:

         ct = self.converter(record.created)
if datefmt:
s = time.strftime(datefmt, ct)
else:
t = time.strftime(self.default_time_format, ct)
s = self.default_msec_format % (t, record.msecs)
return s

我们不难发现,最关键的部分是 record.msecs,因此我们可以知道,我们只需要通过该参数,即可获得秒以下的时间单位。通过测试,我发现这是一个小数,既然如此,剩下的就不用我说了吧~

综上,我们可以得到该类的主要代码:

 class BasicFormatter(Formatter):

     def __init__(self, fmt=None, datefmt=None):
super(BasicFormatter, self).__init__(fmt, datefmt)
self.default_level_fmt = '[%(levelname)s]' def formatTime(self, record, datefmt=None):
''' @override logging.Formatter.formatTime
default case: microseconds is added
otherwise: add microseconds mannually'''
asctime = Formatter.formatTime(self, record, datefmt=datefmt)
return asctime if datefmt is None or datefmt == '' else self.default_msec_format % (asctime, record.msecs) def format(self, record):
''' @override logging.Formatter.format
generate a consistent format'''
msg = Formatter.format(self, record)
pos1 = self._fmt.find(self.default_level_fmt) # return -1 if not find
pos2 = pos1 + len(self.default_level_fmt)
if pos1 > -1:
last_ch = self.default_level_fmt[-1]
repeat = self._get_repeat_times(msg, last_ch, 0, pos2)
pos1 = self._get_index(msg, last_ch, repeat)
return '%-10s%s' % (msg[:pos1], msg[pos1+1:])
else:
return msg

BasicFormatter 主要部分

3.3 具体的 CmdColoredFormatter  格式类:

这个类已经不再是抽象了,而是在 BasicFormatter 的基础上对 logging 中的信息进一步美化——上色的过程

这个类只负责上色,不涉及 logging 中的时间处理,因此我们只需覆盖 format 方法即可,颜色的处理已经主要聚合在  CmdColor 类中,因此本类较为简单

本类的代码如下:

 class CmdColoredFormatter(BasicFormatter):
''' Cmd Colored Formatter Class''' # levels list and set
__LEVELS = ['NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
__LEVEL_SET = set(__LEVELS) def __init__(self, fmt=None, datefmt=None, **level_colors):
super(CmdColoredFormatter, self).__init__(fmt, datefmt)
self.LOG_COLORS = {} # a dict, used to convert log level to color
self.init_log_colors()
self.set_level_colors(**level_colors) def init_log_colors(self):
''' initialize log config '''
for lev in CmdColoredFormatter.__LEVELS:
self.LOG_COLORS[lev] = '%s' def set_level_colors(self, **kwargs):
''' set each level different colors '''
lev_set = CmdColoredFormatter.__LEVEL_SET
color_set = CmdColor.get_color_set() # check log level and set colors
for lev, color in kwargs.items():
lev, color = lev.upper(), color.lower()
if lev not in lev_set:
raise KeyError("log level '%s' does not exist" % lev)
if color not in color_set:
raise ValueError("log color '%s' does not exist" % color)
self.LOG_COLORS[lev] = ''.join([CmdColor.get_color_by_str(color), '%s', CmdColor.get_color_by_str('reset')]) def format(self, record):
''' @override BasicFormatter.format'''
msg = super(CmdColoredFormatter, self).format(record)
# msg = BasicFormatter.format(self, record) # 本行和上一行等价
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 可以简化我们的代码,并且具有良好的可扩展新

 # 在某个函数定义内调用,可获得函数的所有参数,以 dict 为形式
# 每次调用时返回一个新的 dict,注意,参数 self 或者 cls 也会包含在内
# 需要用 pop() 方法去除
arg_dict = locals() # 获取对象中某个属性或方法,不存在时返回 default 中的内容
getattr(obj, name, default=None)
# 动态设置对象中的属性值或者函数指针
setattr(obj, name, value)

3. 添加 handler: 

目前还没有用到更复杂的 http 和 socket 的 handler , 因此这里暂时没有封装相应的方法,后续可以封装成一个简单工厂,等用到再说。

目前只用到了 fileHandler 和 streamHandler ,因此只能输出到控制台以及文件。

 def __add_filehandler(self):
''' Add a file handler to logger '''
# Filehandler
if self.backup_count == 0:
self.filehandler = logging.FileHandler(self.filename, self.filemode)
# RotatingFileHandler
elif self.when is None:
self.filehandler = logging.handlers.RotatingFileHandler(self.filename,
self.filemode, self.limit, self.backup_count)
# TimedRotatingFileHandler
else:
self.filehandler = logging.handlers.TimedRotatingFileHandler(self.filename,
self.when, 1, self.backup_count) formatter = BasicFormatter(self.filefmt, self.filedatefmt)
self.filehandler.setFormatter(formatter)
self.logger.addHandler(self.filehandler) def __add_streamhandler(self):
''' Add a stream handler to logger '''
self.streamhandler = logging.StreamHandler()
self.streamhandler.setLevel(self.cmdlevel)
formatter = CmdColoredFormatter(self.cmdfmt, self.cmddatefmt,
**self.cmd_color_dict) if self.colorful else BasicFormatter(self.cmdfmt, self.cmddatefmt)
self.streamhandler.setFormatter(formatter)
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__ 是一个容易忽略的细节。

相应实现如下:

 @classmethod
def get_logger(cls, **kwargs):
loggername = kwargs['loggername']
cls.__lock.acquire() # lock current thread
if loggername in cls.__name2logger:
cls.__name2logger[loggername].set_logger(**kwargs)
else:
log_obj = object.__new__(cls)
cls.__init__(log_obj, **kwargs)
cls.__name2logger[loggername] = log_obj
cls.__lock.release() # release lock
return cls.__name2logger[loggername]

get_logger 的实现

5. set_logger: 通过一个方法设置所有的相关参数

这里体现出了 setattr 的用处,通过这样的方法能够动态的添加 / 修改相关的对象属性

通过对象的属性重新加载

其实现如下:

 def set_logger(self, **kwargs):
''' Configure logger with dict settings '''
for k, v in kwargs.items():
if k not in Logger.__log_arg_set:
raise KeyError("config argument '%s' does not exist" % k)
setattr(self, k, v) # add instance attributes if self.cmd_color_dict is None:
self.cmd_color_dict = {'debug': 'green', 'warning':'yellow', 'error':'red', 'critical':'purple'}
if isinstance(self.cmdlevel, str):
self.cmdlevel = getattr(logging, self.cmdlevel.upper(), logging.DEBUG)
if isinstance(self.filelevel, str):
self.filelevel = getattr(logging, self.filelevel.upper(), logging.INFO) self.__init_logger()
self.__import_log_func()
if self.cmdlog:
self.__add_streamhandler()
if self.filelog:
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'}

相应的实现如下:

 import inspect

 # 基于 loggername 的单例装饰器
def singletonLoggerByName(cls):
__name2logger = {}
def getValueByArg(orderedDict, arg):
return str(orderedDict[arg]).partition('=')[-1] def wrapper(self, logger_init, **kwargs):
default_values = inspect.signature(logger_init).parameters
name = kwargs.get('loggername', getValueByArg(default_values, 'loggername'))
print('name not in __name2logger: %r' % (name not in __name2logger))
if name not in __name2logger:
logger_init(self, **kwargs)
__name2logger[name] = self
print(__name2logger[name])
return __name2logger[name] # 装饰器用于 __init__ 是不行的,因为 python 中 __init__ 只能返回 None, 这样单例模式中后续的引用无法绑定到第一次的实例上
return wrapper

7.效果图: 

完整代码详见:log/logger.py

参考资料:大佬的博客

今天就到这里啦~lalala

python 简单日志框架 自定义logger的更多相关文章

  1. Moon转告给你一个比Log4net更好日志框架--TracerX Logger 及其对应的日志查看器

    一.介绍 TracerX logger是一个易于上手,且拥有众多高级特性的.NET日志框架. 它能够发送输出结果到多目的地(循环文件.事件日志等....).它也能生成文本和二进制文件.它拥有一个强大的 ...

  2. python简单日志处理

    简单日志处理 import datetime import re logfile='''58.61.164.141 - - [22/Feb/2010:09:51:46 +0800] "GET ...

  3. Python 第三方日志框架loguru使用

    解决中文乱码问题 项目地址 github: https://github.com/Delgan/loguru 文档:https://loguru.readthedocs.io/en/stable/in ...

  4. 日志框架之Logger

    概述 在我们日常的开发中,肯定是少不了要和 Log 打交道,回想一下我们是怎么使用 Log 的:先定义一个静态常量 TAG,TAG 的值通常是当前类的类名,然后在需要打印 Log 的地方,调用 Log ...

  5. python简单日志统计

    业务场景:在一个目录里,有许多日志文件,里面是一条条的json数据,格式如下,为防止一个账号被多个ip使用,现在我想知道:哪些用户登录了哪些ip,和哪些ip登录了哪些用户,如果一个ip对应一个用户,就 ...

  6. Python 日志打印之自定义logger handler

    日志打印之自定义logger handler By:授客 QQ:1033553122 #实践环境 WIN 10 Python 3.6.5 #实践代码 handler.py #!/usr/bin/env ...

  7. python nose测试框架全面介绍七--日志相关

    引: 之前使用nose框架时,一直使用--logging-config的log文件来生成日志,具体的log配置可见之前python nose测试框架全面介绍四. 但使用一段时间后,发出一个问题,生成的 ...

  8. 一个简单好用的日志框架NLog

    之前我介绍过如何使用log4net来记录日志,但最近喜欢上了另一个简单好用的日志框架NLog. 关于NLog和log4net的比较这里就不多讨论了,感兴趣的朋友可以参看.NET日志工具介绍和log4n ...

  9. 【Java】Java日志框架Logback的简单例子

    常用的日志框架 SLF4J,全称Simple Logging Facade for Java,即Java简单日志外观框架,顾名思义,它并非具体的日志实现,而是日志外观框架 java.util.logg ...

随机推荐

  1. springboot-为内置tomcat设置虚拟目录

    需求 项目使用springboot开发,以jar包方式部署.项目中文件上传均保存到D判断下的upload目录下. 在浏览器中输入http://localhost:8080/upload/logo_1. ...

  2. android极光推送初步了解

    推送可以及时,主动的与用户发起交互 (1)继承jar包,照示例AndroidManifest.xml添加. (2)自定义MyApp继承自Application,在onCreate方法中调用JPushI ...

  3. 与http协作的web服务器、http首部(第五章、第六章)

    第五章 与http协作的web服务器 1.用单台虚拟主机实现多个域名 通过域名访问主机,经过DNS解析成ip地址,反向代理,可以代理多台服务器,正向代理则相反,代理客户端 2.通信数据转化程序:代理. ...

  4. html 制作静态页面新知识

    1.在区块线边框添加一条水平线 例如:<div  style:"height :300px;width:800px;border-bottom: solid 1px orange ;& ...

  5. Python代码解决RenderView窗口not found问题

    源 起 Error:setParent: Object 'renderView' not found 这是一个在工作中很常见的问题,以前做特效的时候有10%的概率会碰到,多发生在打开其他组交接来的Ma ...

  6. 【比赛】STSRM 09

    第一题 题意:n个点,每个点坐标pi属性ai,从右往左将遇到的点向左ai范围内的点消除,后继续扫描. 现可以在扫描开始前提前消除从右往左任意点,问最少消除数(提前+扫描). n,pi,ai<=1 ...

  7. 【BZOJ】1419 Red is good

    [算法]期望DP [题解]其实把状态表示出来就是很简单的期望DP. f[i][j]表示i张红牌,j张黑牌的期望. i=0时,f[0][j]=0. j=0时,f[i][0]=i. f[i][j]=max ...

  8. Git彻底删除历史提交记录的方法

    有时候我们可能会遇到git提交错误的情况,比如提交了敏感的信息或者提交了错误的版本.这个时候我们想将提交到代码库的记录删除,我们要怎么做呢? 首先,我们需要找到我们需要回滚到的提交点的hash,可以使 ...

  9. HTML中设置超链接字体 & 字体颜色

    定义链接样式 CSS为一些特殊效果准备了特定的工具,我们称之为“伪类”.其中有几项是我们经常用到的,下面我们就详细介绍一下经常用于定义链接样式的四个伪类,它们分别是: :link :visited : ...

  10. php中的__call()函数重载

    <?php #调用类中没有的方法时, 会自动调用__call方法重载 #第一个参数是调用时的方法名, 第二个参数为参数组成的数组 class Cat{ public function Hello ...