python tornado下logging.handlers.HTTPHandler多打印一個None的解決方案

0、
版本,忽略版本寫博客都是耍流氓!
Python==2.7.10 
Tornado==4.2

1、
背景:因爲用tornado,各種異步,導致業務邏輯日誌不能準確定位,因爲它輸出日誌輸到一半就要去搞別的業務邏輯,然後再回來繼續輸出日誌,導致日誌看起來像是:
2017-09-29 23:59:57,459 BusinessFactory.py-create()-270 [INFO] [1000108695] 【獲取用戶Profile接口】
2017-09-29 23:59:57,460 GetUserProfile.py-run()-21 [INFO] 獲取用戶profile,user_id=1000108695
2017-09-29 23:59:57,470 UserProfile.py-create_user_profile()-45 [INFO] 用戶設備個數:0
2017-09-29 23:59:57,494 APIMain.py-post()-19 [INFO] 【版本:234, 協議:pb】
2017-09-29 23:59:57,517 BusinessFactory.py-create()-270 [INFO] [1000109733] 【獲取系統設置接口】
2017-09-29 23:59:57,549 BusinessFactory.py-create()-270 [INFO] [1000109733] 【獲取用戶Config接口】
2017-09-29 23:59:57,559 web.py-log_request()-1908 [INFO] 200 POST /api (127.0.0.1) 66.55ms
2017-09-29 23:59:57,584 UserProfile.py-create_user_profile()-67 [INFO] 用戶功課個數:1
2017-09-29 23:59:57,586 UserProfile.py-create_user_profile()-80 [INFO] 1000108695有第三方用戶
2017-09-29 23:59:57,588 web.py-log_request()-1908 [INFO] 200 POST /api (127.0.0.1) 154.04ms
可以看到,“獲取用戶Profile接口”打印到“用戶設備個數:0”這一句後就開始去處理“獲取系統配置接口”,處理完系統接口後再繼續打印“用戶功課個數:1”。。。所以不能精準定位啊!!各種谷歌無果,因爲人家提出的解決方案,基本上都是基於日誌服務器+logging.handlers.HTTPHandler,但這個並不能解決日誌不成順序的問題呀。
無奈自己造輪子吧。
造輪子期間遇到這麼一件有趣的事,打印到日誌服務器總會帶一個None:
2017-09-29 17:54:51,780 - GetUserProfileFromThird.run.78 - ERROR - x1
None
2017-09-29 17:54:51,780 - GetUserProfileFromThird.run.78 - ERROR - yyy
None

2、
首先貼出我們的代碼,先感謝殘陽似血的博客:http://qinxuye.me/article/build-log-server-with-tornado/,我們就在這個基礎上修改。
因爲受到tornado代碼的精神污染,也開始喜歡在代碼中加大量的註釋。。

tornado的業務handler:
# coding=utf-8
import re
import json
import logging

import tornado.web
from mylog.mylogger import my_logger


