logging模块


文章目录

  • logging模块
  • 日志级别
  • 格式字符串
  • 举例
  • 默认级别
  • 消息构建
  • 修改日期格式
  • 输出到文件
  • Logger类
  • 构造
  • 层次结构
  • Level级别设置
  • Handler
  • 日志流
  • level的继承
  • 继承关系及信息传递
  • Formatter
  • Filter


日志级别

日志级别Level

数值

CRITICAL(危险)

50

ERROR(错误)

40

WARNING(警告)

30

INFO(信息)

20

DEBUG(调试)

10

NOTSET(未设置)

0

日志级别指的是产生日志的事件的严重程度。
设置一个级别后,严重程度低于设置值的日志消息将被忽略。

debug(), info(), warning(), error(), critical()方法。

格式字符串

属性名

格式

描述

日志消息内容

%(message)s

The logged message,computed as msg % args. 当调用Formatter.format()时设置

asctime

%(asctime)s

创建LogRecord时的可读时间。默认情况下,他的格式为’2003-07-08 16:49:45,896’(逗号后面的数字时毫秒部分的时间)

函数名

%(funcName)s

日志调用所在的函数名

日志级别名称

%(levelname)s

消息的级别名称CRITICAL,ERROR,WARNING,INFO,DEBUG

日志级别数值

%(levelno)s

消息的级别数字,对应CRITICAL,ERROR,WARNING,INFO,DEBUG

行号

%(lineno)s

日志调用所在的源码行号

模块

%(module)s

模块(filename的名字部分)

进程ID

%(process)d

进程ID

线程ID

%(thread)d

线程ID

进程名

%(processName)s

进程名

线程名

%(threadName)s

线程名

logger名

%(name)s

logger名

举例

默认级别

import logging

FORMAT = '%(asctime)-15s\tHread info: %(thread)d %(threadName)s %(message)s'
logging.basicConfig(format=FORMAT)

logging.error('error')#高于默认级别
logging.warning('warning')#默认级别
logging.info('info')#级别低于默认wraning,不显示
#标准错误输出stderr,如果用ipython时可能会出现上下文干涩,导致显示出错
#2019-11-05 20:21:13,496	Hread info: 94412 MainThread error
#2019-11-05 20:21:13,496	Hread info: 94412 MainThread warning

消息构建

import logging

FORMAT = '%(asctime)s\tHread info: %(thread)d %(threadName)s %(message)s'
logging.basicConfig(format=FORMAT,level=logging.INFO)

logging.info('i am {}'.format(20))#单一字符串
logging.warning('i am %d %s',20,'years old')#c风格
#屏幕输出
#2019-11-05 20:34:13,434	Hread info: 114140 MainThread i am 20
#2019-11-05 20:34:13,434	Hread info: 114140 MainThread i am 20 years old

上例时基本的使用方法,大多数时候,使用的是info,正常运行信息的输出

日志级别和格式字符串扩展的例子

FORMAT = 'Thread info: %(thread)d %(threadName)s %(message)s %(school)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

d = {'school':'magedu.com'}
logging.info('i am {}'.format(20),extra=d)#extra额外的
logging.warning('iam %s %s',20,'years old',extra=d)
#屏幕输出
#Thread info: 114216 MainThread i am 20 magedu.com
#Thread info: 114216 MainThread iam 20 years old magedu.com

修改日期格式

import logging
logging.basicConfig(format='%(asctime)s %(message)s',datefmt='%Y/%m/%d %I:%M:%S')
logging.warnig('this event was logged')
#屏幕输出
#2019/11/05 09:06:47 this event was logged

输出到文件

import logging
logging.basicConfig(format='%(asctime)s %%(message)s',filename='d:/test.log')
for _ in range(5):
	logging.warning('this event was logged')

filename设置日志文件;filemode设置读写模式

Logger类

在logging模块中,顶层代码中有

root = RootLogging(WARNING)#大约在1731行,指定根logger对象的默认级别,就在basicConfig函数上面
logger.root = root#为类Logger增加类属性root

logging模块加载的时候,会创建一个全局的对象root,它是一个RootLogger实例,及rootlogger。根logger对象的默认级别是WARNING.

RootLogger类继承自Logger类
类Logger初始化方法签名是(name,level=0)
类RootLogger初始化方法签名(level),本质上调用的是Logger.__init__(self,'root',WARNING)

调用logging.basicConfig来重新调整级别,就是对这个根Logger的级别进行修改

构造

Logger实例的构建,使用Logger类也行,但推荐getLogger函数(工厂方法)

Logger.manager = Manager(Logger.root)#大约在源码1925行,为Logger类注入一个manager类属性

def getLogger(name=None):
    """
    Return a logger with the specified name, creating it if necessary.
	If no name is specified, return the root logger.
    """
    if name:
        return Logger.manager.getLogger(name)
    else:
        return root#没有指定名称,返回根logger

使用工厂方法返回一个Logger实例。
指定name,返回一个名称为name的Logger实例。如果再次使用相同的名字,返回同一个实例。背后使用了一个字典保证同一个名称返回同一个Logger实例。
未指定name,返回根Logger实例。

a = logging.Logger('hello', 20)
b = logging.Logger('hello', 20)
print(a, id(a))
print(b, id(b))
c = logging.getLogger('hello')
d = logging.getLogger('hello')
print(c,id(c))
print(d,id(d))
#屏幕输出
#<Logger hello (INFO)> 1666498117512
#<Logger hello (INFO)> 1666498609864
#<Logger hello (WARNING)> 1666498622920
#<Logger hello (WARNING)> 1666498622920

层次结构

Logger是层次结构的,使用 . 点号分割,如’a’, ‘a.b’, 或是’a.b.c.b’,a是a.b的父parent,a.b是a的子child。对于foo来说,名字foo.bar,foo.bar.baz,foo.bam都是foo的后代。

#父子 层级关系
#根logger

root = logging.root#Logger.root = RootLogger(WARNING)在源码1819行
print(root,id(root))
root = logging.getLogger()
print(root,id(root))
print(root.name,type(root),root.parent)#根logger没有父
print('--' * 30)

parent = logging.getLogger(__name__)#模块级logger
print(parent.name,type(parent),id(parent.parent),id(parent))

child = logging.getLogger('{}{}'.format(__name__,'child'))#子logger
print(child.name,type(child),id(child.parent),id(child))
#屏幕输出
#<RootLogger root (WARNING)> 2278161444040
#<RootLogger root (WARNING)> 2278161444040
#root <class 'logging.RootLogger'> None
#------------------------------------------------------------
#__main__ <class 'logging.Logger'> 2278161444040 2278159217992
#__main__child <class 'logging.Logger'> 2278161444040 2278159218056

Level级别设置

FORMAT = 'Thread info: %(thread)d %(threadName)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

logger = logging.getLogger(__name__)
print(logger.name,type(logger),logger.level)#新的logger实例的level是什么?
logger.info('1 info')
print(logger.getEffectiveLevel())#等效等级,从哪里来?

logger.setLevel(25)#设置logger的level
print('level ------ 25')
print(logger.getEffectiveLevel(),logger.level)
logger.info('2 info')

logger.setLevel(42)
print('level ------ 42')
logger.warning('3warning')
logger.error('4 error')
logger.critical('5 citical')

root = logging.getLogger()#根logger
root.info('6 root info')#不影响
#屏幕输出
#__main__ <class 'logging.Logger'> 0
#20
#level ------ 25
#25 25
#level ------ 42
#Thread info: 102560 MainThread 1 info
#Thread info: 102560 MainThread 5 citical
#Thread info: 102560 MainThread 6 root info

每一个logger实例都有级别,也有等效级别。logger类进行出厂配置时如果没有给定等级,则等级默认为NOTSET,下例等效等级源码,如果当前logger等级等效为False,则找其父的等级,直至到root,如果root的等级也等效为False,最后返回NOTSET。logger对象可以在创建后动态的修改自己的level。等效level决定着logger实例能输出什么级别地1信息。

注:推荐使用setLevel方法修改级别

def getEffectiveLevel(self):
    """
    Get the effective level for this logger.

    Loop through this logger and its parents in the logger hierarchy,
    looking for a non-zero logging level. Return the first one found.
    """
    logger = self
    while logger:
        if logger.level:
            return logger.level
        logger = logger.parent
    return NOTSET

Handler

Handler控制日志信息的输出目的地,可以是控制台,文件。

  • 可以单独设置level
  • 可以单独设置格式
  • 可以设置过滤器

Handler类层次

  • Handler
  • StreamHandler#不指定使用sys.stderr
  • FileHandler#文件
  • _StderrHandler#标准输出
  • NullHandler#什么都不做

日志输出其实时Handler做的,也就是真正干活的时Handler。

在logging.basicConfig函数中,如下:

if handlers is None:
	filename = kwargs.pop("filename", None)
	mode = kwargs.pop("filemode", 'a')
	if filename:
		h = FileHandler(filename, mode)
	else:
		stream = kwargs.pop("stream", None)
		h = StreamHandler(stream)
	handlers = [h]

如果设置文件名,则为根Logger加一个输出到文件的FileHandler;如果没有设置文件名,则为根Logger加一个StreamHandler,默认输出到stderr。
也就是说,根Looger至少会有一个handler的。

给logger添加一个handler

import logging

FORMAT = 'Thread info: %(thread)d %(threadName)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

logger = logging.getLogger('test')
print(logger.name,type(logger))

logger.info('line 1')#当basicConfig里面未设置输出模式时,默认使用stderr,

handler = logging.FileHandler('test.log','w')#创建Filehandler后
logger.addHandler(handler)#给logger对象绑定一个Filehandler,

logger.info('line 2')#该logger同时有了stream,和File两种handler,所以在文件和控制台都出现了line 2

那么创建handler的初始等级是什么?源码中,如Filehandler,stderrhandler,继承自streamhandler,streamhandler继承自handler,其中只有stderr和祖先handler提到了level,但都是默认未NOTSET,所以初始等级为0

日志流

level的继承
FORMAT = 'Thread info: %(thread)d %(threadName)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

log1 = logging.getLogger('s')
print(log1.level,log1.getEffectiveLevel())
#控制台输出
#0 20

logger实例

  • 如果不设置level,则初始化level为0
  • 如果设置了level,就优先使用自己的level,否则继承最近的主线level
  • 信息是否能狗从该logger上例上输出,就是要看当前函数的level是否大于等于logger的有效level,否则输出不了
继承关系及信息传递
  • 每一个Logger实例的level如同阀门,如果这么阀门太高,水就进不来。
  • 如果level没有设置,就用父logger的,如果父logger的level没有设置,继续找父的父,最终可以找到root上,如果root数字了就用它的,如果没设置,root的默认值为WARNING
  • 消息传递流程
  • 如果消息在某一个logger对象上产生,这个logger就是当前logger,首先消息level要和当前logger的Effectivelevel比较,如果低于当前logger的Effectivelevel,则流程结束,否则生成log记录
  • 日志记录会交给当前logger的所有handler处理,记录还要和每一个handler的级别分别比较,低的不处理,否则按照handler输出日志记录
  • 当前logger的所有handler处理完后,就要看自己的propagate属性,如果时true表示向父logger传递这个日志记录,否则流程结束
  • 如果日志记录传到了父logger,不需要和父logger的level比较,而是直接交给父的所有handler,父logger成为当前logger。重复2,3步骤,直到当前logger的父logger时None退出,也就时说当前logger最后一般时root logger(是否能到root logger要看中间的logger是否允许propagate)
  • logger实例初始化的propagate属性为True,既允许向父logger传递消息
  • logging.basicConfig函数
  • 如果root没有handler,就默认创建一个StreamHandler,如果设置了filename,就创建一个FileHandler。如果设置了format参数,就会用它生成一个Formatter对象,否则会生成缺省Formatter,并把这个formatter加入到刚创建的handler上,然后把这些handler加入到root.handlers列表上。level是设置给root logger的。
    如果root.handlers列表不为空, logger.basicConfig的调用什么都不做

官方日志流转图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WfS32iK6-1573036627581)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1573017337503.png)]

参考logging.Logger类的callHandlers方法

实例

import logging
logging.basicConfig(level=20,format='%(asctime)s %(name)s %(message)s')

root =  logging.getLogger()
root.setLevel(30)
print(root.level,root.handlers)

import sys
h0 = logging.StreamHandler(sys.stdout)
h0.setLevel(20)
root.addHandler(h0)
print(root.name,root.handlers)
#控制台输出
#30 [<StreamHandler <stderr> (NOTSET)>]
#root [<StreamHandler <stderr> (NOTSET)>, <StreamHandler <stdout> (INFO)>]

