当我们在使⽤Flask时,如何记录⽇志
我们在开发基于Flask的Web应⽤时,往往容易忽略了对⽇志的使⽤,⽽在Flask的官⽅⽂档中,对⽇志这块的介绍也仅仅停留在如何与系统集成上。记录⽇志这个看似很简单的事情,在实际中很多⼈却不⼀定能做好,要么不知道何时进⾏⽇志记录,要么就是记录的⽇志然并卵。所以,今天就来说说记录⽇志这件⼩事。
说它是件⼩事,因为它的确不会影响你系统的正常流程,有没有它系统都能跑起来,也正因为这样,很多⼈便忽略了⽇志的处理,或者⼲脆都没有配置⽇志输出,整个系统没有任何⽇志输出(Nginx⽇志不算)。当然,如果是我们⾃⼰开发的⼀些⼩⽹站,⼤家⽤来练练⼿或着⽤户量不⼤,有没有⽇志都⼀样,但是对于⼀些⼤型的系统,它的⽤户是很多的,在任何⼀个环节都可能出问题,为了能够及时的定位问题和监控系统运⾏状态,正确合理的记录⽇志就⾮常⾮常重要了。⼀般情况下,我们需要关注的是三个⽅⾯的内容:
⽇志信息的集中采集、存储和检索:这个主要是在多节点的情况下如何⽅便的查看⽇志信息。
⽇志信息的输出策略:要保证⽇志输出的全⾯⽽⼜不显凌乱,⽅便开发⼈员跟踪和分析问题。
关键业务的⽇志输出:我们常说的打点就属于这个范畴,⽐如我们的⼀些浏览记录等,这个就需要根据
业务要求来做针对性设计了。我们在这⾥主要是围绕第⼆个问题来展开,也是我们开发⼈员最直接接触的情况。
⽇志输出级别
从Flask 0.3 版本开始,系统就已经帮我们预配置了⼀个logger,⽽这个logger的使⽤也是⾮常简单:
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
('An error occurred')
其中的app就是Flask的实例,⽽这个app.logger也就是⼀个标准的logging Logger,因此,我们在使⽤app.logger时可选择的输出级别与python logging中的定义是⼀致的。
ERROR:这个级别的⽇志意味着系统中发⽣了⾮常严重的问题,必须有⼈马上处理,⽐如数据库不可⽤了,系统的关键业务流程⾛不下去了等等。很多⼈在实际开发的时候,不会去区分问题的重要程度,只要有问题就error记录下来,其实这样是⾮常不负责任的,因为对于成熟的系统,都会有⼀套完整的报错机制,那这个错误信息什么时候需要发出来,很多都是依据单位时间内error⽇志的数量来确
定的。因此如果我们不分轻重缓急,⼀律error对待,就会徒增报错的频率,久⽽久之,我们的救⽕队员对错误警报就不会那么在意,这个警报也就失去了原始的意义。
WARN:发⽣这个级别的问题时,处理过程可以继续,但必须要对这个问题给予额外的关注。假设我们现在有⼀个系统,希望⽤户每⼀个⽉更换⼀次密码,⽽到期后,如果⽤户没有更新密码我们还要让⽤户可以继续登录,这种情况下,我们在记录⽇志时就需要使⽤WARN级别了,也就是允许这种情况存在,但必须及时做跟踪检查。
INFO:这个级别的⽇志我们⽤的也是⽐较多,它⼀般的使⽤场景是重要的业务处理已经结束,我们通过这些INFO级别的⽇志信息,可以很快的了解应⽤正在做什么。我们以在12306上买⽕车票为例,对每⼀张票对应⼀个INFO信息描述“[who] booked ticket from [where] to [where]”。
DEBUG和TRACE:我们把这两个级别放在⼀起说,是应为这两个级别的⽇志是只限于开发⼈员使⽤的,⽤来在开发过程中进⾏调试,但是其实我们有时候很难将DEBUG和TRACE区分开来,⼀般情况下,我们使⽤DEBUG⾜以。
以上就是我们实际开发中最多接触的⼏种⽇志级别,基本能覆盖99%的情况。最后我们要注意的就是,最好能尽可能输出更多的⽇志信息,并且不做任何过滤,同时输出的每⼀条⽇志的详细信息要切当,让我们可以快速过滤并找到所需的信息。
⽇志输出信息
当我们确定了使⽤哪个级别来写⼊⽇志后,下⼀步要做的就是确定要记录什么样的信息。针对这⼀块的内容,其实⼜可以细分。
⽇志应该记录什么
⼀般来说,⽇志的记录要满⾜⼏个要求:可读、⼲净、详细和⾃描述。我们先来看⼏个反模式的例⼦:
子处app.logger.debug('message procesd')
app.logger._message_id())
app.logger.debug('message with id %s', _message_id())
if isinstance(message, TextMessage):
...
el:
app.logger.warn('unknow message type')
上⾯列出的这⼏个例⼦,问题在什么地⽅呢?当看到这些⽇志信息后,我们⾸先意识到的肯定是哪⾥出了问题,但是,这个问题产⽣的原因是什么我们并不清楚,也就是说,只根据这样的⽇志我们是没办法对问题进⾏修复的。所以我们在记录⽇志的时候,应该要尽量的详细,⽇志的上下⽂要交代清楚。
4万以下的新车
另外⼀种反模式的⽇志信息,我们通常叫做魔法⽇志。就⽐如说,有的开发⼈员为了⾃⼰查找信息⽅便,会输出⼀些类似&&&###>>>>>>的⽇志,这些特殊的符号只有开发者本⼈清楚到底是做什么的,⽽对其他⼈来说,看到这些⽇志绝对是⼀脸懵逼。即使是开发者本⼈,哪怕当时能够清楚这些魔法⽇志的特殊含义,但时间⼀长,估计他们也很难回想起当时为啥要输出这些⿁东西了吧。
其次,还有⼀种是要关注外部系统,也就是在和任何外部系统通信的时候,建议记录所有进出系统的数据。但是,记录这些信息时,性能是⼀个⽐较头痛的问题,有时,需要和外部系统交换的信息量太⼤,导致⽆法记录所有信息。但是,在调试或测试期间我们还是愿意记录所有信息,并准备为此付出⼀些性能的代价,这个可以通过仔细控制log级别来实现,⽐如:
fig['debug']:咳嗽带血怎么回事
app.logger.debug('...')
el:
app.logger.info('...')
晚上看的小说最后要提的⼀点就是,我们输出的这些⽇志信息中,绝对不可以透露系统密码和⼀些个⼈信息。
如何打印⽇志内容
当我们明确了该⽤哪个级别去记录哪些信息后,就要把这些信息输出到⽇志⽂件中,但是想正确⾼效的打印⽇志内容也并⾮⼀件简单的事。Flask已经帮我们预配置了⼀个logger,我们可以使⽤这个logger来完成我们所有的打印操作。
我们在记录⽇志的时候绝对不可以使⽤print,即使我们在开发调试的时候能够在控制台看到打印的信息,但是这样的信息并不会记录到⽇志⽂件中,当我们的程序上线后,跟没有记录⽇志的效果是⼀样的。因此,哪怕是在开发调试时,也要尽量使⽤logger。
最简单的⽇志打印就是输出⼀个字符串,⽐如像下⾯这样
app.logger.info('this is a string')
孕妇感冒吃什么药
但⼤部分时候,我们要记录的信息都会包含⼀些参数,有⼀种实现⽅式是提前构造出这个字符串
message_info = 'the message is %s' % info
app.logger.info(message_info)
这种写法也没啥问题,但其实,logger内部也可以帮助我们完成上⾯的操作,也就是我们还可以写成下⾯这样
app.logger.info('the message if %s', info)
这样看起来是不是简洁了好多呢?
记录异常信息
记录异常信息严格来说也应该算到⽇志输出的范畴,之所以把它拿出来单独说,是因为除了说明应该怎样去记录异常外,这⾥还要说下如何去⾃定义异常。
正确的记录异常信息
对于异常,我们更想看到的其实是它的堆栈信息,⽽不是简单的⼀句话,堆栈信息可以帮助我们快速的定位问题出处。如果想打印堆栈,我们前⾯的记录⽅法就不再实⽤了,哪怕我们把Exception的实例丢到logger⾥打印出来的也仅仅是错误信息⽽不是堆栈信息。⽐如下⾯的例⼦
a = [1, 2, 3]
try:
print a[3]
except Exception, e:
<(e)
如果我们查看⽇志,发现打印出的仅仅是⼀⾏错误信息:
Tue, 24 Jan 2017 16:07:20 demo.py[line:22] ERROR list index out of range
那如何打印出堆栈信息呢?python给我们提供了⼀个exception⽅法,它的使⽤跟debug、info、warn、error⼏个⽅法是⼀样的,我们可以把上⾯的代码修改⼀下
a = [1, 2, 3]
try:
print a[3]
except Exception, e:
之后我们再看⽇志输出
Tue, 24 Jan 2017 17:19:37 demo.py[line:22] ERROR list index out of range
Traceback (most recent call last):
File "/Urs/xiaoz/developments/python/ttest/demo.py", line 20, in test
print a[3]
IndexError: list index out of range
除了我们上⾯使⽤error⽅法打印的错误信息外,还打印出了出错的堆栈信息,由此看见,exception⽅法打印的⽇志会包含两项,第⼀项就是调⽤exception⽅法时传⼊的message,还有⼀项是紧跟在message后⾯的堆栈信息。
当我们使⽤exception⽅法时,它记录的⽇志级别为ERROR,如果我们希望打印出堆栈信息,同时⼜
不希望使⽤ERROR这个级别怎么办呢?如果你查看exception⽅法的实现,会发现,它只是多加了⼀⾏代码kwargs['exc_info'] = 1,然后调⽤了error⽅法,以此类推,如果我们希望打印堆栈信息就可以像下⾯这样写:app.logger.info('message info is %s', message, exc_info=1)。
Flask允许我们⾃定义对异常的处理,⼀般情况下,我们会做⼀些统⼀处理,⽐如下⾯这样
@handler(500)
def internal_rver_error(e):
ption('error 500: %s', e)
respon = json_error('internal rver error')
respon.status_code = 500
return respon
我们在返回结果的同时,对错误信息进⾏了记录处理,这样做也是为了避免模板代码,减少开发⼈员的⼯作量。但是,在减少我们开发量的同时,这也意味着可能会带来另外⼀个问题,很多程序员喜欢捕获异常后将错误写⼊⽇志,然后再将异常包装后重新抛出,这样会重复打印⽇志信息:
... some thing ...75年属什么
try:
... do some thing ...
except Exception, e:
(e)
rai SomeException(e)
还有⼀种情况,如果我们在捕获异常的时候,不分情况统⼀捕获Exception也是不对的。直接捕获Exception固然⽅便,但是我们捕获的范围太⼤的话,有的时候会吃掉关键的信息,⽽这些被吃掉的异常⼜没有打印错误信息和堆栈,⼀旦有问题,是很难排查的,⽐如我们定义了下⾯的⽅法
def some_method():
.. do some thing ..
try:
.. do dome thing 2 ..
return True
except Exception, e:
return Fal
当do some thing 2中发⽣异常时,我们是没法察觉到的,这样不但会使⽅法返回不正确,也会给排查带来困难。
⾃定义异常
⾸先,我们看看为什么要⾃定义异常,在需要抛出异常的地⽅,我们直接rai Exception()不好吗?答案很显然,肯定是不好,具体的原因,我们下⾯就逐条分析下。
1. ⼤多时候我们都是使⽤json来为不同端提供接⼝⽀持,不管成功与否,都必须使⽤统⼀的数据格式。如果系统充斥着各种异常就很难
做到统⼀。
2. 要能反映出该异常的重要程度,⽐如:如果是参数校验异常则被认为不是很重要,可能直接记下warn⽇志就⾏了,⽽orm异常必须要
记录error⽇志。
3. 最后,对于异常的信息要有区分,⽐如,对于orm异常,我们希望给⽤户看到的是⼀条简单的系统出错的提⽰信息,⽽我们在查看⽇
志时必须要有详细的异常信息。
为了解决上⾯的问题,需要我们来⾃定义异常,并且使⽤的时候也尽量要使⽤已定义的异常类。这⾥我们来看⼀种实现⽅式,⼤家可以参考
class BaError(Exception):
default_status_code = 200
LEVEL_DEBUG = 0
LEVEL_INFO = 1
LEVEL_WARN = 2
LEVEL_ERROR = 3
def __init__(lf, message, status_code=None, extras=None, parent_error=None):
lf._message = message
lf._code = status_code
lf.parent_error = parent_error
lf.level = BaError.LEVEL_DEBUG
@property
def status_code(lf):
if not lf._code:
return BaError.default_status_code
return lf._code
def to_dict(lf):
rv = {
'error_message': lf._message,
'status_code': lf.status_code,
'success': Fal
责任的定义
等呀等}
return rv
class ValidationError(BaError):
def __init__(lf, message, extras=None):
super(ValidationError, lf).__init__(message=message, extras=extras)
lf.level = BaError.LEVEL_INFO
class NotFoundError(BaError):
def __init__(lf, message, extras=None):
super(NotFoundError, lf).__init__(message=message, extras=extras)
lf.level = BaError.LEVEL_WARN
class FormError(BaError):
def __init__(lf, form):
message = _validate_error()
super(FormError, lf).__init__(message, extras=form.data)
lf.level = BaError.LEVEL_INFO
class OrmError(BaError):
def __init__(lf, message, status_code=None, extras=None, parent_error=None):
super(OrmError, lf).__init__(message, status_code, extras, parent_error)
lf.level = BaError.LEVEL_ERROR
定义了上⾯的异常信息后,我们在统⼀处理错误信息的时候就可以像下⾯这样实现: