Python Logging库HTTP

发布时间:2019-09-28 08:38:47编辑:auto阅读(1605)

    问题

    Python的logging库是标准库中用来实现日志的库,功能强大,而且使用起来也算是方便。该库提供了很多个不同的Handler,用来对日志进行不同的处理。例如FileHandler用来将日志记录到文件,RotateFileHandler用来将日志记录到文件而且支持日志文件滚动备份,还有本文中所说的HttpHandler,可以将日志通过HTTP请求发送到服务器上。

    使用Python的logging模块的过程大约有如下几个步骤:

    1. 根据配置文件、配置字典或者调用方法的方式初始化日志配置,并获取一个logger。

    2. 调用logger实例的如下方法来发出一条日志:critical, error, warning, info, debug。这些方法的定义如下,以info为例:

      logger.info(fmt, *args, exc_info, extra)
      

      P.S. 本文的目的不是说明logging如何使用,所以具体的用法请参考官方文档。

    当logger对象调用info等方法发出一条日志时,他可以接受像C语言中的printf函数或者Python3中的pritnf函数一样的前两个参数:格式化字符串和对应的参数列表,用来表示要发出的日志的内容。当logging模块真的要发出这条日志时,才会对字符串进行格式化,并且加入最终的日志字符串中。因此,在Python参考手册(第4版)中(19.7节,289页)有强调了如下这一点:发出日志消息时,应该避免在发出消息时带有字符串格式化的代码(即格式化一条消息,然后把结果传递到日志记录模块中)。原因是,直接传递格式化后的字符串会导致参数被完全求值,这个有可能是非必要的,会导致日志性能下降。举个例子:

    正确方式:
    logger.info("hello, %s", "myname")
    
    错误方式:
    logger.info("hello, %s" % "myname")
    

    那么问题来了,如果一个logger的handler使用了HttpHandler,这个坑爹货居然不会在发出日志前对日志内容部分进行格式化,而是只发送了前面的fmt字符串到http服务器,结果就像下面这样:

    WARNING Tue Jan 27 15:27:34 2015 admin.config 192.168.100.126 POST /user/login User [%s] logged in failed.
    而我们期待的应该是:

    WARNING Fri Jan 23 11:36:45 2015 admin.config 192.168.100.126 POST /user/login User [admin] logged in failed.

    解决方法

    使用logging模块提供的Filter功能。

    直接给出实例代码:

    # -*- coding: utf-8 -*-
    
    import logging
    import logging.config
    import logging.handlers
    
    
    log_config_dict = {
        "version": 1,
        "formatters": {
            "format_def": {
                "format": "%(levelname)-8s %(asctime)s %(name)s %(ip)s "
                "%(method)s %(path)s %(message)s",
            },
        },
        "handlers": {
            "handler_http": {
                "class": "logging.handlers.HTTPHandler",
                "formatter": "format_def",
                "level": "INFO",
                "host": "192.168.100.1:8888",
                "url": "/log/admin",
                "method": "POST",
            },
        },
        "loggers": {
            "admin.config": {
                "level": "INFO",
                "propagate": 0,
                "handlers": ["handler_http"],
            },
            "admin.api": {
                "level": "INFO",
                "propagate": 0,
                "handlers": ["handler_http"],
            }
        },
    }
    
    
    class RequestFilter(logging.Filter):
        """A filter used to add extra information to a record.
    
        Add ip, method and path information to a record for a HTTP request.
    
        Attributes:
            name: logger's name
        """
        def __init__(self, name):
            self.name = name
    
        def filter(self, record):
            # 这里调用getMessage()方法得到格式化后的日志内容,
            # HTTP服务器上只要读取POST中的message参数即可。
            record.message = record.getMessage()
            return True
    
    
    def init_log():
        logging.config.dictConfig(log_config_dict)
    
    
    def get_logger(name):
        if type(name) is not str:
            return None
    
        log = logging.getLogger(name)
        log.addFilter(RequestFilter(name))  # 添加一个过滤器用来进行消息格式化
        log.addHandler(logging.NullHandler())
    
        return log
    
    
    def get_config_logger():
        return get_logger("admin.config")
    
    
    def get_api_logger():
        return get_logger("admin.api")
    

    上面的中的中文注释部分直接说明了解决方案。

关键字