Formatter

logging的Formatter类它允许指定某个格式的字符串,如果提供None,那么%(message)s将会作为默认值。

import logging
logging.basicConfig(level=20,format='%(asctime)s %(name)s %(message)s')

#根logger的操作
root =  logging.getLogger()
root.setLevel(30)
#增加handler
import sys
h0 = logging.StreamHandler(sys.stdout)
h0.setLevel(20)
root.addHandler(h0)

for h in root.handlers:
    print('root handler = {},formatter = {}'.format(h,h.formatter._fmt if h.formatter else None))

#为h0增加一个Formatter
fmt0 = logging.Formatter('*********%(message)s********')
h0.setFormatter(fmt0)

logging.error('root test--------')#不能低于logger的有效级别
#控制台输出
#root [<StreamHandler <stderr> (NOTSET)>, <StreamHandler <stdout> (INFO)>]
#root handler = <StreamHandler <stderr> (NOTSET)>,formatter = %(asctime)s %(name)s %(message)s
#root handler = <StreamHandler <stdout> (INFO)>,formatter = None
#*********root test--------********

Filter

Filter分为两种:

  • 可以为handler增加过滤器,所以这种过滤器只影响某个handler,不会影响整个处理流程
  • 如果过滤器增加到logger上,就会影响流程

为logger增加Filter

import logging
logging.basicConfig(level=30,format='%(message)s')

log1 = logging.getLogger('ss')
log1.setLevel(30)
h1 = logging.StreamHandler()#没给值,默认使用stderr
h1.setLevel(10)
f1 = logging.Formatter('log1-h1 %(message)s')#生成一个formatter
h1.setFormatter(f1)#为h1的formater变更为f1

#filter = logging.Filter('s')#生成过滤器
filter = logging.Filter('k')#生成过滤器
h1.addFilter(filter)#h1上增加了该过滤器
print(filter.name,filter.nlen)
log1.addHandler(h1)

log1.warning('1*********test')
log1.addFilter(filter)
log1.warning('2*********test')
#控制台输出
#k 1
#1*********test

上例中先log1的handler设置了过滤器,结果日志输出成功,为log1设置后日志却无法输出了,为什么?因为之前有讲过,logger的propagate设置为True,所以日志输入的并不是log1,而是他的父,也就是root。因为root的handler的level默认为0,且还没有过滤器,

接下来一起看看FIlter过滤logger源码

class Filter(object):
    def __init__(self, name=''):
        self.name = name#name为k
        self.nlen = len(name)#长度为1

    def filter(self, record):#该record为上例中的ss
        if self.nlen == 0:#k的长度为1,不满足条件
            return True
        elif self.name == record.name:#k和ss不相等,不满足条件
            return True
        elif record.name.find(self.name, 0, self.nlen) != 0:
        #'ss'.find('k',0,1) !=0:
        #find方法查找到返会该值的索引,否则返回-1
        #该代码相当于record.name.startswith(filter.name)
            return False
        return (record.name[self.nlen] == ".")
    	#该代码的前提是:长度不为0,名字不相同,且以相同字符串开头。
    	#假如record.name为s.b.v,self.name为s.bb,
        #return (s.b.v[len(s.bb) == '.'])很显然结果返回False

如果上面测试通过,还要求return(record.name[self.nlen]=='.'),也就是说不但以filter。name开头,而且下一个字符必须是 . 点号

那么下面的代码能输出吗?

log4 = logging.getLogger('k4')
log4.warning('s4')

d方法查找到返会该值的索引,否则返回-1
#该代码相当于record.name.startswith(filter.name)
return False
return (record.name[self.nlen] == “.”)
#该代码的前提是:长度不为0,名字不相同,且以相同字符串开头。
#假如record.name为s.b.v,self.name为s.bb,
#return (s.b.v[len(s.bb) == ‘.’])很显然结果返回False

如果上面测试通过,还要求`return(record.name[self.nlen]=='.')`,也就是说不但以filter。name开头,而且下一个字符必须是  .  点号

那么下面的代码能输出吗?

```python
log4 = logging.getLogger('k4')
log4.warning('s4')