1. 程式人生 > >python日誌,支持彩色打印和文件大小切片寫入和寫入mongodb

python日誌,支持彩色打印和文件大小切片寫入和寫入mongodb

att 測試 arch ftime 使用 如果 title 方式 odi

支持不同logger name的日誌寫入不同的文件,不同logger name日誌寫入不同的mongodb 的collection。LogManager比較容易調用,因為裏面的內部方法全都使用了下劃線,使用了下劃線的就是保護和私有方法不需要外界調用,也不需要看懂他,在調用時候pycharm不會自動補全提示這些帶下劃線的無關方法了,只暴露了get_and_add_handlers和get_without_handlers兩個可能需要調用的方法,pyrcharm可以自動補全這兩個方法。

主要思路和模式是:

logger添加handler後,每次寫logger.debug/info就支持日誌多種記錄方式,是因為logger和handler是發布者和訂閱者的關系,在設計模式裏面叫做觀察者模式。當進行logger.debug時候,logger會調用自己的_log方法,_log方法調用了handle方法,handle調用了call handler方法,callhandler找到所有訂閱者,調用了訂閱者(這裏的訂閱者就是各種已被addHandler添加到列表的handler對象)的handle方法,handler的handler方法會調用emit方法。所以在寫自定義handler時候只需要繼承logging.Handler類,重寫emit方法就可以了。如果有特殊的需要傳入其他參數,除了emit方法以外,還需要重寫init方法。

所以一共有兩個主要的設計模式在裏面,logger和各種handler對象之間用的模式是觀察者模式,各種花樣的handler類和基本的Handler類是使用了模板模式(模板模式是把主要的方法步驟在基類寫,具體的某些不同步驟在模板類被設置為了抽象方法,子類必須重寫。)

技術分享圖片
# coding=utf8
"""
日誌管理,支持日誌打印到控制臺或寫入文件或mongodb
使用方式為  logger = LogManager(‘logger_name‘).get_and_add_handlers(log_level_int=1, is_add_stream_handler=True, log_path=None, log_filename=None, log_file_size=10,mongo_url=None,formatter_template=2)
或者 logger = LogManager(‘logger_name‘).get_without_handlers(),此種不立即記錄日誌,之後可以在單獨統一的總閘處對所有日誌根據loggerame進行get_and_add_handlers就能捕獲到所有日誌並記錄了
"""
import os
import unittest
import time
import re
from collections import OrderedDict
import pymongo
import logging
from logging.handlers import RotatingFileHandler

if os.name == ‘posix‘:
    from cloghandler import ConcurrentRotatingFileHandler

formatter_dict = {
    1: logging.Formatter(‘日誌時間【%(asctime)s】 - 日誌名稱【%(name)s】 - 文件【%(filename)s】 - 第【%(lineno)d】行 - 日誌等級【%(levelname)s】 - 日誌信息【%(message)s】‘, "%Y-%m-%d %H:%M:%S"),
    2: logging.Formatter(‘%(asctime)s - %(name)s - %(filename)s - %(lineno)d - %(levelname)s - %(message)s‘, "%Y-%m-%d %H:%M:%S"),
}


class LogLevelException(Exception):
    def __init__(self, log_level):
        err = ‘設置的日誌級別是 {0}, 設置錯誤,請設置為1 2 3 4 5 範圍的數字‘.format(log_level)
        Exception.__init__(self, err)


class MongoHandler(logging.Handler):
    """
    一個mongodb的log handler,支持日誌按loggername創建不同的集合寫入mongodb中
    """
    msg_pattern = re.compile(‘(\d+-\d+-\d+ \d+:\d+:\d+) - (\S*?) - (\S*?) - (\d+) - (\S*?) - ([\s\S]*)‘)

    def __init__(self, mongo_url, mongo_database=‘logs‘):
        """
        :param mongo_url:  mongo連接
        :param mongo_database: 保存日誌ide數據庫,默認使用logs數據庫
        """
        logging.Handler.__init__(self)
        mongo_client = pymongo.MongoClient(mongo_url)
        self.mongo_db = mongo_client.get_database(mongo_database)

    def emit(self, record):
        try:
            """以下使用解析日誌模板的方式提取出字段"""
            # msg = self.format(record)
            # logging.LogRecord
            # msg_match = self.msg_pattern.search(msg)
            # log_info_dict = {‘time‘: msg_match.group(1),
            #                  ‘name‘: msg_match.group(2),
            #                  ‘file_name‘: msg_match.group(3),
            #                  ‘line_no‘: msg_match.group(4),
            #                  ‘log_level‘: msg_match.group(5),
            #                  ‘detail_msg‘: msg_match.group(6),
            #                  }
            level_str = None
            if record.levelno == 10:
                level_str = ‘DEBUG‘
            elif record.levelno == 20:
                level_str = ‘INFO‘
            elif record.levelno == 30:
                level_str = ‘WARNING‘
            elif record.levelno == 40:
                level_str = ‘ERROR‘
            elif record.levelno == 50:
                level_str = ‘CRITICAL‘
            log_info_dict = OrderedDict()
            log_info_dict[‘time‘] = time.strftime(‘%Y-%m-%d %H:%M:%S‘)
            log_info_dict[‘name‘] = record.name
            log_info_dict[‘file_path‘] = record.pathname
            log_info_dict[‘file_name‘] = record.filename
            log_info_dict[‘func_name‘] = record.funcName
            log_info_dict[‘line_no‘] = record.lineno
            log_info_dict[‘log_level‘] = level_str
            log_info_dict[‘detail_msg‘] = record.msg
            col = self.mongo_db.get_collection(record.name)
            col.insert_one(log_info_dict)
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)


