Python logging 模块封装

python自带的记录日志模块 logging 非常强大,但是每次写程序都要写很多东西来配置使用,体验不怎么友好,花时间封装在一个文件里面,用的时候直接调用,并且提供配置接口保留其强大的功能还是非常有必要的。

logging介绍

Python的logging模块提供了通用的日志系统,可以方便第三方模块或者是应用使用。这个模块提供不同的日志级别,并可以采用不同的方式记录日志,比如文件,HTTP GET/POST,SMTP,Socket等,甚至可以自己实现具体的日志记录方式。

logging模块与log4j的机制是一样的,只是具体的实现细节不同。模块提供logger,handler,filter,formatter。

  • logger:提供日志接口,供应用代码使用。
  • handler:将日志记录(log record)发送到合适的目的地(destination),比如文件,socket,cmd等。一个logger对象可以通过addHandler方法添加0到多个handler,每个handler又可以定义不同日志级别,以实现日志分级过滤显示。每个handler还可以单独设置自己的formatter格式。
  • filter:提供一种优雅的方式决定一个日志记录是否发送到handler。
  • formatter:指定日志记录输出的具体格式。

与log4j类似,logger,handler和日志消息的调用可以有具体的日志级别(Level),只有在日志消息的级别大于logger和handler的级别才会呈现。

默认的日志级别设置为WARNING(日志级别等级CRITICAL > ERROR > WARNING > INFO > DEBUG > NOTSET), 小于WARNING级别的日志都不输出, 大于等于WARNING级别的日志都会输出。

更多可以自己搜索一下。

封装成log.py

源码在github上有:python_log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Last Update: 2015/06/30 10:53:52
# file mylog.py
__author__ = "Mingo <wangbandi@gmail.com>"
__status__ = "Development"

__all__ = ['set_logger', 'debug', 'info', 'warning', 'error',
'critical', 'exception']

import os
import sys
import traceback
import logging
import logging.handlers

class ColoredFormatter(logging.Formatter):
'''A colorful formatter.'''

def __init__(self, fmt = None, datefmt = None):
logging.Formatter.__init__(self, fmt, datefmt)

def format(self, record):
# Color escape string
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'
# Define log color
LOG_COLORS = {
'DEBUG': '%s',
'INFO': COLOR_GREEN + '%s' + COLOR_RESET,
'WARNING': COLOR_YELLOW + '%s' + COLOR_RESET,
'ERROR': COLOR_RED + '%s' + COLOR_RESET,
'CRITICAL': COLOR_RED + '%s' + COLOR_RESET,
'EXCEPTION': COLOR_RED + '%s' + COLOR_RESET,
}
level_name = record.levelname
msg = logging.Formatter.format(self, record)
return LOG_COLORS.get(level_name, '%s') % msg

class Log():
def __init__(self, filename = None, mode = 'a',
cmdlevel='DEBUG',
filelevel='INFO',
cmdfmt = '[%(asctime)s] %(filename)s line:%(lineno)d %(levelname)-8s%(message)s',
filefmt = '[%(asctime)s] %(levelname)-8s%(message)s',
cmddatefmt = '%H:%M:%S',
filedatefmt = '%Y-%m-%d %H:%M:%S',
backup_count = 0, limit = 20480, when = None, colorful = False):

self.filename = filename
if self.filename is None:
self.filename = getattr(sys.modules['__main__'], '__file__', 'log.py')
self.filename = os.path.basename(self.filename.replace('.py', '.log'))
#self.filename = os.path.join('/tmp', self.filename)
self.mode = mode
self.cmdlevel = cmdlevel
self.filelevel = filelevel
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.DEBUG)
self.filefmt = filefmt
self.cmdfmt = cmdfmt
self.filedatefmt = filedatefmt
self.cmddatefmt = cmddatefmt
self.backup_count = backup_count
self.limit = limit
self.when = when
self.colorful = colorful
self.logger = None
self.streamhandler = None
self.filehandler = None
if self.cmdlevel > 10:
self.filefmt = '[%(asctime)s] %(levelname)-8s%(message)s'
self.cmdfmt = '[%(asctime)s] %(levelname)-8s%(message)s'
self.cmddatefmt = '%Y-%m-%d %H:%M:%S'
self.set_logger(cmdlevel = self.cmdlevel)

