段子里说,使用日志的程序员鄙视使用print记录信息的程序员,所以作为一个合格的程序员,合理的记录日志实数非常必要的。本文主要介绍在python中如何使用logging模块记录日志。
1. 如何记录日志
我想对于程序员来说,记录日志的重要性不言而喻,各种bug的调试都离不开日志信息的参考,但是如何记录日志以及该记录一些什么信息却不是轻而易举就能掌握的。对于如何使用日志,网络大神已经给出了很好的答案。
- 不应该自己写log,应该熟练的使用编程语言中对应的日志记录的模块。
- 应该将log信息对应到适当的级别,有一下经验可以遵循:
- TRACE level: 如果使用在生产环境中,这是一个代码异味(code smell)。它可以用于开发过程中追踪bug,但不要提交到你的版本控制系统
- DEBUG level: 把一切东西都记录在这里。这在debug过程中最常用到。我主张在进入生产阶段前减少debug语句的数量,只留下最有意义的部分,在调试(troubleshooting)的时候激活。
- INFO level: 把用户行为(user-driven)和系统的特定行为(例如计划任务…)
- NOTICE level: 这是生产环境中使用的级别。把一切不认为是错误的,可以记录的事件都log起来
- WARN level: 记录在这个级别的事件都有可能成为一个error。例如,一次调用数据库使用的时间超过了预设时间,或者内存缓存即将到达容量上限。这可以让你适当地发出警报,或者在调试时更好地理解系统在failure之前做了些什么
- ERROR level: 把每一个错误条件都记录在这。例如API调用返回了错误,或是内部错误条件
- FATAL level: 末日来了。它极少被用到,在实际程序中也不应该出现多少。在这个级别上进行log意味着程序要结束了。例如一个网络守护进程无法bind到socket上,那么它唯一能做的就只有log到这里,然后退出运行。
- 应该写有意义的log,每一个log都应该对应有其价值
- 日志信息最好是使用英语,并且合理的使用一些公认的简称或者是代码
- 日志格式比较重要,最好是遵循日志库提供的标准格式
- 记录日志的时候可以多考虑阅读者
- 日志的功能不仅在于调试,在审查、建档、统计等功能中也用的比较多
2. logging模块简介
logging是python内置的一个标准模块,主要用于输出或者保存程序运行日志,它有以下优点: - 可以设置输出日志的等级、日志保存路径、日志文件回滚 - 不仅可以控制台输出,还可以文件保存,不仅可以在代码中配置,还支持从配置文件中直接加载日志的配置 与print()函数相比,logging有很多优势,如果还在用print()记录日志,从现在起,习惯使用logging吧。
3. logging的等级
logging中包含了6个等级,分别是:(log是一个logging实例)
log等级 | 使用范围 | 函数 | 备注 |
---|---|---|---|
FATAL | 致命错误 | log.fatal | |
CRITICAL | 特别糟糕的事情 | log.critical | 如内存耗尽、磁盘空间为空,一般很少使用 |
ERROR | 发生错误时 | log.error | 如 IO操作失败或者连接问题 |
WARNING | 发生很重要的事件,但是并不是错误时 | log.warning | 如用户登录密码错误 |
INFO | 处理请求或者状态变化等日常事务 | log.info | |
DEBUG | 调试过程中使用DEBUG等级 | log.debug | 如 算法中每个循环的中间状态 |
在这些等级中,由下向上严重性依次递增,也就是DEBUG是最轻的,FATAL是最严重的。每一个等级都对应于一个函数用于记录对应等级的日志。当设置了输出日志的等级后,只会输出或者保存当前等级以及更严重等级的信息,这样可以在不同的环境中输出不用的调试信息。例如设置了日志等级是DEBUG,那么所有的日志都会输出;如果设置为ERROR,那么就只有ERROR,CRITICAL,FATAL这三个等级会被保存或者输出。
4. logging的使用
logging的使用流程是:1. 配置logging,包括是在控制台输出还是保存在文件、输出内容、输出格式等(可以在代码中直接配置,也可以在配置中加载配置)2.初始化一个日志类 3. 使用不同的级别日志函数记录日志
4.1 基本使用:在控制台输出日志信息
logging的基本使用是在控制台中,使用例程:
代码语言:javascript复制import logging
# config the logging
logging.basicConfig(level=logging.DEBUG, format='%(lineno)d - %(asctime)s - %(levelname)s - %(message)s')
# create a logging object
log = logging.getLogger(__name__)
# using logging function to log
log.info('start logging')
log.debug('this is a debug')
log.warning('just a warning')
log.error('here is a error')
log.critical('here is a critical error')
log.fatal(' big bug')
因为设置的level是DEBUG,所以所有的信息都会打印输出:
代码语言:javascript复制12 - 2018-09-16 21:05:38,837 - INFO - start logging
13 - 2018-09-16 21:05:38,838 - DEBUG - this is a debug
14 - 2018-09-16 21:05:38,839 - WARNING - just a warning
15 - 2018-09-16 21:05:38,840 - ERROR - here is a error
16 - 2018-09-16 21:05:38,841 - CRITICAL - here is a critical error
17 - 2018-09-16 21:05:38,842 - CRITICAL - big bug
在logging.basicconfig中把level设置为ERROR,输出为:
代码语言:javascript复制15 - 2018-09-16 21:54:12,820 - ERROR - here is a error
16 - 2018-09-16 21:54:12,821 - CRITICAL - here is a critical error
17 - 2018-09-16 21:54:12,821 - CRITICAL - big bug
这个例子就验证了前面所说的:当设置了输出日志的等级后,只会输出或者保存当前等级以及更严重等级的信息,这样可以在不同的环境中输出不用的调试信息。例如设置了日志等级是DEBUG,那么所有的日志都会输出;如果设置为ERROR,那么就只有ERROR,CRITICAL,FATAL这三个等级会被保存或者输出。
- 下面说一下logging.basicConfig函数参数的含义:
参数名 | 作用 | 备注 |
---|---|---|
filename | 指定日志文件名 | |
filemode | 和file函数意义相同,指定日志文件的打开模式,’w’或者’a’; | |
format | 指定输出的格式和内容 | |
datefmt | 指定时间格式,同time.strftime() | |
level | 设置日志级别 | 默认为logging.WARNNING |
stream | 指定将日志的输出流,可以指定输出到sys.stderr,sys.stdout或者文件 | 默认输出到sys.stderr,当stream和filename同时指定时,stream被忽略; |
- format参数的选择
参数名 | 参数意义 | 备注 |
---|---|---|
%(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 | 打印日志信息 |
在实例中:format='%(lineno)d - %(asctime)s - %(levelname)s - %(message)s'
就是打印当前运行行,时间,级别,日志信息
4.2 将日志保存在文件
将日志保存在文件一般有三种形式:只保存在文件、控制台显示同时保存在文件、日志回滚
4.2.1 只保存在文件
这种方式的使用过程是:设置logging并创建一个FileHandler,并对输出消息的格式进行设置,将其添加到logger,然后将日志写入到指定的文件中。
代码语言:javascript复制import logging
# Instantiate a logging
logger = logging.getLogger(__name__)
# set level
logger.setLevel(level = logging.INFO)
# use FileHander to set file
handler = logging.FileHandler("log.txt")
# set level
handler.setLevel(logging.INFO)
# set format
formatter = logging.Formatter('%(lineno)d - %(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("this is a info")
logger.debug("just debug")
logger.warning("this ia a warning")
logger.error("sorry, this is a bug")
查看txt中的内容:
代码语言:javascript复制16 - 2018-09-16 22:19:31,449 - __main__ - INFO - this is a info
18 - 2018-09-16 22:19:31,449 - __main__ - WARNING - this ia a warning
19 - 2018-09-16 22:19:31,449 - __main__ - ERROR - sorry, this is a bug
因为LEVEL设置为INFO所以debug的日志没有记录(debug级别比info更低)
- NOTES
代码中有两个设置level的地方,logger.setLevel(level = logging.INFO)
是设置整个logging的level,handler.setLevel(logging.INFO)
是设置打印到文件的level,实际工作中只会保存两个level中级别最严格的那个。
4.2.2 控制台输出同时文件保存
代码语言:javascript复制import logging
logger = logging.getLogger(__name__)
logger.setLevel(level = logging.INFO)
handler = logging.FileHandler("log.txt")
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
console = logging.StreamHandler()
console.setLevel(logging.ERROR)
# TWO kind way must be added
logger.addHandler(handler)
logger.addHandler(console)
logger.info("this is a info")
logger.debug("just debug")
logger.warning("this ia a warning")
logger.error("sorry, this is a bug")
控制台输出:
代码语言:javascript复制sorry, this is a bug
txt文件中的内容:
代码语言:javascript复制2018-09-16 22:30:30,913 - __main__ - INFO - this is a info
2018-09-16 22:30:30,914 - __main__ - WARNING - this ia a warning
2018-09-16 22:30:30,914 - __main__ - ERROR - sorry, this is a bug
因为控制台和文件的level设置的不一样,所以日志信息不一样 - notes 也就是说,保存文件个控制台都可以单独设置level,实际工作中以单独了level和全局level中最严格的那个level为准
4.2.3 日志回滚
将日志信息输出到一个单一的文件中,随着应用程序的持续使用,该日志文件会越来越庞大,进而影响系统的性能。因此,有必要对日志文件按某种条件进行切分,要切分日志文件,这种方式就是日志回滚。 分割日志的触发条件:大小、日期,或者大小加上日期。说是切分,实际上是,当一个日志文件达到触发条件后,对日志文件进行重命名,之后再新建原来名称的日志文件(此时就是空文件了),新产生的日志就写入新的日志文件。 为啥叫回滚呢?当分割的日志文件达到指定数目的上限个数时,最老的日志文件就会被删除。 logging模块中使用RotatingFileHandler,可以实现日志回滚。
代码语言:javascript复制import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger(__name__)
logger.setLevel(level = logging.INFO)
#定义一个RotatingFileHandler,最多备份3个日志文件,每个日志文件最大1K
rHandler = RotatingFileHandler("log.txt",maxBytes = 1*512,backupCount = 3)
rHandler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
rHandler.setFormatter(formatter)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(formatter)
logger.addHandler(rHandler)
logger.addHandler(console)
logger.info("this is a info")
logger.debug("just debug")
logger.warning("this ia a warning")
logger.error("sorry, this is a bug")
多运行几次,就看到产生了很多txt文件,但最多三个txt日志文件
代码语言:javascript复制-a---- 2018/9/16 22:40 453 log.txt
-a---- 2018/9/16 22:39 512 log.txt.1
4.2.4 捕获异常
logging还可以用来捕获python异常,
代码语言:javascript复制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)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
logger.addHandler(handler)
logger.addHandler(console)
logger.info("this is a info")
logger.debug("just debug")
logger.warning("this ia a warning")
logger.error("sorry, this is a bug")
try:
open("a.txt","rb")
except (SystemExit,KeyboardInterrupt):
raise
except Exception:
logger.error(" ",exc_info = True)
txt文件中就可以看到:
代码语言:javascript复制Traceback (most recent call last):
File "g:myfileshomePythonHBNget_ibfo.py", line 21, in <module>
open("a.txt","rb")
FileNotFoundError: [Errno 2] No such file or directory: 'a.txt'
后续更新在不用的模块中同时使用log以及怎么通过配置文件加载logging的配置