class LogAddHandler(tornado.web.RequestHandler):
    tuple_reg = re.compile("^\([^\(\)]*\)$")
    float_reg = re.compile("^\d*\.\d+$")
    int_reg = re.compile("^\d+$")

    def _extract(self, string):
        '''
        由於通過request.arguments的值join起來的仍然是個字符串,這裏我們需要將其轉化爲Python對象
        通過分析,我們可以知道這個對象只能是tuple、float和int
        簡單的來說,這個地方可以使用eval方法,但是通常情況下,"eval is evil"
        所以這裏通過正則的方法進行解析
        '''
        if self.tuple_reg.match(string):
            # 這裏用json.loads來加載一個JS的數組方式來解析Python元組,將前後的括號專爲方括號
            # JS裏的None爲null,這樣得到一個Python list,再轉化爲元組
            return tuple(json.loads('[%s]' % string[1: -1].replace('None', 'null')))
        elif self.float_reg.match(string):
            return float(string)
        elif self.int_reg.match(string):
            return int(string)
        return string

    def post(self):
        '''
        原始的self.request.arguments如下:
        import pprint
        original_args = dict(
            [(k, v) for (k, v) in self.request.arguments.iteritems()]
        )
        pprint.pprint(original_args)

        {'args': ['()'],
         'created': ['1506738449.32'],
         'exc_info': ['None'],
         'exc_text': ['None'],
         'filename': ['GetUserProfileFromThird.py'],
         'funcName': ['run'],
         'levelname': ['ERROR'],
         'levelno': ['40'],
         'lineno': ['78'],
         'module': ['GetUserProfileFromThird'],
         'msecs': ['315.39106369'],
         'msg': ["['x1', 'yyy']"],
         'name': ['monitor'],
         'pathname': ['/Users/ouyang/PycharmProjects/myApp/biz_handlers/third_party/GetUserProfileFromThird.py'],
         'process': ['98843'],
         'processName': ['MainProcess'],
         'relativeCreated': ['57897774.2171'],
         'thread': ['140736844747712'],
         'threadName': ['MainThread']
         }

        '''

        args = dict(
            [(k, self._extract(''.join(v))) for (k, v) in self.request.arguments.iteritems()]
        )
        '''
        import pprint
        pprint.pprint(args)
        結果:
        {
            'threadName': 'MainThread',
            'name': 'monitor',
            'thread': 140736060957632,
            'created': 1506739312.87,
            'process': 1520,
            'args': (),
            'msecs': 872.350931168,
            'filename': 'GetUserProfileFromThird.py',
            'levelno': 40,
            'processName': 'MainProcess',
            'lineno': 78,
            'pathname': '/Users/ouyang/PycharmProjects/myApp/biz_handlers/third_party/GetUserProfileFromThird.py',
            'module': 'GetUserProfileFromThird',
            'exc_text': 'None',
            'exc_info': 'None',
            'funcName': 'run',
            'relativeCreated': 259876.040936,
            'levelname': 'ERROR',
            'msg': "['x1', 'yyy']"
        }
        '''

        '''
        因爲和client端約定好,他們那邊用如下格式傳遞過來
            from logclient import client_logger
            logs = ["x1","yyy"]
            client_logger.error(logs)
        所以這邊要先還原msg_lst = ['x1', 'yyy']
        '''
        msg_lst = args['msg'].replace('[', '').replace(']', '').replace('\'', '').split(',')
        msg_lst = [v.strip() for v in msg_lst]

        '''
        替換'None'爲None,否則會引發如下日誌:
        2017-09-30 11:09:10,625 - GetUserProfileFromThird.run.78 - ERROR - x1
        None
        2017-09-30 11:09:10,625 - GetUserProfileFromThird.run.78 - ERROR - yyy
        None
        '''
        for key, value in args.iteritems():
            if value == 'None':
                args[key] = None

        for msg in msg_lst:
            # 每一次只寫msg_lst中的一條記錄
            args['msg'] = msg

            #import pdb
            #pdb.set_trace()

            # makeLogRecord接受一個字典作爲參數
            record = logging.makeLogRecord(args)
            my_logger.handle(record)
日誌服務器的log配置, mylogger.py:
# coding=utf-8
import os
import sys
import logging


