一、日志
日志是什么?这个不用多解释。百分之九十的程序都需要提供日志功能。Python内置的logging模块,为我们提供了现成的高效好用的日志解决方案。但是,不是所有的场景都需要使用logging模块,下面是Python官方推荐的使用方法:
任务场景 | 最佳工具 |
---|---|
普通情况下,在控制台显示输出 | print() |
报告正常程序操作过程中发生的事件 | logging.info()(或者更详细的logging.debug()) |
发出有关特定事件的警告 | warnings.warn()或者logging.warning() |
报告错误 | 弹出异常 |
在不引发异常的情况下报告错误 | logging.error(), logging.exception()或者logging.critical() |
logging模块定义了下表所示的日志级别,按事件严重程度由低到高排列(注意是全部大写!因为它们是常量。):
级别 | 级别数值 | 使用时机 |
---|---|---|
DEBUG | 10 | 详细信息,常用于调试。 |
INFO | 20 | 程序正常运行过程中产生的一些信息。 |
WARNING | 30 | 警告用户,虽然程序还在正常工作,但有可能发生错误。 |
ERROR | 40 | 由于更严重的问题,程序已不能执行一些功能了。 |
CRITICAL | 50 | 严重错误,程序已不能继续运行。 |
默认级别是WARNING,表示只有WARING和比WARNING更严重的事件才会被记录到日志内,低级别的信息会被忽略。因此,默认情况下,DEBUG和INFO会被忽略,WARING、ERROR和CRITICAL会被记录。
有多种方法用来处理被跟踪的事件。最简单的方法就是把它们打印到终端控制台上。或者将它们写入一个磁盘文件内。
二、简单使用
在什么都不配置和设定的情况下,logging会简单地将日志打印在显示器上,如下例所示:
1 2 3 4 5 6 7 8 9 |
#!/usr/local/bin/python # -*- coding:utf-8 -*- import logging logging.debug('debug message') logging.info('info message') logging.warn('warn message') logging.error('error message') logging.critical('critical message') |
输出:
1 2 3 |
WARNING:root:warn message ERROR:root:error message CRITICAL:root:critical message |
默认情况下,logging模块将日志打印到屏幕上(stdout),日志级别为WARNING(即只有日志级别高于WARNING的日志信息才会输出)。打印出来的内容包括日志级别、调用者和具体的日志信息。所有的这些内容都是可以自定义的,在后面我们会细说。
三、日志写入文件
要把日志输出到文件内,就不能使用上面的方法了,但是logging模块同样给我们提供了一个相对便捷的手段,那就是logging.basicConfig()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 |
#!/usr/local/bin/python # -*- coding:utf-8 -*- import logging # 通过下面的方式进行简单配置输出方式与日志级别 logging.basicConfig(filename='logger.log', level=logging.INFO) logging.debug('debug message') logging.info('info message') logging.warn('warn message') logging.error('error message') logging.critical('critical message') |
标准输出(屏幕)未显示任何信息,发现当前工作目录下生成了logger.log,内容如下:
1 2 3 4 |
INFO:root:info message WARNING:root:warn message ERROR:root:error message CRITICAL:root:critical message |
因为通过level=logging.INFO设置日志级别为INFO,所以所有的日志信息均输出出来了。
默认情况下,日志会不断的追加到文件的后面。如果你不想保存之前的日志,每次都清空文件,然后写入当前日志,则可以如下设置:
1 |
logging.basicConfig(filename='logger.log', filemode='w', level=logging.INFO) |
关键是将filemode
设置为‘w’。
四、简单定制化
- 日志传参
在logging模块中通过百分符%方式的格式化控制,生成消息字符串,类同于字符串数据类型的格式化输出,但也有不同之处。
1 2 |
import logging logging.warning('%s before you %s', 'Look', 'leap!') |
结果:
1 |
WARNING:root:Look before you leap! |
可以看到两个%s分别被‘Look’和‘leap!’替代了。
- 消息格式化
要控制消息格式,获得更多的花样,可以提供format参数:
1 2 3 4 5 |
import logging logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) logging.debug('This message should appear on the console') logging.info('So should this') logging.warning('And this, too') |
输出结果:
1 2 3 |
DEBUG:This message should appear on the console INFO:So should this WARNING:And this, too |
对于%(levelname)s
这种东西,是logging模块内置的,可以被输出到日志中的对象,更多的内容在下面将会列举。
- 附加时间信息
要在日志内容中附加时间信息,可以在format字符串中添加%(asctime)s
。
1 2 3 |
import logging logging.basicConfig(format='%(asctime)s %(message)s') logging.warning('is when this event was logged.') |
输出结果:
1 |
2017-12-12 11:41:42,612 is when this event was logged. |
默认情况下,时间的显示使用ISO8601
格式。如果想做更深入的定制,可以提供datefmt
参数,如下所示:
1 2 3 |
import logging logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') logging.warning('is when this event was logged.') |
输出结果:
1 |
12/12/2017 11:46:36 AM is when this event was logged. |
datefmt
参数的定制和time模块的time.strftime()
一样!
五、高级用法
如果只是简单地使用logging,那么使用上面介绍的方法就可以了,如果要深度定制logging,那么就需要对它有更深入的了解。下面的内容才是基本的logging模块的使用方法。logging模块采用了模块化设计,主要包含四种组件:
Logger
:记录器,暴露了应用程序代码能直接使用的接口。
Handler
:处理器,将(记录器产生的)日志记录发送至合适的目的地。
Filter
:过滤器,提供了更好的粒度控制,它可以决定输出哪些日志记录。
Formatter
:格式化器,指明了最终输出中日志记录的布局。
- Logger记录器
logging模块的日志功能是基于Logger类实现的。我们可以通过下面的方法获取一个Logger类的实例(建议以模块名命名logger实例)。
1 |
logger = logging.getLogger(__name__) |
Logger是一个树形层级结构,在使用debug(),info(),warn(),error(),critical()等方法之前必须先创建一个Logger的实例,即创建一个记录器,如果没有显式的进行创建,则默认创建一个root logger,并应用默认的日志级别(WARN),默认的处理器Handler(StreamHandler,即将日志信息打印在标准输出上),和默认的格式化器Formatter,就像我们在前面举的那些例子一样。
logger对象有三重功能。首先,提供应用程序调用的接口;其次,决定日志记录的级别;最后,将日志内容传递到相关联的handlers中。
总结logger对象的用法,可以分成两类:配置和消息发送。
下面是最常用的配置方法:
Logger.setLevel()
:设置日志记录级别。
Logger.addHandler(),Logger.removeHandler()
:为logger对象添加或删除handler处理器对象。
Logger.addFilter(),Logger.removeFilter()
:为为logger对象添加或删除filter过滤器对象。
配置好logger对象后,就可以使用下面的方法创建日志消息了:
Logger.debug(),Logger.info(),Logger.warning(),Logger.error(),and Logger.critical()
:创建对应级别的日志,但不一定会被记录。
Logger.exception()
:创建一个类似Logger.error()的日志消息。不同的是Logger.exception()保存有一个追踪栈。该方法只能在异常handler中调用。
Logger.log()
::显式的创建一条日志,是前面几种方法的通用方法。
注意,getLogger()方法返回一个logger对象的引用,并以你提供的name参数命名,如果未提供名字,那么默认为‘root’。使用同样的name参数,多次调用getLogger(),将返回同样的logger对象。
- Handlers处理器
Handlers对象是日志信息的处理器、分发器。它们将日志分发到不同的目的地。比如有时候我们希望将所有的日志都记录在本地文件内,将error及其以上级别的日志发送到标准输出stdout,将critical级别的日志以邮件的方法发送给管理员。这就需要同时有三个独立的handler,分别负责一个方向的日志处理。
logging模块使用较多的handlers有两个,StreamHandler和FileHandler。
StreamHandler
标准输出stdout(如显示器)分发器。
创建方法: sh = logging.StreamHandler(stream=None)
FileHandler
将日志保存到磁盘文件的处理器。
创建方法: fh = logging.FileHandler(filename, mode=’a’, encoding=None, delay=False)
handlers对象有下面的方法:
setLevel():和logger对象的一样,设置日志记录级别。那为什么要设置两层日志级别呢?logger对象的日志级别是全局性的,对所有handler都有效,相当于默认等级。而handlers的日志级别只对自己接收到的logger传来的日志有效,进行了更深一层的过滤。
setFormatter():设置当前handler对象使用的消息格式。
addFilter(),removeFilter():配置或删除一个filter过滤对象。
logging模块内置了下面的handler处理器,从字面上你就能看出它们的大概用途:
StreamHandler:日志输出到流,可以是sys.stderr、sys.stdout或者文件。
FileHandler:日志输出到文件。
RotatingFileHandler:按文件大小进行日志文件切割处理。
TimedRotatingFileHandler:按时间进行日志文件切割处理。
SocketHandler:远程输出日志到TCP/IP sockets。
DatagramHandler:远程输出日志到UDP sockets。
SMTPHandler:远程输出日志到邮件地址。
SysLogHandler:日志输出到syslog。
NTEventLogHandler:远程输出日志到Windows NT/2000/XP的事件日志。
HTTPHandler:通过”GET”或”POST”远程输出到HTTP服务器。
- Formatters
Formatter对象用来最终设置日志信息的顺序、结构和内容。其构造方法为:
1 |
ft = logging.Formatter.__init__(fmt=None, datefmt=None, style=’%’) |
如果不指定datefmt,那么它默认是%Y-%m-%d %H:%M:%S
样式的。
style参数默认为百分符%,这表示前面的fmt参数应该是一个%(<dictionary key>)s
格式的字符串,而可以使用的logging内置的keys,如下表所示:
属性 | 格式 | 描述 |
---|---|---|
asctime | %(asctime)s | 日志产生的时间,默认格式为2003-07-08 16:49:45,896 |
created | %(created)f | time.time()生成的日志创建时间戳 |
filename | %(filename)s | 生成日志的程序名 |
funcName | %(funcName)s | 调用日志的函数名 |
levelname | %(levelname)s | 日志级别 (‘DEBUG’, ‘INFO’, ‘WARNING’, ‘ERROR’, ‘CRITICAL’) |
levelno | %(levelno)s | 日志级别对应的数值 |
lineno | %(lineno)d | 日志所针对的代码行号(如果可用的话) |
module | %(module)s | 生成日志的模块名 |
msecs | %(msecs)d | 日志生成时间的毫秒部分 |
message | %(message)s | 具体的日志信息 |
name | %(name)s | 日志调用者 |
pathname | %(pathname)s | 生成日志的文件的完整路径 |
process | %(process)d | 生成日志的进程ID(如果可用) |
processName | %(processName)s | 进程名(如果可用) |
thread | %(thread)d | 生成日志的线程ID(如果可用) |
threadName | %(threadName)s | 线程名(如果可用) |
- Filter过滤器
Handlers和Loggers可以使用Filters来完成比日志级别更复杂的过滤。比如我们定义了filter = logging.Filter('a.b.c')
,并将这个Filter添加到了一个Handler上,则使用该Handler的Logger中只有名字带a.b.c前缀的Logger才能输出其日志。
创建方法:filter = logging.Filter(name='')
例如:
1 2 |
filter = logging.Filter('mylogger.child1.child2') fh.addFilter(filter) |
六、配置日志模块
常用的有以下几种配置logging的方法:
1. 显式创建记录器Logger、处理器Handler和格式化器Formatter,然后使用Python的代码调用上面介绍过的配置函数。
2. 通过简单方式进行配置,使用basicConfig()函数直接进行配置。
3. 通过配置文件进行配置,使用fileConfig()函数读取配置文件。
4. 通过配置字典进行配置,使用dictConfig()函数读取配置信息。
5. 通过网络进行配置,使用listen()函数进行网络配置。
下面对每个方法进行介绍一下。
第一种方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# simple_logging_module.py import logging # 创建logger记录器 logger = logging.getLogger('simple_example') logger.setLevel(logging.DEBUG) # 创建一个处理器(这里使用控制台输出),并将日志级别设置为debug ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) # 创建formatter格式化器 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # 将formatter添加到ch处理器 ch.setFormatter(formatter) # 将ch添加到logger logger.addHandler(ch) # 然后就可以开始使用了! logger.debug('debug message') logger.info('info message') logger.warn('warn message') logger.error('error message') logger.critical('critical message') |
在命令行中运行上面的代码,输出结果如下:
1 2 3 4 5 6 |
$ python simple_logging_module.py 2017-12-19 15:10:26,618 - simple_example - DEBUG - debug message 2017-12-19 15:10:26,620 - simple_example - INFO - info message 2017-12-19 15:10:26,695 - simple_example - WARNING - warn message 2017-12-19 15:10:26,697 - simple_example - ERROR - error message 2017-12-19 15:10:26,773 - simple_example - CRITICAL - critical message |
上面也介绍了handle的一些方法,这里再举例一个TimedRotatingFileHandler,根据时间来切割日志文件。
1 2 3 |
ch = TimedRotatingFileHandler('app', when='m', interval=1, backupCount=2, encoding=None) ch.suffix = "%Y-%m-%d_%H-%M.log" ch.setLevel(logging.DEBUG) |
定义文件切割方式,可以按天、小时、分钟进行。然后定义切割文件的后缀,一般都是按照时间来记录。
这里要特别注意后缀格式,上面是一种固定格式,不然你就会碰到被切割文件无法自动删除的问题。主要因为删除文件在源码中是通过正则来匹配的,且匹配规则都是写死的,根据不同的切割条件来定义删除匹配规则的。下面是TimedRotatingFileHandler方法中的匹配规则,其他方法都类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# logging/handlers.py if self.when == 'S': self.interval = 1 # one second self.suffix = "%Y-%m-%d_%H-%M-%S" self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$" elif self.when == 'M': self.interval = 60 # one minute self.suffix = "%Y-%m-%d_%H-%M" self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}(\.\w+)?$" elif self.when == 'H': self.interval = 60 * 60 # one hour self.suffix = "%Y-%m-%d_%H" self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}(\.\w+)?$" elif self.when == 'D' or self.when == 'MIDNIGHT': self.interval = 60 * 60 * 24 # one day self.suffix = "%Y-%m-%d" self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$" elif self.when.startswith('W'): self.interval = 60 * 60 * 24 * 7 # one week if len(self.when) != 2: raise ValueError("You must specify a day for weekly rollover from 0 to 6 (0 is Monday): %s" % self.when) if self.when[1] < '0' or self.when[1] > '6': raise ValueError("Invalid day specified for weekly rollover: %s" % self.when) self.dayOfWeek = int(self.when[1]) self.suffix = "%Y-%m-%d" self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$" |
第二种方法
通过basicConfig方式属于一种简易方式,就像我们上面使用的一样。相关参数都定义在此函数中即可,创建方式如下:
1 |
logging.basicConfig(filename='logger.log', level=logging.INFO, format='%(asctime)s %(message)s') |
basicConfig关键字参数:
关键字 | 描述 |
---|---|
filename | 创建一个FileHandler,使用指定的文件名,而不是使用StreamHandler。 |
filemode | 如果指明了文件名,指明打开文件的模式(如果没有指明filemode,默认为’a’)。 |
format | handler使用指明的格式化字符串。 |
datefmt | 使用指明的日期/时间格式。 |
level | 指明根logger的级别。 |
stream | 使用指明的流来初始化StreamHandler。该参数与’filename’不兼容,如果两个都有,’stream’被忽略。 |
格式 | 描述 |
---|---|
%(levelno)s | 打印日志级别的数值 |
%(levelname)s | 打印日志级别名称 |
%(pathname)s | 打印当前执行程序的路径 |
%(filename)s | 打印当前执行程序名称 |
%(funcName)s | 打印日志的当前函数 |
%(lineno)d | 打印日志的当前行号 |
%(asctime)s | 打印日志的时间 |
%(thread)d | 打印线程id |
%(threadName)s | 打印线程名称 |
%(process)d | 打印进程ID |
%(message)s | 打印日志信息 |
关于时间格式,参考time.strftime。
第三种方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# simple_logging_config.py import logging import logging.config # 读取config文件 logging.config.fileConfig('logging.conf') # 创建logger记录器 logger = logging.getLogger('simpleExample') # 使用日志功能 logger.debug('debug message') logger.info('info message') logger.warn('warn message') logger.error('error message') logger.critical('critical message') |
其中的logging.conf配置文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
[loggers] keys=root,simpleExample [handlers] keys=consoleHandler [formatters] keys=simpleFormatter [logger_root] level=DEBUG handlers=consoleHandler [logger_simpleExample] level=DEBUG handlers=consoleHandler qualname=simpleExample propagate=0 [handler_consoleHandler] class=StreamHandler level=DEBUG formatter=simpleFormatter args=(sys.stdout,) [formatter_simpleFormatter] format=%(asctime)s - %(name)s - %(levelname)s - %(message)s datefmt= |
在命令行中执行代码,结果如下:
1 2 3 4 5 6 |
$ python simple_logging_config.py 2017-12-19 15:38:55,977 - simpleExample - DEBUG - debug message 2017-12-19 15:38:55,979 - simpleExample - INFO - info message 2017-12-19 15:38:56,054 - simpleExample - WARNING - warn message 2017-12-19 15:38:56,055 - simpleExample - ERROR - error message 2017-12-19 15:38:56,130 - simpleExample - CRITICAL - critical message |
Python官方更推荐第三种新的配置方法,类字典形式的配置信息,因为Python的字典运用形式多样,操作灵活。比如,你可以通过JSON格式保存字典,或者YAML格式保存信息,然后读取成字典。当然,你也可以直接在Python代码里编写传统的带有配置信息的字典。一切都是基于键值对形式的就OK。
下面的例子就是基于YAML配置文件的日志。logging.conf.yaml
配置文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
version: 1 formatters: simple: format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' handlers: console: class: logging.StreamHandler level: DEBUG formatter: simple stream: ext://sys.stdout loggers: simpleExample: level: DEBUG handlers: [console] propagate: no root: level: DEBUG handlers: [console] |
这里要先通过pip安装yaml模块:
1 |
$ pip install pyyaml |
yaml模块的使用很简单,使用open()方法打开一个yaml文件对象,然后使用yaml的load()方法将文件内容读成一个Python的字典对象。最后我们根据这个字典对象,使用logging.conf的dictConfig()方法,获取配置信息。如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import logging import logging.config import yaml # 通过yaml文件配置logging f = open("logging.conf.yaml") dic = yaml.load(f) f.close() logging.config.dictConfig(dic) # 创建logger logger = logging.getLogger('simpleExample') # 输出日志 logger.debug('debug message') logger.info('info message') logger.warn('warn message') logger.error('error message') logger.critical('critical message') |
输出结果:
1 2 3 4 5 |
2017-12-27 17:41:09,241 - simpleExample - DEBUG - debug message 2017-12-27 17:41:09,242 - simpleExample - INFO - info message 2017-12-27 17:41:09,242 - simpleExample - WARNING - warn message 2017-12-27 17:41:09,242 - simpleExample - ERROR - error message 2017-12-27 17:41:09,242 - simpleExample - CRITICAL - critical message |
最后关于第四种、第五种配置方式,用到的少,真的用到再补充。
七、日志处理流程
日志事件信息在loggers和handlers中的逻辑流程如下图所示:
1. 判断日志的等级是否大于Logger对象的等级,如果大于,则往下执行,否则,流程结束。
2. 产生日志。第一步,判断是否有异常,如果有,则添加异常信息。第二步,处理日志记录方法(如debug,info等)中的占位符,即一般的字符串格式化处理。
3. 使用注册到Logger对象中的Filters进行过滤。如果有多个过滤器,则依次过滤;只要有一个过滤器返回假,则过滤结束,且该日志信息将丢弃,不再处理,而处理流程也至此结束。否则,处理流程往下执行。
4. 在当前Logger对象中查找Handlers,如果找不到任何Handler,则往上到该Logger对象的父Logger中查找;如果找到一个或多个Handler,则依次用Handler来处理日志信息。但在每个Handler处理日志信息过程中,会首先判断日志信息的等级是否大于该Handler的等级,如果大于,则往下执行(由Logger对象进入Handler对象中),否则,处理流程结束。
5. 执行Handler对象中的filter方法,该方法会依次执行注册到该Handler对象中的Filter。如果有一个Filter判断该日志信息为假,则此后的所有Filter都不再执行,而直接将该日志信息丢弃,处理流程结束。
6. 使用Formatter类格式化最终的输出结果。 注:Formatter同上述第2步的字符串格式化不同,它会添加额外的信息,比如日志产生的时间,产生日志的源代码所在的源文件的路径等等。
7. 真正地输出日志信息(到网络,文件,终端,邮件等)。至于输出到哪个目的地,由Handler的种类来决定。
<摘自>
https://www.jianshu.com/p/feb86c06c4f4
http://www.liujiangblog.com/course/python/71
https://docs.python.org/3/whatsnew/3.7.html#logging