Python logging模块使用

logging模块简介

logging模块是Python内置的标准模块,主要用于输出运行日志,可以设置输出日志的等级、日志保存路径、日志文件回滚等;相比print,具备如下优点:

1、可以通过设置不同的日志等级,在release版本中只输出重要信息,而不必显示大量的调试信息;

2、print将所有信息都输出到标准输出中,严重影响开发者从标准输出中查看其它数据;logging则可以由开发者决定将信息输出到什么地方,以及怎么输出;

logging模块使用

logging的日志等级

在Python3.7中,logging模块将日志等级设为以下几类:

  • logging.CRITICAL:特别糟糕的事情,如内存耗尽、磁盘空间为空
  • logging.ERROR:发生错误时,如IO操作失败或者连接问题
  • logging.WARNING:发生很重要的事件,但是并不是错误时,如用户登录密码错误
  • logging.INFO:处理请求或者状态变化等日常事务
  • logging.DEBUG:调试过程中使用DEBUG等级,如算法中每个循环的中间状态

这几类等级的排序:CRITICAL > ERROR > WARNING > INFO > DEBUG

重点说明:

  • 设置了日志等级之后,调用比设置等级低的日志函数,其日志内容不会输出到日志文件中。
  • 默认情况下python的logging模块会将日志打印到了标准输出中,且只显示大于等于WARNING级别的日志,这说明python将默认的日志级别设置为WARNING。

将日志输出到控制台

import logging

logging.basicConfig(level = logging.INFO,format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# 测试代码
logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")

运行后,控制台输出以下内容:

2020-02-20 11:20:21,187 - __main__ - INFO - Start print log
2020-02-20 11:20:21,187 - __main__ - WARNING - Something maybe fail.
2020-02-20 11:20:21,187 - __main__ - INFO - Finish

将日志写入指定文件

导入logging模块,创建一个FileHandler,并对输出消息的格式进行设置,将其添加到logger,然后将日志写入到指定的文件中。

import logging

logger = logging.getLogger(__name__)
logger.setLevel(level = logging.INFO)

handler = logging.FileHandler("log.txt")
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

# 测试代码
logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")

log.txt中的日志内容:

2020-02-20 11:23:39,875 - __main__ - INFO - Start print log
2020-02-20 11:23:39,875 - __main__ - WARNING - Something maybe fail.
2020-02-20 11:23:39,875 - __main__ - INFO - Finish

关于logging.Formatter的参数说明:

  • %(levelno)s:打印日志级别的数值
  • %(levelname)s:打印日志级别的名称
  • %(pathname)s:打印当前执行程序的路径,其实就是sys.argv[0]
  • %(filename)s:打印当前执行程序名
  • %(funcName)s:打印日志的当前函数
  • %(lineno)d:打印日志的当前行号
  • %(asctime)s:打印日志的时间
  • %(thread)d:打印线程ID
  • %(threadName)s:打印线程名称
  • %(process)d:打印进程ID
  • %(message)s:打印日志信息

将日志同时输出到控制台和日志文件

基于上面的代码,在logger中添加StreamHandler,则可以将日志输出到控制台上。

import logging

logger = logging.getLogger(__name__)
logger.setLevel(level = logging.INFO)

handler = logging.FileHandler("log.txt")
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

# 将日志也输出到控制台上
console = logging.StreamHandler()
console.setLevel(logging.INFO)
logger.addHandler(console)

# 测试代码
logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")

这样我们就可以在控制台上看到与文件中一样的日志内容。

从上面的代码中,可以发现,logging有一个日志处理的主对象(如:上面代码中的 logger ),其他的处理方式都是通过addHandler函数添加进去的。

logging中常用的Handler

StreamHandler

函数原型

class logging.StreamHandler(stream=None)

函数说明

日志信息会输出到指定的stream中,可以是sys.stderr,sys.stdout或者文件。

参数说明

如果stream参数为空,则默认输出到sys.stderr。

FileHandler

函数原型

class logging.FileHandler(filename, mode='a', encoding=None, delay=False)

函数说明

继承自StreamHandler,功能是将日志信息输出到磁盘文件上。

参数说明

  • mode参数默认为append;

  • 若delay参数为true时,文件直到emit方法被执行才会打开。默认情况下,日志文件可以无限增大。

WatchedFileHandler

函数原型

class logging.handlers.WatchedFileHandler(filename, mode='a', encoding=None, delay=False)

函数说明

  • 位于logging.handlers模块中。
  • 该函数用于监视文件的状态。如果文件被改变了,那么就关闭当前流,重新打开文件,创建一个新的流。
  • newsyslog或者logrotate的使用会导致文件改变。
  • 这个函数是专门为linux/unix系统设计的,因为在windows系统上,正在被打开的文件是不会被改变的。

参数说明

该函数参数和FileHandler相同。

RotatingFileHandler

函数原型

class logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)

函数说明

  • 位于logging.handlers模块。
  • 支持循环生成日志文件

