• Python2/Python3自定义日志类教程


    一、说明

    1.1 背景说明

    Python的logging功能是比较丰富的支持不同层次的日志输出,但或是我们想在日志前输出时间、或是我们想要将日志输入到文件,我们还是想要自定义日志类。

    之前自己也尝试写过但感觉文档太乱看不懂怎么写,今天有人拿个半成品来问为什么代码报错,在其基础上改造了一下。

    1.2 logging级别说明

    logging日志级别及对应值如下,默认情况下直接运行只有INFO及以上级别才会输出(本质上是大于等于20才会输出),调试模式运行DEBUG日志才会输出。

    可以通过自定义输出日志级别,指定直接运行输出什么级别的日志;不过调试模式打印的日志应该是不可以修改的。

    Level

    Numeric value

    CRITICAL

    50

    ERROR

    40

    WARNING

    30

    INFO

    20

    DEBUG

    10

    NOTSET

    0

    二、实现代码

    2.1 Python2实现代码

    # -*- coding: utf-8 -*-
    import os
    import datetime
    import logging
    
    class LogConfig:
        def __init__(self,log_type="console"):
            # 指定日志输出到控制台时的初始化
            if log_type == "console":
                logging.basicConfig(level=logging.INFO,
                                    format='%(asctime)s %(levelname)s %(message)s',
                                    datefmt='%Y-%m-%d %H:%M:%S',
                                    )
            # 指定日志输出到文件的初始化
            elif log_type == "file":
                # 创建存放日志的目录
                if not os.path.exists('./log'):
                    os.mkdir('./log')
    
                # 操作系统本身不允许文件名包含:等特殊字符,所以这里也不要用,不然赋给filename时会报错
                nowTime = datetime.datetime.now().strftime('%Y-%m-%d')
                file_name = './log/%s.log' % nowTime
    
                # python2.7也有logging.basicConfig(),但只直接用logging.basicConfig(),写中文时会报错
                # 所以为风格统一,我们这里不使用logging.basicConfig(),全通过set设置
                root_logger = logging.getLogger()
                root_logger.setLevel(logging.INFO)
                handler = logging.FileHandler(filename=file_name, encoding='utf-8', mode='a')
                formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
                handler.setFormatter(formatter)
                root_logger.addHandler(handler)
    
        def getLogger(self):
            logger = logging.getLogger()
            return logger
    
    if __name__ == "__main__":
        # log_type = "console"
        log_type = "file"
        logger = LogConfig(log_type).getLogger()
        logger.debug('print by debug')
        logger.info('print by info')
        logger.warning('print by warning')

    2.2 Python3实现代码

    python3.3 之后logging.basicConfig()中提供了handlers参数,我们可借助handlers参数来指定编码。

    python3.3之前的python3版本写法得和python2一样。另外python3.9之后logging.basicConfig()会直接提供encoding参数,到时可以更方便。

    import os
    import datetime
    import logging
    
    class LogConfig:
        def __init__(self,log_type="console"):
            # 指定日志输出到控制台时的初始化
            if log_type == "console":
                logging.basicConfig(level=logging.INFO,
                                    format='%(asctime)s %(levelname)s %(message)s',
                                    datefmt='%Y-%m-%d %H:%M:%S',
                                    )
            # 指定日志输出到文件的初始化
            elif log_type == "file":
                # 创建存放日志的目录
                if not os.path.exists('./log'):
                    os.mkdir('./log')
    
                # 操作系统本身不允许文件名包含:等特殊字符,所以这里也不要用,不然赋给filename时会报错
                nowTime = datetime.datetime.now().strftime('%Y-%m-%d')
    
                file_name = './log/%s.log' % nowTime
                file_handler = logging.FileHandler(filename=file_name,encoding='utf-8', mode='a')
                # level----指定打印的日志等级;默认为WARNING;可为NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL
                # format----指定整条日志的格式;这里设置为“时间-等级-日志内容”
                # datefmt----format中时间的格式;
                # filename----日志输出到的文件;默认打印到控制台
                # filemode----日志文件读写形式;默认为“a”;配合filename使用,如果不用filename该参数也可不用
                # 本来输出到文件使用filename和filemode两个参数就可以了,不需要handlers
                # 但是logging将日志输出到文件时中文会乱码,而logging.basicConfig又没有提供指定编码的参数(python3.9之后才提供有直接的encoding参数)
                # 要指定编码只能使用handlers。另外handlers还是python3.3 之后才提供的参数,在此之前的版本请参考python2的写法
                logging.basicConfig(level=logging.INFO,
                                    format='%(asctime)s %(levelname)s %(message)s',
                                    datefmt='%Y-%m-%d %H:%M:%S',
                                    # filename=file_name,
                                    # filemode='a',
                                    handlers=[file_handler],
                                    )
    
        def getLogger(self):
            logger = logging.getLogger()
            return logger
    
    if __name__ == "__main__":
        # log_type = "console"
        log_type = "file"
        logger = LogConfig(log_type).getLogger()
        logger.debug('print by debug')
        logger.info('print by info')
        logger.warning('print by warning')

    三、日志截图

    四、更科学的日志定义方式(20200310更新)

    通过近段时间的使用发现原先的方法就自己用用没问题,但与别人产生调用及上生产时就会存在几个问题:

    第一个问题是,直接通过logging.basicConfig()进行配置,会同时影响被调用库的日志设置。

    第二个问题是,原现的文件日志形式只能输出到一个给定的文件,不能实现不同的日志类型输出到不同的日志文件。

    第三个问题是,原现的文件日志形式使用"w"模式则上次日志会被清除,使用"a"模式则日志又会无限增长需要注意清理。

    前两个问题通过getLogger时给定一个名称而不是直接获取根logger进行处理;第三个问题通过使用RotatingFileHandler等替换FileHandler进行处理。更新代码如下:

    import inspect
    import logging
    import logging.handlers
    import os
    
    
    class LogConfig:
        def __init__(self, log_level=logging.INFO):
            if __name__ != "__main__":
                self.logger = self.get_file_logger(log_level=log_level)
            pass
    
        def get_console_logger(self):
            def _gen_file_logger_handler():
                _handler = logging.StreamHandler()
                formatter = logging.Formatter(
                    "[%(asctime)s %(msecs)03d][%(process)d][tid=%(thread)d][%(name)s][%(levelname)s] %(message)s [%(filename)s"
                    " %(funcName)s %(lineno)s] ", datefmt="%Y-%m-%d %H:%M:%S")
                _handler.setLevel(logging.INFO)
                _handler.setFormatter(formatter)
                return _handler
            def _gen_console_logger():
                _console_logger = logging.getLogger("console_logger")
                _console_logger.addHandler(handler)
                return _console_logger
    
            handler = _gen_file_logger_handler()
            console_logger = _gen_console_logger()
            return console_logger
    
        # 允许同时存在不同的logger,即可以传入不同log_file_name实例化不同logger即可实现不同类型日志写入不同文件
        # logger自带了线程锁,是线程安全的,所以多线程不用自己实现日志文件锁
        def get_file_logger(self, logger_name=None, log_file_name=None, log_level=logging.INFO):
            def _get_log_file_name():
                # 如果已定义有日志文件则直接原样返回
                if log_file_name:
                    return log_file_name
                # 如果是直接运行的,那取当前文件名
                if __name__ == "__main__":
                    caller_file_name = __file__
                # 如果是被调用的,则取上层调用文件文件名
                else:
                    frame = inspect.stack()[3]
                    module = inspect.getmodule(frame[0])
                    caller_file_name = module.__file__
                inner_log_file_name = f"{os.path.basename(caller_file_name)[:-3]}.log"
                return inner_log_file_name
            def _make_sure_log_dir_exist():
                if not os.path.isdir(log_file_dir):
                    os.mkdir(log_file_dir)
            def _gen_file_logger_handler():
                # 操作系统本身不允许文件名包含:等特殊字符,所以这里也不要用,不然赋给filename时会报错
                # nowTime = datetime.datetime.now().strftime('%Y-%m-%d')
                file_path = f'{log_file_dir}/{log_file_name}'
                # formatter = logging.Formatter(
                #     "[%(asctime)s %(msecs)03d][%(process)d][tid=%(thread)d][%(name)s][%(levelname)s] %(message)s [%(filename)s"
                #     " %(funcName)s %(lineno)s] ", datefmt="%Y-%m-%d %H:%M:%S")
                formatter = logging.Formatter(
                    "[%(asctime)s %(msecs)03d][%(name)s][%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
                # filename----日志文件
                # when----更换日志文件的时间单位
                # interval----更换日志文件的时间单位个数;这里是7天换一个文件
                # backupCount----保存的旧日志文件个数;这里即只保留上一个日志文件
                # encoding----日志文件编码
                # 实际使用感觉,如果程序是crontab这样定时运行而不是一直运行着,那这个按时间滚动并不生效
                # _handler = logging.handlers.TimedRotatingFileHandler(
                #     filename=file_path,
                #     when='D',
                #     interval=7,
                #     backupCount=1,
                #     encoding='utf-8',
                # )
                # filename--日志文件
                # mode--日志文件打开模式
                # maxBytes--日志文件最大大小。每次调用打印日志时logging去检测日志大小是否达到设定的上限,如果达到则更换日志文件
                # backupCount--保存的旧日志文件个数。xxx表示当前日志文件,则xxx.1表示上一份日志文件、xxx.2表示上上份日志文件...
                # encoding----日志文件编码
                _handler = logging.handlers.RotatingFileHandler(
                    filename=file_path,
                    mode='a',
                    maxBytes=1024 * 1024 * 100,
                    backupCount=1,
                    encoding='utf-8',
                )
                # 实际发现这里setLevel并不起作用
                # _handler.setLevel(logging.DEBUG)
                _handler.setFormatter(formatter)
                return _handler
            def _gen_file_logger():
                # 如果指定了logger_name那直接采用,如果没有使用日志文件名为logger_name
                nonlocal logger_name
                if not logger_name:
                    logger_name = log_file_name
                _file_logger = logging.getLogger(logger_name)
                _file_logger.addHandler(handler)
                _file_logger.setLevel(log_level)
                return _file_logger
            log_file_name = _get_log_file_name()
            log_file_dir = "log"
            _make_sure_log_dir_exist()
            handler = _gen_file_logger_handler()
            file_logger = _gen_file_logger()
            return file_logger
    
    if __name__ == "__main__":
        # logger = LogConfig().get_console_logger()
        # log_file_name不同,返回的是不同的logger,这样就可以方便地定义多个logger
        log_file_name = "random_file_name.log"
        logger = LogConfig().get_file_logger(log_file_name=log_file_name)
        logger.debug('print by debug')
        logger.info('print by info')
        logger.warning('print by warning')
    View Code

    参考:

    https://docs.python.org/3/library/logging.html#levels

    https://www.jianshu.com/p/2ec5187a953e

    https://docs.python.org/2/library/logging.html#logging.basicConfig

    https://docs.python.org/3/library/logging.html#logging.basicConfig

    https://stackoverflow.com/questions/10706547/add-encoding-parameter-to-logging-basicconfig

  • 相关阅读:
    Express请求处理-静态资源的处理
    PostMan怎样携带登录信息请求后台接口防止出现无法访问资源问题
    Express请求处理-GET和POST请求参数的获取
    Express请求处理-构建模块化路由
    Winform中将WebBrower浏览器控件由IE内核修改为Chrome的WebKit内核
    Electron项目怎样打包成桌面exe应用
    Vue项目打包成桌面程序exe除了使用electron-vue你还可以这样
    Vue项目怎样打包并部署在WindowsServer服务器通过IP访问
    Vue本地执行build之后打开dist目录下index.html正常访问
    H5背景音乐
  • 原文地址:https://www.cnblogs.com/lsdb/p/10273409.html
Copyright © 2020-2023  润新知