浅谈logging模块

2019-04-22  本文已影响0人  oo的布丁

最近遇到了个头疼的问题,python的logging进程安全问题,借着这个机会,彻底把logging模块研究了一遍,分享一下心得。
首先介绍一下logging的几个组件,介绍之前先把配置文件放出来,下面会详细介绍:

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "simple": {
            # 简单的输出模式
            'format': '%(asctime)s [%(name)s:%(lineno)d] [%(levelname)s]- %(message)s'
        },
        'standard': {
            # 较为复杂的输出模式,可以进行自定义
            'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(levelname)s]- %(message)s'
        },
    },

    "handlers": {
        # 输出到控制台的handler
        "debug": {
            # 定义输出流的类
            "class": "logging.StreamHandler",
            # handler等级,如果实际执行等级高于此等级,则不触发handler
            "level": "DEBUG",
            # 输出的日志格式
            "formatter": "simple",
            # 流调用系统输出
            "stream": "ext://sys.stdout"
        },
        # 写入到文件的hanler,写入等级为info,命名为request是为了专门记录一些网络请求日志
        "request": {
            # 定义写入文件的日志类,此类为按时间分割日志类,还有一些按日志大小分割日志的类等
            "class": "mlogging.TimedRotatingFileHandler_MP",
            # 日志等级
            "level": "INFO",
            # 日志写入格式,因为要写入到文件后期可能会debug用,所以用了较为详细的standard日志格式
            "formatter": "standard",
            # 要写入的文件名
            "filename":  "./request.log",
            # 分割单位,D日,H小时,M分钟,W星期,一般是以小时或天为单位
            # 比如文件名为test.log,到凌晨0点的时候会自动分离出test.log.yyyy-mm-dd
            "when": 'D',
            "encoding": "utf8"
        },
        # 写入到文件的hanler,写入等级为info
        "info": {
            # 不再赘述,详细见request logger
            "class": "mlogging.TimedRotatingFileHandler_MP",
            "level": "INFO",
            "formatter": "standard",
            "filename": "./info.log"),
            "when": 'D',
            "encoding": "utf8"
        },
        # 写入到文件的hanler,写入等级为warning
        "warning": {
            # 不再赘述,详细见request logger
            "class": "mlogging.TimedRotatingFileHandler_MP",
            "level": "WARNING",
            "formatter": "standard",
            "filename": "./warning.log"),
            "when": 'D',
            "encoding": "utf8"
        },
        # 写入到文件的hanler,写入等级为error
        "error": {
            # 不再赘述,详细见request logger
            "class": "mlogging.TimedRotatingFileHandler_MP",
            "level": "ERROR",
            "formatter": "standard",
            "filename": "./error.log"),
            "when": 'D',
            "encoding": "utf8"
        },
        # 写入到文件的hanler,写入等级为critical(一般有重大错误的时候才会用到此类打印)
        "critical": {
            # 不再赘述,详细见request logger
            "class": "mlogging.TimedRotatingFileHandler_MP",
            "level": "CRITICAL",
            "formatter": "standard",
            "filename": "./critical.log"),
            "when": 'D',
            "encoding": "utf8"
        },
    },
    "loggers": {
        # logger名字
        "debug_logger": {
            # logger集成的handler
            'handlers': ['debug'],
            # logger等级,如果实际执行等级,高于此等级,则不触发此logger,logger中所有的handler均不会被触发
            'level': "DEBUG",
            # 是否继承root日志,如果继承,root的handler会加入到当前logger的handlers中
            'propagate': False
        },
        # 不再赘述,详见debug_logger
        "request_logger": {
            'handlers': ['debug', 'request'],
            'level': "INFO",
            'propagate': False
        },
        # 不再赘述,详见debug_logger
        "info_logger": {
            'handlers': ['debug', 'info'],
            'level': "INFO",
            'propagate': False
        },
        # 不再赘述,详见debug_logger
        "warning_logger": {
            'handlers': ['debug', 'warning'],
            'level': "WARNING",
            'propagate': False
        },
        # 不再赘述,详见debug_logger
        "error_logger": {
            'handlers': ['debug', 'error'],
            'level': "ERROR",
            'propagate': False
        },
        # 不再赘述,详见debug_logger
        "critical_logger": {
            'handlers': ['debug', 'critical'],
            'level': "CRITICAL",
            'propagate': False
        },

    },
    # 基础logger,当不指定logger名称时,默认选择此logger
    "root": {
        'handlers': ['info'],
        'level': "DEBUG",
        'propagate': False
    }
}

