python 初识 logging 模块
需求
因为最近公司有一个监听微信公众号消息的项目需要24小时运行,测试了一段时间要部署到线上了,感觉不能再无脑使用Print,打印的消息也不能24小时盯着,还是用日志保存起来规范。Python 的 logging 模块早有耳闻,而且也再很多项目中看见过,但是因为自己的项目都比较小型,平时很少用到,一直对这个模块模模糊糊的感觉,这次也抽了一天来学习,接下来就进入正题吧。
日志的好处
- 持久的记录,因为print的输出信息打印在控制台的消息有上限,而且容易被清除掉。
- Logging模块的输出语句可以通过多个处理器输出到任意位置,如即打印再控制台,又输出到日志文件,还能写入远程服务器等等,而且配置很简单。
- logging能输出很多额外的信息,时间,文件名,函数名,路径,线程id等等
下面看一下基本用法,第一个实例
import logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
logger.info('info')
logger.debug('debug')
logger.warning('warning')
logger.error('error')
先看一下输出结果:
2019-05-14 20:09:54,906 - __main__ - INFO - info
2019-05-14 20:09:54,906 - __main__ - WARNING - warning
2019-05-14 20:09:54,906 - __main__ - ERROR - error
我们逐句分析一下代码,从第一行开始 logging 是 python 内置的模块,直接导入即可,然后第二句代码是配置,通过 logging.basicConfig 配置全局的日志 level 和 format 信息。
level 顾名思义是等级的意思,下面是官方的等级信息表,每个等级都对应的一个数值,
也就是 level=logging.INFO 也可以写成 level = 20。这里我们设置成INFO,也就是只输出INFO 级别以上的信息。
等级 | 数值 |
---|---|
CRITICAL | 50 |
FATAL | 50 |
ERROR | 40 |
WARNING | 30 |
WARN | 30 |
INFO | 20 |
DEBUG | 10 |
NOTSET | 0 |
另外我们还指定了 format 格式的字符串,包括 asctime、name、levelname、message 四个内容,分别代表运行时间、模块名称、日志级别、日志内容,输出语句,即日志的输出语句就是这四者的组合。还有很多 LogRecord attributes 也就是日志记录的属性,这里我从官方的表中选了一些比较有用的日志记录属性贴出来。
属性名称 | 格式 | 描述 |
---|---|---|
asctime | %(asctime)s | LogRecord 创建时的人类可读时间 。默认情况下,格式为’2003-07-08 16:49:45,896’(逗号后面的数字是毫秒部分时间)。 |
created | %(created)f | LogRecord 创建时间(返回时间time.time() )。 |
exc_info | You shouldn’t need to format this yourself. | 异常元组(sys.exc_info )如果没有发生异常,则为None 。 |
filename | %(filename)s | 调用日记的文件名pathname 。 |
funcName | %(funcName)s | 包含日志记录调用的函数的名称。 |
levelname | %(levelname)s | 文本日志记录级别消息('DEBUG' ,'INFO' ,'WARNING' ,'ERROR' ,'CRITICAL' )。 |
LINENO | %(lineno)d | 发出日志记录调用的源行号(如果代码没出错的话)。 |
message | %(message)s | 记录的消息,可以使用 msg %args 的形式传入消息 |
module | %(module)s | 模块(名称部分filename )。 |
msg | %(msecs)d | LogRecord 创建时的毫秒部分时间 。 |
name | %(name)s | 记录器的名称。 |
pathname | %(pathname)s | 发出日志记录调用的源文件的完整路径名(如果可用)。 |
process | %(process)d | 进程ID(如果可用)。 |
processName | %(processName)s | 进程名称(如果可用)。 |
thread | %(thread)d | 线程ID(如果可用)。 |
threadName | %(threadName)s | 线程名称(如果可用)。 |
第三行代码声明了一个 logger 对象,它是日志输出的主类。官方声明:
Logger永远不会直接实例化,而是始终通过模块级函数实例化
logging.getLogger(name)
。getLogger()
具有相同名称的多次调用将始终返回对同一Logger对象的引用。
在初始化的时候我们传入了模块的名称,这里直接使用__name__ 来代替了,就是模块的名称,如果直接运行这个脚本的话就是__main__,如果是 import 的模块的话就是被引入模块的名称, 这个变量在不同的模块中的名字是不同的,所以一般使用__name__来表示就好了。例如我们把上面的脚本直接在别的文件导入,然后运行,结果如下:
import log_test1
2019-05-14 20:56:23,089 - log_test1 - INFO - info
2019-05-14 20:56:23,089 - log_test1 - WARNING - warning
2019-05-14 20:56:23,089 - log_test1 - ERROR - error
我们再回顾一下刚才再主文件的输出
2019-05-14 20:09:54,906 - __main__ - INFO - info
2019-05-14 20:09:54,906 - __main__ - WARNING - warning
2019-05-14 20:09:54,906 - __main__ - ERROR - error
第 4-7 行代码我们分别调用 logger 对象的 debug(), info(),warning(), error()等方法输出日志信息。可以看到只输出了3条日志信息,每条日志对应了指定的格式化内容,另外 DEBUG 的信息没有输出,因为我们在 logging.basicConfig 中的 level 设置为了 INFO ,所以只有大于 INFO 级别的日志会输出, 从日记表中可以看出 DEBUG 级别低于 INFO ,所以就过滤掉了。
我们刚才只使用到了 logging.basicConfig 的2个参数,现在全面得了解一些全局配置有那些参数,这里引用一下官方的文档:
Format | Description |
---|---|
filename | 即日志输出的文件名,如果指定了这个信息之后,实际上会启用 FileHandler,而不再是 StreamHandler,这样日志信息便会输出到文件中了。 |
filemode | 这个是指定日志文件的写入方式,有两种形式,一种是 w,一种是 a,分别代表清除后写入和追加写入,默认为 a 。 |
format | 使用指定的格式字符串作为处理程序。 |
datefmt | 使用指定的日期/时间格式,如time.strftime() 所示 |
style | 如果 format 参数指定了,这个参数就可以指定格式化时的占位符风格,如 %、{、$ 等。 |
level | 指定日志输出的类别,程序会输出大于等于此级别的信息。 |
stream | 在没有指定 filename 的时候会默认使用 StreamHandler,这时 stream 可以指定初始化的文件流。 |
handlers | 可以指定日志处理时所使用的 Handlers,必须是可迭代的。 |
我们再用一个实例来展示这些参数
import logging
logging.basicConfig(level=10,
filename='Filehandler_test.log',
format='%(asctime)s - %(filename)s - '
'%(name)s - %(levelname)s - '
'%(lineno)d - %(module)s - '
'%(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
filemode='a')
logger = logging.getLogger(__name__)
logger.info('info message')
logger.debug('debug message')
logger.warning('Warning message')
logger.error('error message')
这里 basicConfig 指定了输出文件的名称为 Filehandler_test.log,所有参数分别是日志的记录等级 level ,输出 format 格式化输出字段 format , 时间的输出格式 datefmt ,变成了 %Y-%m-%d,还有写入日志的方法 filemode 是 ‘a’ 即追加。
另外输出的 format 格式增加了里面分别是:字符串格式的时间 %(asctime)s,文件名 %(filename)s,记录器的名称 %(name)s, 等级 %(levelname)s, 代码中写入日志的行数 %(lineno)d, 模块名(即去掉.py的文件名) %(module)s,输出消息 %(message)s。
运行一下代码,可以看到代码目录下创建了一个 Filehandler_test.log 文件,其中内容如下:
2019-05-16 15:20:43 - log_test1.py - __main__ - INFO - 12 - log_test1 - info message
2019-05-16 15:20:43 - log_test1.py - __main__ - DEBUG - 13 - log_test1 - debug message
2019-05-16 15:20:43 - log_test1.py - __main__ - WARNING - 14 - log_test1 - Warning message
2019-05-16 15:20:43 - log_test1.py - __main__ - ERROR - 15 - log_test1 - error message
从左到右对应 format 中的参数。
Handler
接下来介绍一个我们上面提到一个名称 Handler ,百度翻译是“处理程序”的意思,以下是官方文档给出的 handler 子类
-
StreamHandler
实例将消息发送到流(类文件对象), 一般用来输出到控制台。 -
FileHandler
实例将消息发送到磁盘文件。 -
BaseRotatingHandler
是在某个点旋转日志文件的处理程序的基类,不建议直接实例化。建议使用RotatingFileHandler
或TimedRotatingFileHandler
。 -
RotatingFileHandler
实例将消息发送到磁盘文件,支持最大日志文件大小和日志文件轮换。 -
TimedRotatingFileHandler
实例将消息发送到磁盘文件,以特定的时间间隔旋转日志文件。 -
SocketHandler
实例将消息发送到TCP / IP套接字。从3.4开始,也支持Unix域套接字。 -
DatagramHandler
实例将消息发送到UDP套接字。从3.4开始,也支持Unix域套接字。 -
SMTPHandler
实例将消息发送到指定的电子邮件地址。 -
SysLogHandler
实例将消息发送到Unix syslog守护程序,可能在远程计算机上。 -
NTEventLogHandler
实例将消息发送到Windows NT / 2000 / XP事件日志。 -
MemoryHandler
实例将消息发送到内存中的缓冲区,只要满足特定条件,就会刷新。 -
HTTPHandler
实例使用任一语义GET
或POST
语义将消息发送到HTTP服务器。 -
WatchedFileHandler
实例会监视他们要登录的文件。如果文件发生更改,则会关闭该文件并使用文件名重新打开。此处理程序仅在类Unix系统上有用; Windows不支持使用的基础机制。 -
QueueHandler
实例将消息发送到队列,例如在queue
或multiprocessing
模块中实现的那些队列。 -
NullHandler
实例对错误消息不做任何事情。它们由想要使用日志记录的库开发人员使用,但是想要避免“如果库用户没有配置日志记录,则可以显示”无法找到记录器XXX的消息处理器“消息。有关更多信息,请参阅配置库的日志记录。下面我们使用二个 Handler 来实现日志同时输出到控制台、文件:
import logging
import sys
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)
# StreamHandler
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(level=logging.DEBUG)
logger.addHandler(stream_handler)
# FileHandler
file_handler = logging.FileHandler('Filehandler_test2.log')
file_handler.setLevel(level=logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Log
logger.info('info message')
logger.debug('debug message')
logger.warning('Warning message')
logger.error('error message')
控制台输出如下:
info message
debug message
Warning message
error message
文件输出如下:
2019-05-16 15:55:42,216 - __main__ - INFO - info message
2019-05-16 15:55:42,216 - __main__ - WARNING - Warning message
2019-05-16 15:55:42,216 - __main__ - ERROR - error message
控制台输出简单的原因是,我们没对控制台的 StreamHandler 配置 Formatter ,所以控制台只输出了日记的内容,没有包含其他格式化信息。而 FileHandler 设着了 Formatter 后,就又了输出格式后的信息。
另外每个 Handler 还可以设置 level 信息,最终输出结果的 level 信息会取 Logger 对象的 level 和 Handler 对象的 level 的交集。
Formatter
在进行日志格式化输出的时候,我们可以不借助于 basicConfig 来全局配置格式化输出内容,使用 Formatter 来对每个 handler 作不同的处理,下面我们再来单独看下 Formatter 的用法:
import logging
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.WARN)
formatter = logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y/%m/%d %H:%M:%S')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.debug('Debugging')
logger.critical('Critical Something')
logger.error('Error Occurred')
logger.warning('Warning exists')
logger.info('Finished')
在第四行我们指定了一个 Formatter,并传入了 fmt 和 datefmt 参数,这样就指定了日志结果的输出格式和时间格式,然后 handler 通过 setFormatter() 方法设置此 Formatter 对象即可,输出结果如下:
2019-05-16 16:11:18 - __main__ - WARNING - Warning message
2019-05-16 16:11:18 - __main__ - ERROR - error message
这样我们就可以每个 Handler 单独配置输出的格式,非常灵活。
捕获 Traceback
logging 有2种方法可以输出详细的 Traceback 错误信息,下面看一个实例:
import logging
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)
# Formatter
formatter = logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
# StreamHandler
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
# log
logger.debug('debug message')
logger.info('info message')
try:
result = 1 / 0
except ZeroDivisionError:
logger.error('Faild result', exc_info=True)
logger.exception('Faild result')
logger.info('finish')
第一种方法:我们在 error() 方法中添加了一个参数,将 exc_info 设置为了 True,这样我们就可以输出执行过程中的信息了,即完整的 Traceback 信息。
第二种方法:使用 logger.exception() 其实它的源码就是调用了 logger.error() 方法。
上面的代码运行结果如下:
2019-05-16 16:23:55 - __main__ - INFO - info message
2019-05-16 16:23:55 - __main__ - ERROR - Faild result
Traceback (most recent call last):
File "D:/cjx/python-work/spider_test/logging_test/log_test2.py", line 42, in <module>
result = 1 / 0
ZeroDivisionError: division by zero
2019-05-16 16:23:55 - __main__ - ERROR - Faild result
Traceback (most recent call last):
File "D:/cjx/python-work/spider_test/logging_test/log_test2.py", line 42, in <module>
result = 1 / 0
ZeroDivisionError: division by zero
2019-05-16 16:23:55 - __main__ - INFO - finish
中间有2个 ERROR 级别的日志完整德记录下了错误,方便我们排查。
使用文件配置
官方文档中提到:在Python 3.2中,引入了一种新的配置日志记录的方法,使用字典来保存配置信息,是新应用程序和部署的推荐配置方法。因为Python字典用于保存配置信息,并且由于您可以使用不同的方式填充该字典,因此您有更多的配置选项。例如,您可以使用 JSON 格式的配置文件,或者,如果您有权访问 YAML 处理功能,则可以使用 YAML 格式的文件来填充配置字典。或者,当然,您可以在Python代码中构造字典,通过套接字以pickle形式接收它,或者使用对您的应用程序有意义的任何方法。
将配置写在配置文件里面,我们可以将配置写入到配置文件,然后运行时读取配置文件里面的配置,这样是更方便管理和维护的,下面我们以一个实例来说明一下,首先我们定义一个 yaml 配置文件:
version: 1.0
formatters:
brief:
format: "%(asctime)s - %(levelname)s - %(message)s"
datefmt: "%Y-%m-%d %H:%M:%S"
simple:
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
datefmt: "%Y-%m-%d %H:%M:%S"
handlers:
console:
class: logging.StreamHandler
formatter: brief
level: DEBUG
stream: ext://sys.stdout
file:
class: logging.FileHandler
formatter: simple
level: INFO
filename: INFO.log
encoding: utf8
error:
class: logging.handlers.RotatingFileHandler
level: ERROR
formatter: simple
filename: ERROR.log
maxBytes: 10485760
backupCount: 20
encoding: utf8
loggers:
main.core:
level: DEBUG
handlers: [console, file, error]
propagate: False
root:
level: DEBUG
handlers: [console]
这里我们定义了 formatters、handlers、loggers、root 等模块,实际上对应的就是各个 Formatter、Handler、Logger 的配置,参数和它们的构造方法都是相同的。
注意其中 loggers 里面的 propagate 一定要设置为 False,不然会和 root 的输出重复打印。
接下来我们定义一个主入口文件,main.py,内容如下
import logging
import core
import yaml
import logging.config
import os
def setup_logging(default_path='config.yaml', default_level=logging.INFO):
path = default_path
if os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
config = yaml.full_load(f)
logging.config.dictConfig(config)
else:
logging.basicConfig(level=default_level)
def log():
logging.debug('Start')
logging.info('Exec')
logging.info('Finished')
if __name__ == '__main__':
yaml_path = 'config.yaml'
setup_logging(yaml_path)
log()
core.run()
其中读取 yaml 文件需要使用对应的库 pyyaml,我们先安装一下
pip install pyyaml
另外这个模块还引入了另外一个模块 core,所以我们定义 core.py 如下:
import logging
logger = logging.getLogger('main.core')
def run():
logger.info('Core Info')
logger.debug('Core Debug')
logger.error('Core Error')
观察配置文件,主入口文件 main.py 实际上对应的是 root 一项配置,它指定了 handlers 是 console,即只输出到控制台。另外在 loggers 一项配置里面,我们定义了 main.core 模块,handlers 是 console、file、error 三项,即输出到控制台、输出到普通文件和回滚文件。
这样运行之后,我们便可以看到所有的运行结果输出到了控制台:
2019-05-16 17:09:26 - DEBUG - Start
2019-05-16 17:09:26 - INFO - Exec
2019-05-16 17:09:26 - INFO - Finished
2019-05-16 17:09:26 - INFO - Core Info
2019-05-16 17:09:26 - DEBUG - Core Debug
2019-05-16 17:09:26 - ERROR - Core Error
在 INFO.log 文件中则包含了 core.py 的运行结果:
2019-05-16 17:12:37 - main.core - INFO - Core Info
2019-05-16 17:12:37 - main.core - ERROR - Core Error
可以看到,通过配置文件,我们可以非常灵活地定义 Handler、Formatter、Logger 等配置,同时也显得非常直观,也非常容易维护,在实际项目中,推荐使用此种方式进行配置。
参考资料
本文章引用 静觅 » Python中logging模块的基本用法 , 并在其之上修改补充了一些内容。
https://cuiqingcai.com/6080.html
https://docs.python.org/3/library/logging.html
http://www.cnblogs.com/dahu-daqing/p/7040764.html
https://docs.python.org/zh-cn/3/howto/logging-cookbook.html#logging-cookbook