Fix the log and prevent the problem of repeated printing when adding handlers to the log multiple times

1. Solve the problem of repeated printing if handlers are added multiple times. Make a judgment in the __add_handlers method.

2. The instance of logger type is returned by get_logger_and_add_handlers and get_logger_without_handlers, no longer use the proxy mode to add the debug info warning error critical method in this class, and solve the problem that the lineno of the log fomatter shows the number of lines related to the method of this class, not the specific printing The number of lines of code in the log location.

# coding=utf8
"""
@author bfzs
"""
import os
import unittest
import logging
from logging.handlers import RotatingFileHandler

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

format_dict = {
    1: logging.Formatter('log time [%(asctime)s] - log name [%(name)s] - file [%(filename)s] - line [%(lineno)d] - log level [ %(levelname)s】 - log information【%(message)s】', "%Y-%m-%d %H:%M:%S"),
    2: logging.Formatter('log time [%(asctime)s] - log name [%(name)s] - file [%(filename)s] - line [%(lineno)d] - log level [ %(levelname)s】 - log information【%(message)s】', "%Y-%m-%d %H:%M:%S"),
    3: logging.Formatter('log time [%(asctime)s] - log name [%(name)s] - file [%(filename)s] - line [%(lineno)d] - log level [ %(levelname)s】 - log information【%(message)s】', "%Y-%m-%d %H:%M:%S"),
    4: logging.Formatter('log time [%(asctime)s] - log name [%(name)s] - file [%(filename)s] - line [%(lineno)d] - log level [ %(levelname)s】 - log information【%(message)s】', "%Y-%m-%d %H:%M:%S"),
    5: logging.Formatter('log time [%(asctime)s] - log name [%(name)s] - file [%(filename)s] - line [%(lineno)d] - log level [ %(levelname)s】 - log information【%(message)s】', "%Y-%m-%d %H:%M:%S"),
}


class LogLevelException(Exception):
    def __init__(self, log_level):
        err = 'The set log level is {0}, the setting is wrong, please set it to a number in the range of 1 2 3 4 5'.format(log_level)
        Exception.__init__(self, err)


class LogManager(object):
    """
    A log class for creating and capturing logs, and supports printing logs to the console and writing to log files.
    """

    def __init__(self, logger_name=None):
        """
        :param logger_name: log name, when it is None, print all logs
        """
        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._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):
        """
       :param log_level_int: Log output level, set to 1 2 3 4 5, corresponding to output DEBUG, INFO, WARNING, ERROR, CRITICAL logs
       :param is_add_stream_handler: Whether to print the log to the console
       :param log_path: Set the folder path for storing logs
       :param log_filename: The name of the log, only when log_path and log_filename are not None, it will be written to the log file.
       :param log_file_size : log size, unit M, default 10M
       :type logger_name :str
       :type log_level_int :int
       :type is_add_stream_handler :bool
       :type log_path :str
       :type log_filename :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._formatter = format_dict[log_level_int]
        self.__set_logger_level()
        self.__add_handlers()
        return self. logger

    def get_logger_without_handlers(self):
        """Returns a logger without hanlers"""
        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()

    def __add_stream_handler(self):
        """
        Logs are displayed to the console
        """
        stream_handler = logging. StreamHandler()
        stream_handler.setLevel(self._logger_level)
        stream_handler.setFormatter(self._formatter)
        self. logger. addHandler(stream_handler)

    def __add_file_handler(self):
        """
        Logs are written to the log file
        """
        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':
            # Use this under windows, non-process safe
            rotate_file_handler = RotatingFileHandler(log_file, mode="a", maxBytes=self._log_file_size * 1024 * 1024, backupCount=10,
                                                      encoding="utf-8")
        if os_name == 'posix':
            # ConcurrentRotatingFileHandler can be used under linux, a process-safe log method
            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)


import time


class Test(unittest. TestCase):
    # @unittest.skip
    def test_repeat_add_handlers_(self):
        """Test repeated adding handlers"""
        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')
        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('The following sentence will not be printed four times and written to the log four times')
        time. sleep(1)
        test_log.debug('This sentence will not be printed four times and written to the log four times')


    def test_get_logger_without_hanlders(self):
        """Test logs without handlers"""
        log = LogManager('test2').get_logger_without_handlers()
        print('The following sentence will not be printed')
        time. sleep(1)
        log.info('This sentence will not be printed')


    def test_add_handlers(self):
        """In this way, you can write debug and info level logs at any specific place, and you only need to specify the level at the main gate to filter, which is very convenient"""
        log0 = LogManager('test3').get_logger_and_add_handlers(2)
        log1 = LogManager('test3').get_logger_without_handlers()
        print('The following sentence is info level and can be printed out')
        time. sleep(1)
        log1.info('This sentence is info level, it can be printed out')
        print('The following sentence is debug level and cannot be printed')
        time. sleep(1)
        log1.debug('This sentence is debug level and cannot be printed out')

    def test_only_write_log_to_file(self):
        """Only write to log file"""
        log5 = LogManager('test5').get_logger_and_add_handlers(is_add_stream_handler=False, log_path='../logs', log_filename='test5.log')
        print('The following sentence is only written to the file')
        log5.debug('This sentence is only written to the file')


if __name__ == "__main__":
    unittest. main()

3. Add a judgment in __add_handlers. If the log already has handlers of this type, it will not be added.

No matter how many times the log handler is added, it will not be printed repeatedly. This is because the log instance is used as a global variable in the module, and the module is repeatedly imported in multiple places in the project, resulting in repeated printing of the log, which can be solved very well.