# 用LOGGING配置logging
logging.config.dictConfig(LOGGING)
# 获取名为warning_logger的logger
logger = logging.getLogger('warning_logger')
# 打印各种日志
logger.debug('debug log')
logger.request('request log')
logger.info('info log')
logger.warning('warning log')
logger.error('error log')
logger.critical('critical log')

看完上面配置,注释多如狗,相信不用讲也能理解的差不多,不过还是分别讲讲。

logging分为3个组件,分别是:formater,handler和logger(配置文件中的version和disable_existing_loggers一般都是写死的,不用管),这三者的关系是这样的,logger负责调用handler(一个logger可以绑定多个handler),handler的打印或写入的内容则依赖于formater(一个handler只能对应一个formater),举个例子:

# 根据日志名从loggers中获取名为“warning_logger”的logger
logger = logging.getLogger('warning_logger')
# 获取的大概就是这一段,日志有2个handlers,分别是debug和warning
# "warning_logger": {
#     'handlers': ['debug', 'warning'],
#     'level': "WARNING",
#     'propagate': False
# }
# 打印warning日志,此时日志等级为warning,所有低于该等级的都会被打印,待会再讲什么是日志等级
logger.warning("warning log")
# 执行上面一段,会分别调用debug handler和warning handler,debug handler负责打印到控制台上,warning handler负责写入到./warning.log文件中。

以上就是一次简单的调用执行的过程,接下来分别细说下这几个模块。

1、formater
在平时写日志的过程中,很多固定的信息都是要记录的,比如日志时间,日志名称,日志等级,线程、进程信息等,每次都写不仅效率低,而且如果忘了写会导致日志内容的不一致,不容易解析,所以要为日志统一一下格式,正如下面配置文件中的,定义了名为simple的简单输出模式和名为standard复杂输出格式,当然还可以自定义其他格式:

"simple": {
            # 简单的输出模式
            'format': '%(asctime)s [%(name)s:%(lineno)d] [%(levelname)s]- %(message)s'
        },
        'standard': {
            # 较为复杂的输出模式,可以进行自定义
            'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(levelname)s]- %(message)s'

下面介绍下logging集成的输出格式中的一些参数,可以直接使用
asctime %(asctime)s 日志事件发生的时间,如:2003-07-08 16:49:45,896
created %(created)f 日志事件发生的时间--时间戳
msecs %(msecs)d 日志事件发生事件的毫秒部分
levelname %(levelname)s 日志等级
levelno %(levelno)s 日志级别的数字形式
name %(name)s 所使用的日志器名称,默认是'root'
message %(message)s 日志记录的文本内容
pathname %(pathname)s 调用日志记录函数的源码文件的全路径
filename %(filename)s pathname的文件名部分,包含文件后缀
module %(module)s filename的名称部分,不包含后缀
lineno %(lineno)d 调用日志记录函数的源代码所在的行号
funcName %(funcName)s 调用日志记录函数的函数名
process %(process)d 进程ID
processName %(processName)s 进程名称,Python 3.1新增
thread %(thread)d 线程ID
threadName %(thread)s 线程名称

2、handler
我目前了解到的,handler只做2件事情,第一是日志的打印,第二日志的写入和切分操作。

针对打印操作,handler可以调用logging.StreamHandler的类,stream配置为ext://sys.stdout,就可以完成打印到控制台。

针对写入操作,logging可以实现日志文件按大小或时间切割,按大小切割时,需要用到RotatingFileHandler类,并且需要制定文件切割的大小;按时间切割时则需要用到TimedRotatingFileHandler类,需要指定切割的时间点。本文以时间切割为例,需要配置when参数来指定切割时间点,具体配置如下:

“S”: Seconds
“M”: Minutes
“H”: Hours
“D”: Days
“W”: Week day (0=Monday)
“midnight”: Roll over at midnight

3、logger
其实没啥可说的,负责调用自己名下的handler。

最后,谈一下日志等级,包含:debug,info,warning,error,critical 5个等级,等级从左到右依次提升,就是说debug最低,critical最高。高等级的行为会触发低等级的logger或handler。

举个例子,等级为info的info_logger绑定了等级为debug的debug_handler和等级为warning的warning_handler,此时执行info_logger.info('info log'),此时执行等级为info,因自身等级不低于info,所以会被触发,此时依次触发名下的handler,首先是debug_handler,因为等级是deng低于info,所以debug_handler会执行,然后是warning_handler,因为等级高于info,所以不会触发。

再举个例子,比如执行info_logger.warning('info log'),此时执行等级为warning,高于info_logger的等级,所以info_logger不会被触发,尽管info_logger下有等级为warning的warning_handler,但是因为info_logger没有被触发,而其又依赖于info_logger存在,所以其也不会被触发。

本来还想写下如何处理logging的进程安全问题,不过突然觉得累了,简单说下。一个是mlogging模块,一个是ConcurrentLogHandler模块,都分别对此作了实现,或者可以改源码自己实现。大致原理都是借助fcntl模块给文件上锁解决的,亲测用python3的fcntl模块为文件加LOCK_EX锁,不仅其他python3的进程无法获取该文件的LOCK_EX锁,就连python2的进程也无法获取,猜测这个锁应该是系统级别的了。
嗯,先写这么多

更新一版最新的代码

import logging.config
import os
import sys
curr_path = os.path.abspath(os.path.dirname(__file__))
sys.path.append(curr_path)

'''
日志的配置参数,日志对象主要有3个子模块,分别为formater(输出格式),handler(日志操作类型),logger(日志名),要分别进行设置。
其中hadlers为日志的具体执行,依赖于formater和日志操作类型或一些属性,比如按大小分片还是时间分片,是写入还是打印到控制台。
logger负责调用handle,一个logger可以调用多个handler,比如logger.info调用了打印到控制台handler(logging.StreamHandler)
和写入到文件handler(mlogging.TimedRotatingFileHandler_MP),在没有指定logger名字的时候,即logger=logging.get_logger()的时候,
logger会自动选择名为root的logger
'''

# 当前兄弟目录
# log_root_path = os.path.join(str(os.path.abspath(__file__).rsplit('/', 1)[0]), 'logs')

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "simple": {
            # 简单的输出模式
            'format': '%(asctime)s|%(name)s|file:%(filename)s|lineno:%(lineno)d|logger:%(name)s|%(levelname)s - %(message)s'
        },
        'standard': {
            # 较为复杂的输出模式,可以进行自定义
            'format': '%(asctime)s threadId:%(thread)d|processId:%(process)d:|file:%(filename)s|lineno:%(lineno)d|func:%(funcName)s|module:%(module)s|logger:%(name)s|%(levelname)s - %(message)s'
        },
    },

    # 过滤器
    "filters": {
        'debug_filter': {
            '()': 'log_filters.DebugFilter'
        },
        'info_filter': {
            '()': 'log_filters.InfoFilter'
        },
        'warning_filter': {
            '()': 'log_filters.WarningFilter'
        },
        'error_filter': {
            '()': 'log_filters.ErrorFilter'
        },
        'critical_filter': {
            '()': 'log_filters.CriticalFilter'
        },
        'no_debug_filter': {
            '()': 'log_filters.NoDebugFilter'
        }
    },

    "handlers": {
        # 写入到文件的hanler,写入等级为info,命名为request是为了专门记录一些网络请求日志
        "request": {
            # 定义写入文件的日志类,此类为按时间分割日志类,还有一些按日志大小分割日志的类等
            "class": "mlogging.TimedRotatingFileHandler_MP",
            # 日志等级
            "level": "INFO",
            # 日志写入格式,因为要写入到文件后期可能会debug用,所以用了较为详细的standard日志格式
            "formatter": "standard",
            # 要写入的文件名
            "filename": os.path.join(log_root_path, 'default', 'request.log'),
            # 分割单位,D日,H小时,M分钟,W星期,一般是以小时或天为单位
            # 比如文件名为test.log,到凌晨0点的时候会自动分离出test.log.yyyy-mm-dd
            "when": 'D',
            "encoding": "utf8",
        },
        # 输出到控制台的handler
        "debug": {
            # 定义输出流的类
            "class": "logging.StreamHandler",
            # handler等级,如果实际执行等级高于此等级,则不触发handler
            "level": "DEBUG",
            # 输出的日志格式
            "formatter": "simple",
            # 流调用系统输出
            "stream": "ext://sys.stdout"
        },
        # 写入到文件的hanler,写入等级为info,命名为request是为了专门记录一些网络请求日志
        "info": {
            # 定义写入文件的日志类,此类为按时间分割日志类,还有一些按日志大小分割日志的类等
            "class": "mlogging.TimedRotatingFileHandler_MP",
            # 日志等级
            "level": "INFO",
            # 日志写入格式,因为要写入到文件后期可能会debug用,所以用了较为详细的standard日志格式
            "formatter": "standard",
            # 要写入的文件名
            "filename": os.path.join(log_root_path, 'default', 'info.log'),
            # 分割单位,D日,H小时,M分钟,W星期,一般是以小时或天为单位
            # 比如文件名为test.log,到凌晨0点的时候会自动分离出test.log.yyyy-mm-dd
            "when": 'D',
            "encoding": "utf8",
            "filters": ["info_filter"]
        },
        # 写入到文件的hanler,写入等级为warning
        "warning": {
            # 不再赘述,详细见request logger
            "class": "mlogging.TimedRotatingFileHandler_MP",
            "level": "WARNING",
            "formatter": "standard",
            "filename": os.path.join(log_root_path, 'default', 'warning.log'),
            "when": 'D',
            "encoding": "utf8",
            "filters": ["info_filter"]
        },
        # 写入到文件的hanler,写入等级为error
        "error": {
            # 不再赘述,详细见request logger
            "class": "mlogging.TimedRotatingFileHandler_MP",
            "level": "ERROR",
            "formatter": "standard",
            "filename": os.path.join(log_root_path, 'default', 'error.log'),
            "when": 'D',
            "encoding": "utf8",
            "filters": ["info_filter"]
        },
        # 写入到文件的hanler,写入等级为critical(一般有重大错误的时候才会用到此类打印)
        "critical": {
            # 不再赘述,详细见request logger
            "class": "mlogging.TimedRotatingFileHandler_MP",
            "level": "CRITICAL",
            "formatter": "standard",
            "filename": os.path.join(log_root_path, 'default', 'critical.log'),
            "when": 'D',
            "encoding": "utf8",
            "filters": ["info_filter"]
        },
    },
    "loggers": {
        # logger名字
        "my_logger": {
            # logger集成的handler
            'handlers': ['debug', 'info', 'warning', 'error', 'critical'],
            # logger等级,如果实际执行等级,高于此等级,则不触发此logger,logger中所有的handler均不会被触发
            'level': "DEBUG",
            # 是否继承root日志,如果继承,root的handler会加入到当前logger的handlers中
            'propagate': False
        },
        # logger名字
        "my_request_logger": {
            # logger集成的handler
            'handlers': ['debug', 'request', 'warning', 'error', 'critical'],
            # logger等级,如果实际执行等级,高于此等级,则不触发此logger,logger中所有的handler均不会被触发
            'level': "DEBUG",
            # 是否继承root日志,如果继承,root的handler会加入到当前logger的handlers中
            'propagate': False
        },
    },
    # 基础logger,当不指定logger名称时,默认选择此logger
    "root": {
        'handlers': ['info'],
        'level': "DEBUG",
        'propagate': False
    }
}