# 創建一個全局的logger
def get_logger():
    print '#########Create a global logger#########'
    logger = logging.getLogger('server_logger')
    filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'my.log')
    handler = logging.FileHandler(filename)
    formatter = logging.Formatter('%(asctime)s-%(name)s-%(module)s.%(funcName)s.%(lineno)d - %(levelname)s - %(message)s')
    handler.setFormatter(formatter)
    handler.setLevel(logging.ERROR)

    '''
    # logger.propagate = False 不要傳遞到父親的參數
    # 默認爲True,如果爲True,那麼root這個logger也會收到。到時候在控制檯就會打印:
    2017-09-30 11:26:22,493-monitor-GetUserProfileFromThird.run.78 - ERROR - x1
    ERROR:monitor:x1
    2017-09-30 11:26:22,493-monitor-GetUserProfileFromThird.run.78 - ERROR - yyy
    ERROR:monitor:yyy

    控制代碼在:logging的Logger類中1318行的callHandlers():
        def callHandlers(self, record):
            """
            如果propagate=True,則會進去else分支,c = c.parent一直回溯到root,
            root也會打印到streamHandler控制檯,導致重複輸出。
            """
            c = self
            found = 0
            while c:
                for hdlr in c.handlers:
                    found = found + 1
                    if record.levelno >= hdlr.level:
                        hdlr.handle(record)
                if not c.propagate:
                    c = None    #break out
                else:
                    c = c.parent
            if (found == 0) and raiseExceptions and not self.manager.emittedNoHandlerWarning:
                sys.stderr.write("No handlers could be found for logger"
                                 " \"%s\"\n" % self.name)
                self.manager.emittedNoHandlerWarning = 1
    '''
    logger.propagate = False


    logger.addHandler(handler)

    # 同時輸到屏幕,便於實施觀察
    handle_for_screen = logging.StreamHandler(sys.stdout)
    handle_for_screen.setFormatter(formatter)
    logger.addHandler(handle_for_screen)
    return logger

my_logger = get_logger()
在其他項目中的log_client.py
# coding=utf-8
import logging
import logging.handlers

logging_host = '127.0.0.1'
logging_port = 8888
logging_add_url = '/log/'


def get_logger():
    logger = logging.getLogger('monitor')
    http_handler = logging.handlers.HTTPHandler(
        '%s:%s' % (logging_host, logging_port),
        logging_add_url,
        method='POST'
    )
    http_handler.setLevel(logging.ERROR)
    logger.addHandler(http_handler)

    return logger

client_logger = get_logger()

3、
開始單步調試,都在logging/__init__,py中!
往下class Logger中(1286行):
    def handle(self, record):
        """
        Call the handlers for the specified record.

        This method is used for unpickled records received from a socket, as
        well as those created locally. Logger-level filtering is applied.
        """
        if (not self.disabled) and self.filter(record):
            self.callHandlers(record)    ##############<<<<< JUMP
往下class Logger中 (1318行):
    def callHandlers(self, record):
        """
        Pass a record to all relevant handlers.

        Loop through all handlers for this logger and its parents in the
        logger hierarchy. If no handler was found, output a one-off error
        message to sys.stderr. Stop searching up the hierarchy whenever a
        logger with the "propagate" attribute set to zero is found - that
        will be the last logger whose handlers are called.
        """
        c = self
        found = 0
        while c:
            for hdlr in c.handlers:
                found = found + 1
                if record.levelno >= hdlr.level:
                    hdlr.handle(record)    ##############<<<<< JUMP
            if not c.propagate:
                c = None    #break out
            else:
                c = c.parent
        if (found == 0) and raiseExceptions and not self.manager.emittedNoHandlerWarning:
            sys.stderr.write("No handlers could be found for logger"
                             " \"%s\"\n" % self.name)
            self.manager.emittedNoHandlerWarning = 1
往下class Handler中(744行):
    def handle(self, record):
        """
        Conditionally emit the specified logging record.

        Emission depends on filters which may have been added to the handler.
        Wrap the actual emission of the record with acquisition/release of
        the I/O thread lock. Returns whether the filter passed the record for
        emission.
        """
        rv = self.filter(record)
        if rv:
            self.acquire()
            try:
                self.emit(record)   ################<<<<< JUMP
            finally:
                self.release()
        return rv
