最近在python中使用日志处理的时候有一个新的需求,将日志写入到数据库中,一开始比较懒,写一个循环,定时的去读本地日志文件,将里面的内容直接写到数据库中,很显然这种处理很low,后来又在外面写了一个服务,但本质上还是用循环读的方式将读出来的内容发个post请求然后入库,这两种方式本质上是一样的,而且第二种在发post请求的时候还遇到了内容过多导致服务解析失败的问题。

其实这个问题本质上是对日志的处理,我们常规的处理主要有两种,一种是在控制台上输出,一种是写到文件中,其实这个问题只是多一步,再将日志写入数据库即可,但是直接写入数据库又失去了一些灵活性,我就想每次打日志的时候将日志信息发一个http请求,在服务端对日志进行写数据库操作。

其实在python的logging模块下有一个HTTPHandler,它可以实现在写日志的时候将该日志向指定的url发送请求,我们来简单看一下它的实现。

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
class HTTPHandler(logging.Handler):
"""
A class which sends records to a Web server, using either GET or
POST semantics.
"""
def __init__(self, host, url, method="GET"):
"""
Initialize the instance with the host, the request URL, and the method
("GET" or "POST")
"""
logging.Handler.__init__(self)
method = string.upper(method)
if method not in ["GET", "POST"]:
raise ValueError, "method must be GET or POST"
self.host = host
self.url = url
self.method = method

def mapLogRecord(self, record):
"""
Default implementation of mapping the log record into a dict
that is sent as the CGI data. Overwrite in your class.
Contributed by Franz Glasner.
"""
return record.__dict__

def emit(self, record):
"""
Emit a record.

Send the record to the Web server as an URL-encoded dictionary
"""
try:
import httplib, urllib
host = self.host
h = httplib.HTTP(host)
url = self.url
data = urllib.urlencode(self.mapLogRecord(record))
if self.method == "GET":
if (string.find(url, '?') >= 0):
sep = '&'
else:
sep = '?'
url = url + "%c%s" % (sep, data)
h.putrequest(self.method, url)
# support multiple hosts on one IP address...
# need to strip optional :port from host, if present
i = string.find(host, ":")
if i >= 0:
host = host[:i]
h.putheader("Host", host)
if self.method == "POST":
h.putheader("Content-type",
"application/x-www-form-urlencoded")
h.putheader("Content-length", str(len(data)))
h.endheaders()
if self.method == "POST":
h.send(data)
h.getreply() #can't do anything with the result
except (KeyboardInterrupt, SystemExit):
raise
except:
self.handleError(record)

它继承了logging.Handler类,并且重写了emit方法,注意,emit 方法是必须要重写的,如果不重写会抛异常。而 emit 方法也就是在执行debug info error 等日志方法时要真正执行的方法。它会在记录日志的时候向指定的host,url 发送指定的方法(GET 或者 POST)请求,而发送的数据则为record原始对象,但是这个对象并不满足我的需求,我只想要按照我的格式打印出来的信息即可,所以这里我们来重新写一个handler来处理请求。

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
import requests 

class CustomHandler(logging.Handler):
def __init__(self,host,uri,baseParam,method="POST"):
logging.Handler.__init__(self)
self.url = "%s/%s"%(host,uri)
self.baseParam = baseParam
method = string.upper(method)
if method not in ["GET", "POST"]:
raise ValueError, "method must be GET or POST"
self.method = method

def emit(self, record):
'''
重写emit方法,这里主要是为了把初始化时的baseParam添加进来
:param record:
:return:
'''
self.baseParam["logdata"] = self.format(record)
if self.method == "GET":
if (string.find(self.url, '?') >= 0):
sep = '&'
else:
sep = '?'
url = self.url + "%c%s" % (sep, data)
requests.get(url,timeout=1)
else:
headers = {
"Content-type":"application/x-www-form-urlencoded",
"Content-length":str(len(data))
}
requests.post(self.url,data=self.baseParam,headers=headers,timeout=1)

我重新定义了一个CustomHandler类,继承自logging.Handler,重写了emit方法,注意这里有一个比较关键的一行代码
self.baseParam["logdata"] = self.format(record) 这行是将record按照自定义的Formatter进行格式化,构造成我想要的样子。

下面来看下它是如何使用的

1
2
3
4
5
6
7
8
9
10
import logging

logger = logging.getLogger("yyx")
logger.setLevel(logging.DEBUG)
fmt = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S')
httphandler = CustomHandler(r"http://127.0.0.1","collog",{"taskid":"aaaa","casenum":123})
httphandler.setFormatter(fmt) #这行代码就是将日志格式化成我们想要的样子
logger.addHandler(httphandler)

logger.debug("aaa杨彦星bbb")

这时当再打印日志的时候就会向127.0.0.1/collog 接口发送一个post的请求,请求的参数为

1
2
3
4
5
{
"taskid":"aaa",
"casenum":123,
"logdata":"[2019-12-12 19:06:40] [INFO] file:test.py line:12 aaa杨彦星bbb"
}

随后我在外面的服务就可以接受到并且将这条记录写入数据库了。

自定义handler给日志处理很大的自由度,我们可以非常方便的将日志放到各各地方,自由处理。