# 定义日志类
class Logger:

    # 单例模式存储对象
    __instance = {}
    __is_init = False

    # 对于每个app name单例模式
    def __new__(cls, app_name='default', *args, **kwargs):
        if app_name not in cls.__instance:
            cls.__instance[app_name] = super().__new__(cls)
        return cls.__instance[app_name]

    # 初始化logger,通过LOGGING配置logger
    def __init__(self, app_name='default', is_debug=True):
        if self.__is_init is True:
            return
        global log_root_path
        log_file_dir = os.path.join(log_root_path, app_name)
        print("app名为%s的日志被初始化了,日志会写入到:%s 文件夹下" % (app_name, log_file_dir))
        self.__is_init = True
        self.is_debug = is_debug

        # 初始化logging配置参数
        global LOGGING
        # 默认路径为当前文件所在目录的兄弟目录logs下的default目录 ./logs/default
        self.default_log_path = os.path.join(log_root_path, 'default')
        # # 默认路径为当前文件所在目录的兄弟目录logs下的app_name目录 ./logs/${app_name}
        self.log_root_path = os.path.join(log_root_path, app_name)
        # 不存在就创建default目录
        if not os.path.exists(self.log_root_path):
            os.makedirs(self.log_root_path)
        # 不存在就创建当前app_name对应的目录
        if not os.path.exists(self.default_log_path):
            os.makedirs(self.default_log_path)

        # 日志名和日志等级的映射
        log_levels = ['debug', 'info', 'warning', 'error',  'critical']

        # 根据app_name动态更新LOGGING配置,为每个app_name创建文件夹,配置handler
        for level in log_levels:
            # handler名称,如default_debug
            handler_name = '%s_%s' % (app_name, level)
            if level == 'debug':
                LOGGING['handlers'][handler_name] = self.get_console_handler_conf()
                # 日志名称,如default_debug_logger
            else:
                # handler 对应文件名,如 logs/default/request.log
                filename = os.path.join(self.log_root_path, (level + '.log'))
                # 日志等级转大写
                lev_up = level.upper()
                LOGGING['handlers'][handler_name] = self.get_file_handler_conf(filename=filename, level=lev_up)
                # 日志名称,如default_error_logger
        # 单独初始化request handler
        request_filename = os.path.join(self.log_root_path, 'request.log')
        request_handler_name = '%s_request' % app_name
        # 绑定request handler
        LOGGING['handlers'][request_handler_name] = self.get_file_handler_conf(filename=request_filename, level="INFO")

        # 添加app logger及app_request logger
        logger_name = '%s_logger' % app_name
        LOGGING['loggers'][logger_name] = self.get_logger_conf(app_name=app_name)
        request_logger_name = '%s_request_logger' % app_name
        LOGGING['loggers'][request_logger_name] = self.get_request_logger_conf(app_name=app_name)

        # import json
        # print(json.dumps(LOGGING, indent=2))

        # 将LOGGING中的配置信息更新到logging中
        logging.config.dictConfig(LOGGING)
        # 这里之所以要分6种logger,而不是只写一种logger,是为了将不同等级的日志信息,写入到不同的文件中,方便后期的debug和统计
        # 获取debug日志,用于只打印到控制台,不写入文件的日志,一般用于打印较长的sql,或是一大段json字符串
        self.debug_logger = logging.getLogger("%s_debug_logger" % app_name)
        # 获取request日志,一般用于记录请求日志,可以在用户开始访问和访问成功的时间点分别记录日志,可以统计访问的成功率
        self.request_logger = logging.getLogger("%s_request_logger" % app_name)
        # 获取info日志,也是最长用的一种日志,用于记录日常业务中的各种行为,是最灵活,使用最广泛的一种日志
        self.info_logger = logging.getLogger("%s_info_logger" % app_name)
        # 获取warning日志,一般用于有一些小错误或瑕疵,但不影响程序运行的行为的警告
        self.warning_logger = logging.getLogger("%s_warning_logger" % app_name)
        # 获取error日志,一般用于主动的错误提示,或者被动的捕获异常时的异常记录日志,常和try。。。except一起使用
        self.error_logger = logging.getLogger("%s_error_logger" % app_name)
        # 获取critical日志,用于重大异常,可能影响整体项目运行的异常
        self.critical_logger = logging.getLogger("%s_critical_logger" % app_name)

    # 控制台输出handler配置
    def get_console_handler_conf(self):
        console_handler_conf = {
            # 定义输出流的类
            "class": "logging.StreamHandler",
            # handler等级,如果实际执行等级高于此等级,则不触发handler
            "level": "DEBUG",
            # 输出的日志格式
            "formatter": "simple",
            # 流调用系统输出
            "stream": "ext://sys.stdout"
        }
        if self.is_debug:
            console_handler_conf['filters'] = ['debug_filter']
        else:
            console_handler_conf['filters'] = ['no_debug_filter']
        return console_handler_conf

    @staticmethod
    # 写入文件handler配置
    def get_file_handler_conf(filename: str, level='INFO'):
        file_handler_conf = {
            # 定义写入文件的日志类,此类为按时间分割日志类,还有一些按日志大小分割日志的类等
            "class": "mlogging.TimedRotatingFileHandler_MP",
            # 日志等级
            "level": "",
            # 日志写入格式,因为要写入到文件后期可能会debug用,所以用了较为详细的standard日志格式
            "formatter": "standard",
            # 要写入的文件名
            "filename": '',
            # 分割单位,D日,H小时,M分钟,W星期,一般是以小时或天为单位
            # 比如文件名为test.log,到凌晨0点的时候会自动分离出test.log.yyyy-mm-dd
            "when": 'D',
            "encoding": "utf8",
            "filters": []
        }
        filters = ['%s_filter' % (level.lower())]
        update_dict = {'filename': filename, 'level': level, 'filters': filters}
        file_handler_conf.update(update_dict)
        return file_handler_conf

    @staticmethod
    # logger 配置
    def get_logger_conf(app_name='default'):
        logger_conf = {
            'handlers': [],
            'level': "DEBUG",
            'propagate': False
        }
        # 如果只是debug级别logger,只配置打印handler,不会记录到文件中
        log_levels = ['debug', 'info', 'warning', 'error', 'critical']
        logger_conf['handlers'] = ['%s_%s' % (app_name, level) for level in log_levels]
        return logger_conf

    @staticmethod
    # request logger配置
    def get_request_logger_conf(app_name='default'):
        logger_conf = {
            'handlers': [],
            'level': "DEBUG",
            'propagate': False
        }
        log_levels = ['debug', 'request', 'warning', 'error', 'critical']
        logger_conf['handlers'] = ['%s_%s' % (app_name, level) for level in log_levels]
        return logger_conf