def init_logger(self):
'''Reload the logger.'''
if self.logger is None:
self.logger = logging.getLogger()
else:
logging.shutdown()
self.logger.handlers = []
self.streamhandler = None
self.filehandler = None
self.logger.setLevel(logging.DEBUG)

def add_streamhandler(self):
'''Add a stream handler to the logger.'''
self.streamhandler = logging.StreamHandler()
self.streamhandler.setLevel(self.cmdlevel)
if self.colorful:
formatter = ColoredFormatter(self.cmdfmt, self.cmddatefmt)
else:
formatter = logging.Formatter(self.cmdfmt, self.cmddatefmt,)
self.streamhandler.setFormatter(formatter)
self.logger.addHandler(self.streamhandler)

def add_filehandler(self):
'''Add a file handler to the logger.'''
# Choose the filehandler based on the passed arguments
if self.backup_count == 0: # Use FileHandler
self.filehandler = logging.FileHandler(self.filename, self.mode)
elif self.when is None: # Use RotatingFileHandler
self.filehandler = logging.handlers.RotatingFileHandler(self.filename,
self.mode, self.limit, self.backup_count)
else: # Use TimedRotatingFileHandler
self.filehandler = logging.handlers.TimedRotatingFileHandler(self.filename,
self.when, 1, self.backup_count)
self.filehandler.setLevel(self.filelevel)
formatter = logging.Formatter(self.filefmt, self.filedatefmt,)
self.filehandler.setFormatter(formatter)
self.logger.addHandler(self.filehandler)

def set_logger(self, **kwargs):
'''Configure the logger.'''
keys = ['mode','cmdlevel','filelevel','filefmt','cmdfmt',\
'filedatefmt','cmddatefmt','backup_count','limit',\
'when','colorful']
for (key, value) in kwargs.items():
if not (key in keys):
return False
setattr(self, key, value)
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.DEBUG)
if not "cmdfmt" in kwargs:
self.filefmt='[%(asctime)s] %(filename)s line:%(lineno)d %(levelname)-8s%(message)s'
self.filedatefmt = '%Y-%m-%d %H:%M:%S'
self.cmdfmt='[%(asctime)s] %(filename)s line:%(lineno)d %(levelname)-8s%(message)s'
self.cmddatefmt = '%H:%M:%S'
if self.cmdlevel > 10:
self.filefmt = '[%(asctime)s] %(levelname)-8s%(message)s'
self.cmdfmt = '[%(asctime)s] %(levelname)-8s%(message)s'
self.cmddatefmt = '%Y-%m-%d %H:%M:%S'
self.init_logger()
self.add_streamhandler()
self.add_filehandler()
# Import the common log functions for convenient
self.import_log_funcs()
return True

def import_log_funcs(self):
'''Import the common log functions from the logger to the class'''
log_funcs = ['debug', 'info', 'warning', 'error', 'critical',
'exception']
for func_name in log_funcs:
func = getattr(self.logger, func_name)
setattr(self, func_name, func)

def trace(self):
info = sys.exc_info()
for file, lineno, function, text in traceback.extract_tb(info[2]):
self.error('%s line:%s in %s:%s' % (file, lineno, function, text))
self.error('%s: %s' % info[:2])

if __name__ == '__main__':
log = Log(cmdlevel='info')
# log = Log(cmdlevel='debug')
log.set_logger(cmdlevel='debug')
log.debug('debug')
log.info('debug%s' % 'haha')
log.error((1,2))
log.error('debug')
log.info({'a':1,'b':2})
os.system("pause")
class A():
def __init__(self, log):
self.log = log
def a(self,a):
self.log.info(a)
class B():
def __init__(self, log):
self.log = log
def b(self,a):
self.log.info(a)
a = A(log)
a.a("test a")
b = B(log)
b.b(5)