往下class StreamHandler中(847行):
    def emit(self, record):
        """
        Emit a record.

        If a formatter is specified, it is used to format the record.
        The record is then written to the stream with a trailing newline.  If
        exception information is present, it is formatted using
        traceback.print_exception and appended to the stream.  If the stream
        has an 'encoding' attribute, it is used to determine how to do the
        output to the stream.
        """
        try:
            msg = self.format(record)    ###############<<<<< JUMP
            stream = self.stream
            fs = "%s\n"
            if not _unicode: #if no unicode support...
                stream.write(fs % msg)
            else:
                try:
                    if (isinstance(msg, unicode) and
                        getattr(stream, 'encoding', None)):
                        ufs = u'%s\n'
                        try:
                            stream.write(ufs % msg)
                        except UnicodeEncodeError:
                            #Printing to terminals sometimes fails. For example,
                            #with an encoding of 'cp1251', the above write will
                            #work if written to a stream opened or wrapped by
                            #the codecs module, but fail when writing to a
                            #terminal even when the codepage is set to cp1251.
                            #An extra encoding step seems to be needed.
                            stream.write((ufs % msg).encode(stream.encoding))
                    else:
                        stream.write(fs % msg)
                except UnicodeError:
                    stream.write(fs % msg.encode("UTF-8"))
            self.flush()
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)
往下class Handler中(721行):
    def format(self, record):
        """
        Format the specified record.

        If a formatter is set, use it. Otherwise, use the default formatter
        for the module.
        """
        if self.formatter:
            fmt = self.formatter
        else:
            fmt = _defaultFormatter
        return fmt.format(record)   ###############<<<<< JUMP

往下class Formatter中(458行):
    def format(self, record):
        """
        Format the specified record as text.

        The record's attribute dictionary is used as the operand to a
        string formatting operation which yields the returned string.
        Before formatting the dictionary, a couple of preparatory steps
        are carried out. The message attribute of the record is computed
        using LogRecord.getMessage(). If the formatting string uses the
        time (as determined by a call to usesTime(), formatTime() is
        called to format the event time. If there is exception information,
        it is formatted using formatException() and appended to the message.
        """
        record.message = record.getMessage()
        if self.usesTime():
            record.asctime = self.formatTime(record, self.datefmt)
        s = self._fmt % record.__dict__
        if record.exc_info:
            # Cache the traceback text to avoid converting it multiple times
            # (it's constant anyway)
            if not record.exc_text:
                record.exc_text = self.formatException(record.exc_info)
        if record.exc_text:
            if s[-1:] != "\n":    #############<<
                s = s + "\n"
            try:
                s = s + record.exc_text
            except UnicodeError:
                # Sometimes filenames have non-ASCII chars, which can lead
                # to errors when s is Unicode and record.exc_text is str
                # See issue 8924.
                # We also use replace for when there are multiple
                # encodings, e.g. UTF-8 for the filesystem and latin-1
                # for a script. See issue 13232.
                s = s + record.exc_text.decode(sys.getfilesystemencoding(),
                                               'replace')
        return 
ok,到達解決問題的終點,看到:
        if record.exc_text:
            if s[-1:] != "\n":
                s = s + "\n"
            try:
                s = s + record.exc_text
我發現我們在轉tornado參數的時候,exc_text是’None’,而不是None才導致這個迷之None打印。
修復,在處理tornado的傳進來參數的時候:
        if record.exc_text:
            if s[-1:] != "\n":
                s = s + "\n"
            try:
                s = s + record.exc_tex
調完感覺logging這個內置模塊還挺有意思的,一開始先是用個while循環遍歷出logger的所有handler,然後每一個handler分別去handler這個日誌(record),這個handle()的過程其實就是一個加鎖的emit()過程,這個emit()是具體的處理函數,它先用formatter弄出一個msg,然後write到具體的stream(可能是File,也可能是Console)中。

完整的日誌服務器項目請查看:https://github.com/emaste-r/tornado_sync_log_demo
如果覺得有用的話,不妨去github點個Star,O(∩_∩)O~

以上 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章