本笔记完全基于David Beazley的Python教程-Practical Python.
Logging,error handling and diagnostics
logging模块解决的是一个工程级别的诊断信息管理问题:如何在不污染业务逻辑的前提下,记录那些对程序分析有用的信息,并且允许用户自行决定是否查看这些信息.这个之前我们其实也有一些做法,我们将其罗列在下面的表格
| 做法 |
问题 |
| print() |
无级别,无法关闭,无法重定向 |
| pass |
丢失执行信息,丢失诊断信息 |
| logging |
可分级,可配置,可集中管理 |
所以对于日志模块而言,强调的是行为(记录日志)和策略(如何输出)分离.因此logging的设计是分层的,其中有Logger,Handler,Formatter和Filter.
我们从Logger出发,讨论日志模块.logging提供了getLogger函数来返回一个logging.Logger对象,
1 2
| import logging log=logging.getLogger(__name__)
|
这里的__name__是模块名,每个模块都有自己的log,如果getLogger同名,那么他其实是返回的同一个对象,因为Logger是按照名字全局缓存,并且可以通过名字共享配置.一个Logger实例其实代表日志命名空间的一个节点,他并非一个临时对象,而是一个长期存在且可复用的单例式对象.这些Logger会构成一个日志的命名树.Logger的日志等级的显示方式并不一定是打印在屏幕,而是受其内部配置决定.常见的日志等级如下所示
| 级别 |
用途 |
| CRITICAL |
程序无法继续 |
| ERROR |
操作失败 |
| WARNING |
异常但可以继续 |
| INFO |
关键运行信息 |
| DEBUG |
细节诊断信息 |
一般来说,默认输出是WARNING以上的警告信息.其常见用法为
1 2 3 4 5 6 7 8
| import logging name=__name__ log=logging.getLogger(name) log.debug("Debug message from %s", name) log.info("Info message from %s", name) log.warning("Warning message from %s", name) log.error("Error message from %s", name) log.critical("Critical message from %s", name)
|
上面的这些日志代码并不负责调整日志输出的形式,如果我们需要自定义一些日志配置,可以利用basicConifg函数来修改.
1 2 3 4 5 6 7
| import logging logging.basicConfig( filename='app.log', level=logging.WARNING, format='%(levelname)s:%(name)s:%(message)s' )
|
上面的代码一般出现在程序开头,用于告诉程序把日志写到哪里,写多少,怎么写.如果我们不做任何操作,那么默认的level是WARNING,输出的地方是stderr.我们也可以精确的控制某个特定的版块,
1
| logging.getLogger('fileparse').setLevel(logging.DEBUG)
|
只打开fileparse日志的debug播报等级.
Logger用于接受日志事件,那么Handler决定往哪里写,Formatter决定写入的形式,Filter决定要不要写.Handler是日志的输出通道.他负责回答这个日志写到哪里,如终端,文件,缓冲区等.常见的Handler类型如下所示
| Handler |
作用 |
| StreamHandler |
输出到流(默认是stderr) |
| FileHandler |
输出到文件 |
| RotatingFileHandler |
文件滚动 |
| TimeRotatingFileHandler |
按时间滚动 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import logging logger = logging.getLogger("demo") logger.setLevel(logging.DEBUG)
h1 = logging.StreamHandler() h1.setLevel(logging.INFO)
h2 = logging.FileHandler("app.log") h2.setLevel(logging.WARNING) logger.addHandler(h1) logger.addHandler(h2) logger.debug("debug") logger.info("info") logger.warning("warning")
|
如果我们同时给logger配置了多个Handler.同一条日志被赋值并且依次发送给每一个满足条件的Handler.其余的RotatingFileHandler表示当日志文件满足某个条件,当前文件被封存并重命名,新的日志写入一个全新的文件.
1 2 3 4 5 6
| from logging.handlers import RotatingFileHandler handler = RotatingFileHandler( "app.log", maxBytes=1024, backupCount=3 )
|
其可以限制单个日志文件的大小,同时保留历史日志,他能够保存一共n+1个日志,RotatingFileHandler主要是基于文件大小做一个轮转.而对于TimeRotatingFileHandler则是基于时间做轮转.
1 2 3 4 5 6 7
| from logging.handlers import TimedRotatingFileHandler handler = TimedRotatingFileHandler( "app.log", when="D", interval=1, backupCount=7 )
|
这里的when关键字决定轮转的时间单位,上面的when=”D”,interval=1,表示每天轮转一次,如果考虑when=’H’,interval=6,那么就是每六小时轮转一次.如果超过了backupCount,那么最旧的文件就会被自动删除.TimeRotating并不是实时记录器,他只发生在当前时间越过轮转点.
Formatter则是决定记录的日志应该是以什么形式写入.其主要的调用方式如下
1 2 3
| formatter = logging.Formatter( "%(levelname)s:%(name)s:%(message)s" )
|
这里面常用的字段为
| 字段 |
含义 |
| %(levelname)s |
日志级别 |
| %(name)s |
logger 名 |
| %(message)s |
日志内容 |
| %(asctime)s |
时间 |
| %(filename)s |
文件名 |
| %(lineno)d |
行号 |
Formatter并不是挂载在Logger上,而是挂载在Handler上.
1
| handler.setFormatter(formatter)
|
Filter则是对日志做一个筛选,考虑是否需要将这个信息写入日志,和前面提到的播报level不同的是,Filter可以基于任意的条件.
1 2 3
| class MyFilter(logging.Filter): def filter(self, record): return "password" not in record.getMessage()
|
这里返回值True表示通过,False表示拦截.Filter则是既可以挂载在Logger上,也可以挂载在Handler上.
1 2
| logger.addFilter(...) handler.addFilter(...)
|
如果挂载在Logger上,那他会自动作用在所有的Handler上.如果挂载在Handler上,那他只会作用在这个Handler.
给一个完整的代码
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import logging import sys
logging.basicConfig( level=logging.WARNING, format="%(levelname)s:%(name)s:%(message)s" )
class IgnoreKeywordFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: return "ignore" not in record.getMessage()
logger = logging.getLogger("demo.app") logger.setLevel(logging.DEBUG)
logger.propagate = False
console_formatter = logging.Formatter( fmt="%(levelname)s - %(message)s" ) file_formatter = logging.Formatter( fmt="%(asctime)s [%(levelname)s] " "%(name)s %(filename)s:%(lineno)d - %(message)s", datefmt="%Y-%m-%d %H:%M:%S" )
console_handler = logging.StreamHandler(sys.stderr) console_handler.setLevel(logging.INFO) console_handler.setFormatter(console_formatter) console_handler.addFilter(IgnoreKeywordFilter())
file_handler = logging.FileHandler("app.log", mode="w", encoding="utf-8") file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_formatter) file_handler.addFilter(IgnoreKeywordFilter())
logger.addHandler(console_handler) logger.addHandler(file_handler) def main(): logger.debug("This is a DEBUG message") logger.info("Application started") logger.warning("Low disk space") logger.error("Something went wrong") logger.warning("This message should be ignored") try: 1 / 0 except ZeroDivisionError: logger.exception("Unhandled exception occurred") if __name__ == "__main__": main()
|