def fun(a):
return 10/a
try:
a = fun(0)
except:
log.trace()

例子

用的时候把log.py丢到工程目录然后import进来初始化一个Log对象即可。

如果全部使用默认配置(具体值请看Log类的init函数):

1
2
3
4
5
6
7
# tmp.py
import log
log = log.Log()
log.debug('hello, world')
log.info('hello, world')
log.error('hello, world')
log.critical('hello, world')

得到结果:
在命令行跟文件里面分别记录运行日志,命令行呈现大于DEBUG级别的记录,文件记录大于INFO级别的记录。文件名跟主程序的py名字一样,这里是tmp.log。里面内容:

1
2
3
4
5
6
[2016-05-29 17:17:05] tmp.py line:6 INFO    hello, world
[2016-05-29 17:17:05] tmp.py line:7 ERROR hello, world
[2016-05-29 17:17:05] tmp.py line:8 CRITICALhello, world
[2016-05-29 17:17:49] tmp.py line:6 INFO hello, world
[2016-05-29 17:17:49] tmp.py line:7 ERROR hello, world
[2016-05-29 17:17:49] tmp.py line:8 CRITICALhello, world

cmd显示运行结果:

1
2
3
4
5
[17:17:05] tmp.py line:5 DEBUG   hello, world
[17:17:05] tmp.py line:6 INFO hello, world
[17:17:05] tmp.py line:7 ERROR hello, world
[17:17:05] tmp.py line:8 CRITICALhello, world
Hit any key to close this window...

里面的内容符合对文件跟cmd的level级别筛选规则,显示的格式也分别符合Log类init默认初始化参数的formatter设置的规则。

如果要更改设置,直接调用log.set_logger就可以了。一般来说很少会有更改设置的情况,都是直接初始化就一直用到结束。有一种情况就是给出一个可以调整log level的级别的配置接口。这样程序运行的过程中,遇到bug用户可以调整日志级别,然后把debug日志记录获取到发给开发人员分析。

另外,封装的log.py比原来的logging模块多了一个trace接口。可以打印exception的详细信息。具体用法可以看封装的文件里边的例子。

其他

1. formatter配置

下面是formatter用到的匹配规则,其中asctime还可以进一步配置:命令行通过cmddatefmt、文件日志通过filedatefmt,具体写法可参照Log类的初始化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+---------------------+-----------------------------------------------------------------------------
| %(name)s | Logger的名字
| %(levelno)s | 数字形式的日志级别
| %(levelname)s | 文本形式的日志级别
| %(pathname)s | 调用日志输出函数的模块的完整路径名,可能没有
| %(filename)s | 调用日志输出函数的模块的文件名
| %(module)s | 调用日志输出函数的模块名
| %(funcName)s | 调用日志输出函数的函数名
| %(lineno)d | 调用日志输出函数的语句所在的代码行
| %(created)f | 当前时间,用UNIX标准的表示时间的浮 点数表示
| %(relativeCreated)d | 输出日志信息时的,自Logger创建以 来的毫秒数
| %(asctime)s | 字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒
| %(thread)d | 线程ID。可能没有
| %(threadName)s | 线程名。可能没有
| %(process)d | 进程ID。可能没有
| %(message)s | 用户输出的消息
+---------------------+-----------------------------------------------------------------------------

2. Use set_logger to change settings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Change limit size in bytes of default rotating action
log.set_logger(limit = 10240) # 10M

# Use time-rotated file handler, each day has a different log file, see
# logging.handlers.TimedRotatingFileHandler for more help about 'when'
log.set_logger(when = 'D', limit = 1)

# Use normal file handler (not rotated)
log.set_logger(backup_count = 0)

# File log level set to INFO, and stdout log level set to DEBUG
log.set_logger(cmdlevel = 'DEBUG', filelevel = 'INFO')

# Change default log file name and log mode
log.set_logger(filename = 'yyy.log', mode = 'w')

# Change default log formatter
log.set_logger(cmdfmt = '[%(levelname)s] %(message)s')
正在加载中……