Skip to content

Latest commit

 

History

History
165 lines (116 loc) · 7.2 KB

logging2.md

File metadata and controls

165 lines (116 loc) · 7.2 KB

Python的日志系统(二)

在上一期内容中,我们初次接触了logging模块最简单的用法。本篇文章中我们来深入了解一下Logger对象。

日志对象

所有的日志记录都是由Logger对象开始完成的,包括我们使用logging.log等函数,也是由默认的Logger对象来处理的。从上篇内容我们已经知道,logging默认使用的Logger被称为root。实际上,我们可以创建任意名称的Logger对象,方法是利用logging.getLogger函数:

# logs.py
import logging

logger = logging.getLogger('hello')
logger.error("Error from hello logger")

# Error from hello logger

需要注意的是,全新的logger不会定义任何默认的格式,所以为了打印出logger的名称,我们还需要给它定义一个格式。这里存在一个复杂的问题,就是logger实际上不负责消息的格式化处理,所以我们无法直接为logger定义一个消息格式。真正负责处理消息格式的是Handler的对象。另外,消息格式也并非是一个普通的字符串变量,而是由一个Formatter的对象承载。这里我们先放下这个问题,先回归到logger名称的问题中。下面我们将定义一个能打印名称的handlerlogger使用:

fmt = logging.Formatter("%(name)s - %(levelname)s: %(message)s") # 这里创建一个消息格式对象
handler = logging.StreamHandler() # 这里创建一个流式handler
handler.setFormatter(fmt) # 这里给handler设置格式
logger.addHandler(handler) # 这里给logger增加handler
logger.error("Error from hello logger")

# hello - ERROR: Error from hello logger

可以看到,我们顺利打印出了logger的名称。另外,我们也真正接触到了如何从0开始自定义一个简单的logger来使用。

logging模块将Logger按照名称设置为单例模式。也就是说,一个进程中,同一个名称的Logger就是相同的Logger。我们可以在不同的文件中验证一下:

# a.py
import logs # 执行上面的程序
import logging

logger = logging.getLogger("hello")
logger.error("Error from hello logger")

# hello - ERROR: Error from hello logger

logger2 = logging.getLogger("hi")
logger2.error("Error from hi logger")

# Error from hi logger

可以看到,hello由于已经配置过了,在另一个文件内也能够按照格式打印。

过滤日志

我们通过设置消息级别,已经能够初步过滤日志,即,我们只会收到希望的日志级别之上的日志。不过,我们仍旧可以设置更细的日志过滤机制,方法是定义一个具有filter方法的类来完成过滤。该方法接收一个record参数,其中包含了本条日志的各个属性。我们可以定义我们所需的过滤方法,然后,如果需要输出这条日志,那么就返回True,否则,返回False。下面看个例子:

class KeywordFilter:
    keyword = "shit"
    def filter(self, record):
        # 消息本体内容存储于record.msg中
        if self.keyword in record.msg:
            return False
        else:
            return True

logger.addFilter(KeywordFilter())
logger.setLevel(logging.DEBUG)
logger.debug("This tastes like shit")
logger.debug("This is debug msg")

# hello - DEBUG: This is debug msg

可以看到,第一条消息被过滤掉了。

额外的信息

在上一篇文章中,我们提到了日志格式的定义方式。其中,可以增加的属性是有限的。如果我们希望在日志中插入一些额外的属性,我们可以利用上面的过滤器来实现。例如,我们定义一个新的格式,其中需要输出用户IP地址,我们可以在filter函数里将该属性值添加到日志中:

fmt = logging.Formatter("%(name)s - %(levelname)s: %(ip)s - %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(fmt)
logger.addHandler(handler)

logger.error("disconnect!")

# KeyError: 'ip'

可以看到,直接输出内容会给出KeyError的错误,这是因为在格式中我们定义了ip属性。我们利用filter来尝试提供这一属性的值:

class Filter:
    def filter(self, record):
        record.ip = "192.168.0.1"
        return True
    
logger.addFilter(Filter())
logger.error("disconnect!")
# hello - ERROR: 192.168.0.1 - disconnect!

可以看到,虽然我们成功地定义了新的属性,但是利用Filter的方式可以影响所有利用该logger所输出的日志。有没有办法对该logger的输出语句进行细粒度的属性添加呢?有,利用每一个日志输出函数的关键字参数extraextra接收一个字典类型的对象,它可以将自定义的属性替换到日志格式中:

# logger.addFilter(Filter())

extra = {"ip": "192.168.0.1"}
logger.error("disconnect!", extra=extra)
# hello - ERROR: 192.168.0.1 - disconnect!

我们还有第三种方法来为日志增加额外的信息,即利用LoggerAdapterLoggerAdapter的对象创建时,接收一个Logger对象和一个字典数据。默认地,LoggerAdapter会将字典数据作为extra填入Logger对象的方法中,正如我们上面所讲述的。在使用中,我们可以直接通过LoggerAdapter的对象来调用各种输出日志的方法,如debuginfo等等,LoggerAdapter会将输出日志工作委托到初始化时传入的Logger对象来完成:

la = logging.LoggerAdapter(logger, extra)
la.error("disconnect!")

# hello - ERROR: 192.168.0.1 - disconnect!

LoggerAdapter还具有一个process方法,允许我们更进一步去修改日志消息和额外参数。为了使用这一方法,我们需要子类化LoggerAdapter,并实现process方法:

class IPAdapter(logging.LoggerAdapter):
    def process(self, msg, kwargs):
        print(msg, kwargs)
        return msg, kwargs
    
ipa = IPAdapter(logger, extra)
ipa.error("disconnect!")

# disconnect! {}
# KeyError: 'ip'

从上例中我们可以发现,process的两个参数分别为msgkwargs。在实际调用中,msg就是日志消息,而kwargs是一个空字典,并且日志并未正确输出,反而报出KeyError的错,提示ip键不存在。extra不是已经传了吗?这是因为一旦自定义了Adapterprocess,那么Logger就会从process返回的kwargs里寻找extra字典,而忽略掉初始化过程中的extra

class IPAdapter(logging.LoggerAdapter):
    def process(self, msg, kwargs):
        kwargs['extra'] = self.extra
        return msg, kwargs
    
ipa = IPAdapter(logger, extra)
ipa.error("disconnect!")

# hello - ERROR: 192.168.0.1 - disconnect!

当然,这里我们给出其他的extra来满足不同的要求,同时,msg也可以进行相应的修改:

class IPAdapter(logging.LoggerAdapter):
    def process(self, msg, kwargs):
        kwargs['extra'] = {"ip": "***.***.***.***"}
        return msg.upper(), kwargs
    
ipa = IPAdapter(logger, extra)
ipa.error("disconnect!")

# hello - ERROR: ***.***.***.*** - DISCONNECT!