参数说明

  • filename: 自定义日志文件的路径以及文件名。
  • 参数maxBytes和backupCount允许日志文件在达到maxBytes时rollover,即当文件大小达到或者超过maxBytes时,就会新建一个日志文件。上述这两个参数,其中任何一个为0时,rollover都不会发生,也就是文件没有maxBytes限制。
  • backupCount参数是指备份数目,也就是日志最多能保留多少个文件。其文件命名则会在filename后面加上.0到.n的后缀,比如filename参数填error.log,当error.log大小达到maxBytes字节后,会自动再新建一个文件,并命名叫error.log.1,当error.log.1大小达到maxBytes字节后,会再新建一个文件error.log.2,以此类推下去。

TimedRotatingFileHandler

函数原型

class logging.handlers.TimedRotatingFileHandler(filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None)

函数说明

  • 位于logging.handlers模块。
  • 支持定时生成新日志文件。

参数说明

  • filename:日志文件名的前缀prefix。

  • when:是一个字符串,用于描述定时周期的基本单位,字符串的值及意义如下:

    ‘S’:秒
    ‘M’:分钟
    ‘H’:小时
    ‘D’:天
    ‘W0’-‘W6’:工作日(0=星期一)
    ‘midnight’:凌晨

  • interval:设置间隔周期,单位由when参数指定,如:when=’D’,interval=1,表示每天产生一个日志文件。

  • backupCount:表示日志文件的保留个数,超过数量就会丢弃掉老的日志文件;不写则全保存。

  • utc:表示UTC时间,如果utc为true,则启用UTC时间,反之则使用本机时间。

除了上述这些参数之外,TimedRotatingFileHandler还有一个比较重要的成员变量 suffix

suffix是指日志文件名的后缀,suffix中通常带有格式化的时间字符串,filenamesuffix由“.”连接构成文件名。

例如:filename=”runtime”, suffix=”%Y-%m-%d.log”,那么生成的文件名就是 runtime.2020-02-20.log

按天保存单独的日志文件

通常我们在运行中大型python程序时,日志最好是使用按天保存的方式,这样查看起来会很方便,也不会因为日志内容积累太多而不好打开。

说到按周期保存文件,我们很容易会想到使用TimedRotatingFileHandler函数来实现。没错,就是它。

import os
import logging
from logging.handlers import TimedRotatingFileHandler

# 当前文件所在目录(绝对路径)
rootDir = os.path.split(os.path.realpath(__file__))[0]

logger = logging.getLogger(__name__)
logger.setLevel(level = logging.INFO)

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# 在当前文件所在目录下,新建logs目录,用来存放log文件
log_path = os.path.join(rootDir,'logs')
if not os.path.exists(log_path):
os.makedirs(log_path)
filename = os.path.join(log_path,'test')
handler = TimedRotatingFileHandler(filename, when='midnight', interval=1)
handler.setFormatter(formatter)
handler.setLevel(logging.INFO)
handler.suffix = "%Y-%m-%d_%H-%M-%S.log"
logger.addHandler(handler)

# 测试代码
logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")

运行这段代码之后,会发现在该python文件所在目录下创建了一个logs目录,并在logs目录里面创建了一个log文件。

重要说明:

大家可能会发现上述代码,在调用TimedRotatingFileHandler时,传入的when参数值是’midnight’,那为什么不传’D’呢?它不也可以表示按天为单位周期来生成日志吗?其实传’D’也是可以的,但有个条件要满足:就是该python脚本会一直运行不会停止,比如长期在后台运行。

那为什么会有这么一个奇怪的要求呢?

这个就要看看TimedRotatingFileHandler源码(源码位于:Python37\Lib\logging\handlers.py)在时间计算这块是怎么实现的了。

贴上TimedRotatingFileHandler类的 init 函数的部分代码:

self.extMatch = re.compile(self.extMatch, re.ASCII)
self.interval = self.interval * interval # multiply by units requested
if os.path.exists(filename):
t = os.stat(filename)[ST_MTIME]
else:
t = int(time.time())
self.rolloverAt = self.computeRollover(t) # 计算何时分割日志

从上面代码可以发现,TimedRotatingFileHandler是根据当前文件的修改时间或当前时间戳来判断是否需要把日志分割开来,而每次初始化TimedRotatingFileHandler这个类,时间就会重新计算一次。

所以,如果项目是持续运行的,那么这个类只会初始化一次,这时通过传入参数when=’D’ 是可以正常按天来分割日志的。而只要python脚本一重启,那这个时间就会重新算,日志分割时间也就会推迟了。

然后我们再阅读下computeRollover函数源码,发现如果传参when=’midnight’,那么它会计算当前时间到凌晨零点的秒数,然后每次写日志时都会判断有没有过凌晨,一旦过了凌晨就会自动分割日志。


到这里,关于python logging模块的内容就介绍完了。总的来说,logging模块用起来还是相当方便的,功能也比较强大,值得我们好好深入学习和使用。

参考链接: