我的工作,主要是在linux上写一些服务程序,而对于一个运行的服务来说,日志非常重要,因为它记录了运行时的所有信息。大多数情况下,我们很少去翻看自己的日志,但是如果系统出现异常,那么日志的作用就非常明显了。
而且不仅仅是在线上运行时,在调试程序的时候,打一些必要的debug级日志,对于定位错误也很有用,在上线的时候,我们只需要将日志级别设计为info,就可以“除去”这些debug信息了。
粗暴的过去
有那么一群粗暴的程序员,无论是调试代码,还是打印日志,总是会print
各种上,首先这些print
很少能留下极其有用的信息,还有你无法简单的将这些日志定位到各个地方(终端or文件),也不能已各种格式去打印它们,而且你还会将它们和真正的print
进行混淆,很多人喜欢用print
,仅仅是因为它们确实很简单,没什么门槛,所以同样的,它们什么也不能干。这些粗暴的人,永远都不会知道ipdb有多爽,logging有多爽。
python logging模块
每种语言都会带有logging模块,用法也是大同小异,绝对不会出现说我python能做的一个库,别的语言做不出来。
相比于print,日志有如下好处:
你可以控制消息的级别,过滤掉那些并不重要的消息。
你可决定输出到什么地方,以及怎么输出。
你可以打印不同级别的日志。
1 | logger.debug('Debug...') |
还可以设置打印的级别,比如如果你设置为info级别,就会过滤掉debug级别的日志。
1 | logging.basicConfig(level=logging.Info) |
还有日志的打印格式:
1 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') |
一般而言,都要为一个日志,设置多个handler,每个handler代表不同的行为,最常用的两种handler是StreamHandler和FileHandler,其中StreamHandler用来输出到流,而FileHandler用来输出到文件。
还有一种很常用的是TimeRotatingFileHandler,当日志文件较多,就可以使用此方法来按天切割,或者是按其他方式切割。
还可以使用配置文件,来配置各种参数,目前的python logger模块,不但支持fileConfg,和dictConfig的配置方式:
fileConfig:
1 | [formatter_default] |
dict:
1 | 'formatters': { |
还可以通过 json和yaml来记录日志。
这里要注意 disable_existing_loggers参数,防止在你加载配置之前就创建logger,然后发现没什么效果。
1 | { |
载入json的代码:
1 | import json |
私房菜
在工作的过程中,总结了一些和打印日志有关的技巧,在这里分享一下。
调试代码
之前有说过,即使是调试代码,我也会使用日志模块而非print,[line:%(lineno)d]
会显示行号,可以方便的让我知道,此条信息在那个位置。而且,这些地方,将来也可以直接作为真实的配置,改几行配置就ok了。
必要的打印
首先作为一个系统而言,大多数情况,你都要接受一些数据,处理它们,然后在返回一些数据给别人,(比如你的一个web server)。
所以接受到的数据,和你要发出去的数据,这些都是必须要打的。而且在和资源打交道的时候,也要记录关键的信息,那么哪些算是资源呢,就是你写try catch
的地方,比如说磁盘访问,比如数据库访问,比如请求网络服务器,这些都算是与小系统的交互,必须要将输入和输出写入日志。而且这些内容都会伴有异常,所以遇到异常更是要已error写入logger。
作为一个webserver,我会将请求者的ip和请求时间,打印到日志中。
区分debug,info,error,critical
在调试的时候,一般使用debug,比如我在数据库操作时,会把将要执行的sql语句,做成debug。
1 | logger.debug('sql ....' % (args..)) |
或者有大量中间状态的循环,我也会使用debug,来将结果打印。
日常处理就用info了,这算是使用最频率的打印了。
没有发生异常的错误,但是却是比较重要的事情,就应该使用warning:比如,当用户登录密码错误时,或者连接变慢时。
error经常就是和Exception搭配的东西了。
critical几乎没用过,据说磁盘满了,内存溢出,才会用到。
日志命名
很长一段时间,我在各种传日志对象,为了让它们公用一个日志文件,后面我使用了配置,而不是代码来设计日志的handler,在那些需要打印日志的模块中,它们只需要使用__name__
获取就行了:
1 | logger = logging.getLogger(__name__) |
收集信息的技巧
需要注意的是,很多人在打印error的时候,直接error(e)
,实际上这样并不能记录完整的异常栈信息,你应该捕获异常并用 traceback 把它们记录下来。
1 | except Exception as e: |
还有在打印类似于[],{},()
等数据结构的时候,如果里面有中文,那么将打出来的是类似u'\u4f60\u597d'
的unicode,这简直查看日志增加难度,一个比较好的做法,是把它们转成json再去打印,当然转也是有技巧的,需要设置参数。
1 | json.dumps(d, ensure_ascii=False) |
层次结构
日志消息类似事件,会在层次结构中流动
层次结构中的每个logger都有机会将这个消息输出、忽略、传递给父级。
而日志默认并没有配置其等级(或设置为NOTSET))。这意味着logger只会把消息传递给父级,然后不断重复这个步骤,一直到根logger。(你设置等级,消息也是先处理,然后在传递给父类)
所以我们常常要配置根logger。使用basicConfig
。因为无论怎样,所有的消息都会到root那里:
1 | # -*- coding: utf-8 -*- |
找不到handler
初学python打日志的人,常常会看到一些:No handlers could be found for logger "xxx"
如果你没有配置任何日志处理函数,但是某处尝试打印日志消息。这个消息沿着层次结构向上传递,直到在结构链的顶处失败。所以要么你配置basicConfig,要么给你的logger加一个handler。