最近在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
64
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
33
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
11
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给日志处理很大的自由度,我们可以非常方便的将日志放到各各地方,自由处理。