# 获取日常logger
def get_logger(app_name: str, is_debug=True):
    Logger(app_name, is_debug=is_debug)
    logger_name = '%s_logger' % app_name
    logger = logging.getLogger(logger_name)
    return logger


# 获取request logger
def get_request_logger(app_name: str, is_debug=True):
    Logger(app_name, is_debug=is_debug)
    logger_name = '%s_request_logger' % app_name
    logger = logging.getLogger(logger_name)
    return logger


if __name__ == '__main__':
    # 单例模式测试
    logger = get_request_logger('lr_test', is_debug=True)

    logger.debug('debug log')
    logger.debug('debug log')
    logger.debug('debug log')

filter代码:

import logging


class NoDebugFilter(logging.Filter):

    def filter(self, record):
        return False


class DebugFilter(logging.Filter):

    def filter(self, record):
        level = record.levelname.upper()
        if level == 'DEBUG':
            return True
        return False


class InfoFilter(logging.Filter):

    def filter(self, record):
        level = record.levelname.upper()
        if level in {'INFO', 'WARNING', 'ERROR'}:
            return True
        return False


class WarningFilter(logging.Filter):

    def filter(self, record):
        level = record.levelname.upper()
        if level == 'WARNING':
            return True
        return False


class ErrorFilter(logging.Filter):

    def filter(self, record):
        level = record.levelname.upper()
        if level == 'ERROR':
            return True
        return False


class CriticalFilter(logging.Filter):

    def filter(self, record):
        level = record.levelname.upper()
        if level == 'CRITICAL':
            return True
        return False
上一篇下一篇

猜你喜欢

热点阅读