class ColorHandler(logging.StreamHandler):
    """彩色日誌,根據不同級別的日誌顯示不同顏色"""

    def emit(self, record):
        """
        0    40    黑色
        31    41    紅色
        32    42    綠色
        33    43    黃色
        34    44    藍色
        35    45    紫紅色
        36    46    青藍色
        37    47    白色
        :param record:
        :return:
        """
        try:
            # logging.LogRecord.levelno
            msg = self.format(record)

            if record.levelno == 10:
                print(‘\033[0;32m%s\033[0m‘ % msg)  # 綠色
            elif record.levelno == 20:
                print(‘\033[0;36m%s\033[0m‘ % msg)  # 青藍色
            elif record.levelno == 30:
                print(‘\033[0;34m%s\033[0m‘ % msg)  # 藍色
            elif record.levelno == 40:
                print(‘\033[0;35m%s\033[0m‘ % msg)  # 紫紅色
            elif record.levelno == 50:
                print(‘\033[0;31m%s\033[0m‘ % msg)  # 血紅色
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)


class LogManager(object):
    """
    一個日誌類,用於創建和捕獲日誌,支持將日誌打印到控制臺打印和寫入日誌文件。
    """

    def __init__(self, logger_name=None):
        """
        :param logger_name: 日誌名稱,當為None時候打印所有日誌
        """
        self.logger = logging.getLogger(logger_name)
        self._logger_level = None
        self._is_add_stream_handler = None
        self._log_path = None
        self._log_filename = None
        self._log_file_size = None
        self._mongo_url = None
        self._formatter = None

    def get_logger_and_add_handlers(self, log_level_int=1, is_add_stream_handler=True, log_path=None, log_filename=None, log_file_size=10, mongo_url=None, formatter_template=2):
        """
       :param log_level_int: 日誌輸出級別,設置為 1 2 3 4 5,分別對應輸出DEBUG,INFO,WARNING,ERROR,CRITICAL日誌
       :param is_add_stream_handler: 是否打印日誌到控制臺
       :param log_path: 設置存放日誌的文件夾路徑
       :param log_filename: 日誌的名字,僅當log_path和log_filename都不為None時候才寫入到日誌文件。
       :param log_file_size :日誌大小,單位M,默認10M
       :param mongo_url : mongodb的連接,為None時候不添加mongohandler
       :param formatter_template :日誌模板,1為formatter_dict的詳細模板,2為簡要模板
       :type log_level_int :int
       :type is_add_stream_handler :bool
       :type log_path :str
       :type log_filename :str
       :type mongo_url :str
       :type log_file_size :int
       """
        self.__check_log_level(log_level_int)
        self._logger_level = self.__transform_logger_level(log_level_int)
        self._is_add_stream_handler = is_add_stream_handler
        self._log_path = log_path
        self._log_filename = log_filename
        self._log_file_size = log_file_size
        self._mongo_url = mongo_url
        self._formatter = formatter_dict[formatter_template]
        self.__set_logger_level()
        self.__add_handlers()
        return self.logger

    def get_logger_without_handlers(self):
        """返回一個不帶hanlers的logger"""
        return self.logger

    def __set_logger_level(self):
        self.logger.setLevel(self._logger_level)

    @staticmethod
    def __check_log_level(log_level_int):
        if log_level_int not in [1, 2, 3, 4, 5]:
            raise LogLevelException(log_level_int)

    @staticmethod
    def __transform_logger_level(log_level_int):
        logger_level = None
        if log_level_int == 1:
            logger_level = logging.DEBUG
        elif log_level_int == 2:
            logger_level = logging.INFO
        elif log_level_int == 3:
            logger_level = logging.WARNING
        elif log_level_int == 4:
            logger_level = logging.ERROR
        elif log_level_int == 5:
            logger_level = logging.CRITICAL
        return logger_level

    def __add_handlers(self):
        if self._is_add_stream_handler:
            for h in self.logger.handlers:
                if isinstance(h, logging.StreamHandler):
                    break
            else:
                self.__add_stream_handler()
        if all([self._log_path, self._log_filename]):
            for h in self.logger.handlers:
                if os.name == ‘nt‘:
                    if isinstance(h, RotatingFileHandler):
                        break
                if os.name == ‘posix‘:
                    if isinstance(h, (RotatingFileHandler, ConcurrentRotatingFileHandler)):
                        break
            else:
                self.__add_file_handler()
        if self._mongo_url:
            for h in self.logger.handlers:
                if isinstance(h, MongoHandler):
                    break
            else:
                self.__add_mongo_handler()

    def __add_mongo_handler(self):
        """寫入日誌到mongodb"""
        mongo_handler = MongoHandler(self._mongo_url, ‘logs‘)
        mongo_handler.setLevel(logging.DEBUG)
        mongo_handler.setFormatter(self._logger_level)
        self.logger.addHandler(mongo_handler)

    def __add_stream_handler(self):
        """
        日誌顯示到控制臺
        """
        # stream_handler = logging.StreamHandler()
        stream_handler = ColorHandler()  # 不使用streamhandler,使用自定義的彩色日誌
        stream_handler.setLevel(self._logger_level)
        stream_handler.setFormatter(self._formatter)
        self.logger.addHandler(stream_handler)

    def __add_file_handler(self):
        """
        日誌寫入日誌文件
        """
        if not os.path.exists(self._log_path):
            os.makedirs(self._log_path)
        log_file = os.path.join(self._log_path, self._log_filename)
        os_name = os.name
        rotate_file_handler = None
        if os_name == ‘nt‘:
            # windows下用這個,非進程安全
            rotate_file_handler = RotatingFileHandler(log_file, mode="a", maxBytes=self._log_file_size * 1024 * 1024, backupCount=10,
                                                      encoding="utf-8")
        if os_name == ‘posix‘:
            # linux下可以使用ConcurrentRotatingFileHandler,進程安全的日誌方式
            rotate_file_handler = ConcurrentRotatingFileHandler(log_file, mode="a", maxBytes=self._log_file_size * 1024 * 1024,
                                                                backupCount=10, encoding="utf-8")
        rotate_file_handler.setLevel(self._logger_level)
        rotate_file_handler.setFormatter(self._formatter)
        self.logger.addHandler(rotate_file_handler)


class _Test(unittest.TestCase):
    @unittest.skip
    def test_repeat_add_handlers_(self):
        """測試重復添加handlers"""
        LogManager(‘test‘).get_logger_and_add_handlers(1, log_path=‘../logs‘, log_filename=‘test.log‘)
        LogManager(‘test‘).get_logger_and_add_handlers(1, log_path=‘../logs‘, log_filename=‘test.log‘)
        LogManager(‘test‘).get_logger_and_add_handlers(1, log_path=‘../logs‘, log_filename=‘test.log‘)
        test_log = LogManager(‘test‘).get_logger_and_add_handlers(1, log_path=‘../logs‘, log_filename=‘test.log‘)
        print(‘下面這一句不會重復打印四次和寫入日誌四次‘)
        time.sleep(1)
        test_log.debug(‘這一句不會重復打印四次和寫入日誌四次‘)

    @unittest.skip
    def test_get_logger_without_hanlders(self):
        """測試沒有handlers的日誌"""
        log = LogManager(‘test2‘).get_logger_without_handlers()
        print(‘下面這一句不會被打印‘)
        time.sleep(1)
        log.info(‘這一句不會被打印‘)

    @unittest.skip
    def test_add_handlers(self):
        """這樣可以在具體的地方任意寫debug和info級別日誌,只需要在總閘處規定級別就能過濾,很方便"""
        LogManager(‘test3‘).get_logger_and_add_handlers(2)
        log1 = LogManager(‘test3‘).get_logger_without_handlers()
        print(‘下面這一句是info級別,可以被打印出來‘)
        time.sleep(1)
        log1.info(‘這一句是info級別,可以被打印出來‘)
        print(‘下面這一句是debug級別,不能被打印出來‘)
        time.sleep(1)
        log1.debug(‘這一句是debug級別,不能被打印出來‘)

    @unittest.skip
    def test_only_write_log_to_file(self):
        """只寫入日誌文件"""
        log5 = LogManager(‘test5‘).get_logger_and_add_handlers(is_add_stream_handler=False, log_path=‘../logs‘, log_filename=‘test5.log‘)
        print(‘下面這句話只寫入文件‘)
        log5.debug(‘這句話只寫入文件‘)

    def test_color_and_mongo_hanlder(self):
        """測試彩色日誌和日誌寫入mongodb"""
        from app import config
        logger = LogManager(‘helloMongo‘).get_logger_and_add_handlers(mongo_url=config.connect_url)
        logger.debug(‘一個debug級別的日誌‘)
        logger.info(‘一個info級別的日誌‘)
        logger.warning(‘一個warning級別的日誌‘)
        logger.error(‘一個error級別的日誌‘)
        logger.critical(‘一個critical級別的日誌‘)


if __name__ == "__main__":
    unittest.main()
技術分享圖片

test_color_and_mongo_hanlder的效果如下。
因為給logger添加了自定義的ColorHandler,所以控制臺記錄的日誌是彩色的。

技術分享圖片

mongodb裏面

技術分享圖片

使用日誌logger的name屬性作為collection的名字,不同的loggername創建不同的collection。主要保存的字段的有日誌name,是在什麽文件的什麽函數/方法的哪一行打印了什麽級別的日誌和日誌詳情。

python日誌,支持彩色打印和文件大小切片寫入和寫入mongodb