之前写过一篇文章日志的艺术(The art of logging),提到了输出日志的时候记录上下文信息的重要性,我认为上下文信息包括:

  • when:log事件发生的时间
  • where:log事件发生在哪个模块(文件、函数)
  • how important:log 事件的紧急程度
  • who:事件产生者的唯一标识
  • what:具体的事件内容,以及其他所必须的上下文信息

  其中,when、where、how important都很容易通过logging框架自动包含,但是who(事件生产者的唯一标识)就不能框架自动填充了。比如在服务器端,同时有大量的用户,如果日志缺乏用户唯一标识,如(User can not login),那么可以说这样的日志是毫无意义的。特别是当线上出问题的时候,而且是偶发的问题的时候,日志往往是查找问题的唯一途径,如果这个时候日志信息不充分,那就很让人抓狂了。

  虽然在团队中强调过很多次,但是很多同事还是会输出毫无意义的log,也曾试过静态代码检查或者运行时分析,不过都不太优雅,于是希望能够自动添加一些重要的上下文信息,比如用户唯一标识。

  虽然每天都在打log,但事实上我以前也没有深度了解过python logging模块,于是借着这个机会,仔细看了一下logging文档与源码。

  这里补充一句,虽然网上有很多文章介绍python logging模块,但还是建议直接看官方资料,顺序如下:tutorialapi documentcookbook源码

  本文地址:https://www.cnblogs.com/xybaby/p/9197032.html

初识python logging

  logging模块是python官方提供的、标准的日志模块。看代码的话是借鉴了许多log4j中的概念和思想。

  logging模块包括以下几大组件:

  • Loggers expose the interface that application code directly uses.
  • Handlers send the log records (created by loggers) to the appropriate destination.
  • Filters provide a finer grained facility for determining which log records to output.
  • Formatters specify the layout of log records in the final output.

  下面结合一个更完整的例子来逐个介绍。example1.py

 import logging
class ContextFilter(logging.Filter):
def filter(self, record):
record.userid = ''
return True if __name__ == '__main__':
# create logger
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG) # create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# create formatter for console handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# add formatter to console handler
ch.setFormatter(formatter) # create file handler and set level to warn
fh = logging.FileHandler('spam.log')
fh.setLevel(logging.WARN)
# create formatter for file handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(userid)s - %(message)s')
# add formatter to file handler
fh.setFormatter(formatter)
# add context filter to file handler
fh.addFilter(ContextFilter()) # add ch、fh to logger
logger.addHandler(ch)
logger.addHandler(fh) # 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message %s', 'args')
logger.error('error message')
logger.critical('critical message')

  console输出结果:

2018-06-10 10:44:32,342 - simple_example - DEBUG - debug message
2018-06-10 10:44:32,342 - simple_example - INFO - info message
2018-06-10 10:44:32,342 - simple_example - WARNING - warn message args
2018-06-10 10:44:32,342 - simple_example - ERROR - error message
2018-06-10 10:44:32,342 - simple_example - CRITICAL - critical message

  文件内容:

2018-06-10 10:44:32,342 - simple_example - WARNING - 123 - warn message args
2018-06-10 10:44:32,342 - simple_example - ERROR - 123 - error message
2018-06-10 10:44:32,342 - simple_example - CRITICAL - 123 - critical message

logging模块解析

logger

  logger是直接提供给应用程序使用的接口。一般来说,logging的其他几个组件(Handler、Filter、Formatter)都是在初始化Logger的时候使用。

  logger提供了以下API(部分、重要的):

Logger.setLevel(level)
  Sets the threshold for this logger to level. Logging messages which are less severe than level will be ignored.
Logger.isEnabledFor(lvl)
  Indicates if a message of severity lvl would be processed by this logger. 
Logger.debug(msg, *args, **kwargs)
  Logs a message with level DEBUG on this logger.

Logger.info(msg, *args, **kwargs)
  Logs a message with level INFO on this logger.

Logger.warning(msg, *args, **kwargs)
  Logs a message with level WARNING on this logger. same as Logger.warn

Logger.error(msg, *args, **kwargs)
  Logs a message with level ERROR on this logger.

Logger.critical(msg, *args, **kwargs)
  Logs a message with level CRITICAL on this logger. same as Logger.fatal

Logger.addFilter(filter)
  Adds the specified filter filter to this logger.
Logger.removeFilter(filter)
  Removes the specified filter filter from this logger.
Logger.addHandler(hdlr)
  Adds the specified handler hdlr to this logger.
Logger.removeHandler(hdlr)
  Removes the specified handler hdlr from this logger. 

logger实例

  Logger对象不是通过实例化Logger而来的,都是通过 logging.get_logger(name)  获得一个与name关联的logger对象,logging内部会维护这个映射关系,用同样的name反复调用logging.getLogger,事实上返回的是同一个对象。

Multiple calls to getLogger() with the same name will always return a reference to the same Logger object.

  logger有一个父子关系,比如:

1 a_logger = logging.getLogger('A’)
2 ab_logger = logging.getLogger('A.B’)

  通过name的层次关系就可以看出a_logger是ab_logger的parent。这个父子关系是会影响到日志的输出的,后面介绍logging流程的时候可以看到。不过我并没有使用过这种层级关系,所以本文也不会展开介绍。

log level

  log level即日志的重要等级,意味着不同的紧急程度,不同的处理方式。python logging中,包含以下log level:

  

  上表也给出了什么时候使用什么级别的Log level的建议。

  只有当写日志时候的log level大于等于logger的level阈值(通过setLevel设置)时,日志才可能被输出。

  比如example1.py中,输出到文件时,debug message、info message并没有输出,因为file handler的logger level是warning,DEBUG和INFO都小于WARNING。

FIlter && Handler

  从logger中Filter、Handler相关的接口可以看到一个logger可以包含多个handler,多个Filter

  在example1.py中,logger对象指定了两个handler,第14行的StreamHandler,以及第16行的FileHandler。

Handler

  Handler定义了如何处理一条日志,是本地打印,还是输出到文件,还是通过网络发送。

  可以这么理解,日志代表的是一个事件(event),输出日志的应用程序是生产者,而handler是消费者,消费者可以有多个。

  Handler有以下API:

Handler.setLevel(level)
  Sets the threshold for this handler to level. Logging messages which are less severe than level will be ignored. 
Handler.setFormatter(fmt)
  Sets the Formatter for this handler to fmt.
Handler.addFilter(filter)
  Adds the specified filter filter to this handler.
Handler.removeFilter(filter)
  Removes the specified filter filter from this handler.

  对于上述API,首先setFormatter表明了日志的格式,这里是“set”,而不是“add”,这个很好理解。其次,Handler有一些与Logger相同的函数:setLevel、addFilter、removeFilter。为啥要用相同的API,在tutorial中是这样介绍的:

Why are there two setLevel() methods? The level set in the logger determines which severity of messages it will pass to its handlers. The level set in each handler determines which messages that handler will send on.

  但个人认为这个解释很牵强,我觉得在handler(日志事件的消费者)指定过滤条件更合理、更直观。在生产者添加logLevel的目的只是为了开发调试方便:开发的时候通过debug输出调试信息,开发的时候设置高于DEBUG的log level来避免输出这些调试信息。

  python logging提供了大量的实用的Handler,可以写到标准输出、文件、linux syslog、邮件、远程服务器。

Formatter

responsible for converting a LogRecord to (usually) a string which can be interpreted by either a human or an external system  

  可以看到,Formatter将LogRecord输出成字符串。应用程序每输出一条日志的时候,就会创建一个LogRecord对象。看看上述例子:

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger.warn('warn message %s', 'args') 对应输出:
  2018-06-10 10:44:32,342 - simple_example - WARNING - warn message args

  可以看到,Formatter指定了输出了格式与内容,在logger.warn的两个输入参数组成了formatter中的 %(message)s ,而formatter中的 %(asctime) 等则是在生成LogRecord填充

  以下是默认提供的属性:

  

  如果一个handler没有指定Formatter,那么默认的formatter就是  logging.Formatter('%(message)s')  ,在上面的例子中,对于warn这条日志,那么输出结果是 warn message args 

Filter

Filters can be used by Handlers and Loggers for more sophisticated filtering than is provided by levels.

filter(record)

  Is the specified record to be logged? Returns zero for no, nonzero for yes. If deemed appropriate, the record may be modified in-place by this method.

  Filter提供的是比log level更精确的过滤控制,只有当Filter.filter函数返回True的时候,这条日志才会被输出。

  由于Filter.filter函数的参数是LogRecord实例,那么可以修改LogRecord的属性。在example1.py 第25行    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(userid)s - %(message)s')  file formatter中指定了属性%(userid)s, 但userid并不在Formatter的属性列表中,这个userid其实就是通过自定义的ContextFilter来实现的,在ContextFilter.filter中,给record对象添加了userid属性。后面还会细讲。

  官方给出了很多例子,都是通过Filter来修改LogRecord的内容,来输出一些上下文信息(contextual information)。但是我觉得这是一个不好的设计,顾名思义,Filter应该只做过滤的事情,理论上不应该修改输入,builtin filter函数就是这样的。

logging流程

  从上述的介绍中可以看出,应用层打印的一条日志,需要经过很多步才能最终落地;既受到logger(生产者)的log level、Filter过滤,又要受到Handler(消费者)的log level、Filter过滤。

  tutorial中给出了一个完整的流程图:

  

logging性能

lazy logging

  在日志的艺术(The art of logging)一文中提到了lazy logging,即避免过早求值,把字符串的格式化推迟。但对于下面的语句

logger.debug('Message with %s, %s', expensive_func1(), expensive_func2())

  虽然不会过早的字符串格式化(format),但是expensive_func1、expensive_func2这两个函数一定会调用,不管日志是否会被输出。如何避免调用这两个函数,解决办法是这样的

if logger.isEnabledFor(logging.DEBUG):
logger.debug('Message with %s, %s', expensive_func1(),
expensive_func2())

  即先判断是否能够输出DEBUG等级的log,然后再调用logger.debug,这样就避免了调用expensive_func1、expensive_func2这两个函数

  在这里,除了优化性能,还有一个好处就是让代码的意图更加明显(explicit),如果expensive_func是有副作用(side affect),即会改变某些内部状态,那么用这样的代码能更清楚表达expensive_func 不一定会被调用。当然,写在日志里面的表达式不应该有任何副作用、函数也应该是pure function。

性能开关

  python logging模块的性能是较差的,不仅仅是Python语言的问题,更多的是logging模块的实现问题。

  前面提到,应用程序的每一条日志都会生成一个LogRecord对象,然后Formatter将其转换成string,怎么转化的呢,其实核心就一句代码  s = self._fmt % record.__dict__  self是Formatter实例,_fmt即初始化Formatter时传入的参数,record即LogRecord实例。由于LogRecord不知道Formatter需要用到哪些属性,所以干脆计算出所有的属性(见图一),这是eager evaluation,lazy evaluation应该更适合

  也许Formatter只用到了%(message)s 属性,即只需要对logger.debug的输入参数求值即可,但是LogRecord还是会去反复计算线程id、进程id、应用程序的调用信息(文件、函数、行号),这些都是很耗的操作。

  对于部分确定不会用到的属性,可以通过开关关掉:

What you don’t want to collect How to avoid collecting it
Information about where calls were made from. Set logging._srcfile to None. This avoids calling sys._getframe(), which may help to speed up your code in environments like PyPy (which can’t speed up code that uses sys._getframe()).
Threading information. Set logging.logThreads to 0.
Process information. Set logging.logProcesses to 0.

  

  通过下面的代码测试(注释第3到6行),关掉之后有25%左右的性能提升

 import time
import logging
logging._srcfile = None
logging.logThreads = 0
logging.logMultiprocessing = 0
logging.logProcesses = 0 if __name__ == '__main__':
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG) null = logging.NullHandler()
logger.addHandler(null) begin = time.time()
for i in xrange(100000):
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')
print 'cost', time.time() - begin

  进一步,logging为了线程安全,每个Handler都会持有一个互斥锁,每次写日志的时候都会获取锁,写完之后再释放锁。由于GIL的问题,多线程并不能给某些类型的python程序代码性能提升。因此,如果应用程序能保证是单线程,那么可以设置  logging.thread = None  来避免使用锁,简单测试过也会有15%左右性能提升.

logging实用

  再来看几个logging使用方法或者技巧

同一个logger,不同的handler

  在前面介绍logging.Logger的时候已经提到,一个logger(生产者)可以有多个handler(消费者)。不同的handler可以有不同的输出格式(Formatter),不同的输出过滤条件(level、Filter)。

  比如对于线上项目,会有INFO、WARN、ERROR(FATAL)级别的日志产生。不同级别的日志待遇是不一样的,INFO级别的日志,一般只会在查问题的时候用到,记录到文件就行了;而WARN及以上级别的日志,就需要被监控系统采集、统计,会使用到SysLogHandler,然后配合ELK;而对于ERROR或者FATAL级别的日志,需要直接给值班账号发邮件(短信)。

  对于SyslogHandler,使用syslog_tag来设置分发方式是比较高效的,具体可见由一个简单需求到Linux环境下的syslog、unix domain socket。需要注意的是,默认情况下每一个handler都会持有一个unix domain socket的文件描述符。

上下文信息(contextual information)

  回到文章开始的问题,如何在日志中输出必须的上下文信息,还是以用户唯一标识(userid)为例。

  要做到这个事情,有不同的策略。上策是自动化,自动输出补全上下文信息;中策是检测,如果不包含上下文信息,静态代码检查或者运行时报错;下策是强调,这个最不靠谱

  在前面已经提到,将LogRecord输出成字符串核心就是   s = self._fmt % record.__dict__   ,_fmt是Formatter初始化格式,如果我们需要在_fmt中添加了自定义属性,如userid,那么只需要保证record.__dict__中有 ‘userid’ 这个key就行了。

  接下来看看具体的方法:

第一:使用Filter

  如example2所示,直接在给record设置了userid属性。

  但使用Filter有一个缺点,不太灵活,Filter一般在初始化Logger(handler)的时候使用,但userid信息实在调用时才能明确的,而且显然不会只有一个useid。

第二:使用extra参数

  再来看看  Logger.debug(msg, *args, **kwargs)

  kwargs中可以包含两个特殊的key:exc_info, extra。

The second keyword argument is extra which can be used to pass a dictionary which is used to populate the __dict__ of the LogRecord created for the logging event with user-defined attributes. These custom attributes can then be used as you like. For example, they could be incorporated into logged messages.

  具体怎么做到的?看源码就很清楚了

 def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None):
"""
A factory method which can be overridden in subclasses to create
specialized LogRecords.
"""
rv = LogRecord(name, level, fn, lno, msg, args, exc_info, func)
if extra is not None:
for key in extra:
if (key in ["message", "asctime"]) or (key in rv.__dict__):
raise KeyError("Attempt to overwrite %r in LogRecord" % key)
rv.__dict__[key] = extra[key]
return rv

  在生成LogRecord的时候会直接把extra的值赋给LogRecord.__dict__.

  因此,example2代码可以改写成这样:

 import logging

 if __name__ == '__main__':
# create logger
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG) # create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# create formatter for console handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# add formatter to console handler
ch.setFormatter(formatter) # create file handler and set level to warn
fh = logging.FileHandler('spam.log')
fh.setLevel(logging.WARN)
# create formatter for file handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(userid)s - %(message)s')
# add formatter to file handler
fh.setFormatter(formatter) # add ch、fh to logger
logger.addHandler(ch)
logger.addHandler(fh) # 'application' code
logger.debug('debug message', extra={'userid': ''})
logger.info('info message', extra={'userid': ''})
logger.warn('warn message %s', 'args', extra={'userid': ''})
logger.error('error message', extra={'userid': ''})
logger.critical('critical message', extra={'userid': ''})

  使用extra,比Filter更加灵活,每一条日志都可以根据实际情况指定不同的userid。但问题是每一条日志都需要写额外的extra参数,既是在某些环节下extra参数内容都是一样的。

  当然,如果漏了extra,会报trace,也就是上面提到的中策的效果。

第三:使用LoggerAdapter

  使用LoggerAdapter避免了每条日志需要添加extra参数的问题,达到了上策的效果。LoggerAdapter实现很简单,一看代码就明白。

  

 class LoggerAdapter(object):
def __init__(self, logger, extra):
self.logger = logger
self.extra = extra def process(self, msg, kwargs):
"""
Process the logging message and keyword arguments passed in to
a logging call to insert contextual information. You can either
manipulate the message itself, the keyword args or both. Return
the message and kwargs modified (or not) to suit your needs.
Normally, you'll only need to override this one method in a
LoggerAdapter subclass for your specific needs.
"""
kwargs["extra"] = self.extra
return msg, kwargs def debug(self, msg, *args, **kwargs):
msg, kwargs = self.process(msg, kwargs)
self.logger.debug(msg, *args, **kwargs)

  直接使用LoggerAdapter.extra覆盖掉kwargs['extra‘],当然我觉得覆盖并不是一个好主意,用update应该更合适。

  假设类UserRequest处理用户请求,那么每个UserRequest实例都持有一个   LoggerAdapter(logger, {'userid': realuserid})  ,这样写日志的时候就不会额外指定上下文信息(userid)了

缓存日志内容,延迟输出

  来自cookbook buffering-logging-messages-and-outputting-them-conditionally

  这是个非常有趣,且有用的想法。某些场景下,我们需要缓存一些日志,只有当满足某些条件的时候再输出。比如在线上查bug,需要加一些额外的调试(DEBUG或INFO)日志,但是只有当bug复现的情况下(ERROR日志标志问题复现了)才需要打印这些调试日志,因此可以先缓存,当出现了ERROR日志的时候再打印出所有的调试日志。

@log_if_errors(logger)
def foo(fail=False):

  不过上面的例子只适合单个函数调用的情况,而实际环境中,查一个BUG可能需要追踪的是一个调用流程。

总结

  python logging模块还有一些功能并未在文章介绍,比如丰富多样的Handler,比如各种logger配置方式(除了在代码中指定loglevel、formatter、filter,还可以写配置文件,甚至从网络读取配置)。

  python logging本身功能就有丰富,而且也比较好扩展,如果有什么需求,建议先好好看看文档。

references

日志的艺术(The art of logging)

logging — Logging facility for Python

python logging tutorial

logging cookbook

python logging详解及自动添加上下文信息的更多相关文章

  1. 【转】Python之向日志输出中添加上下文信息

    [转]Python之向日志输出中添加上下文信息 除了传递给日志记录函数的参数(如msg)外,有时候我们还想在日志输出中包含一些额外的上下文信息.比如,在一个网络应用中,可能希望在日志中记录客户端的特定 ...

  2. Python之向日志输出中添加上下文信息

    除了传递给日志记录函数的参数(如msg)外,有时候我们还想在日志输出中包含一些额外的上下文信息.比如,在一个网络应用中,可能希望在日志中记录客户端的特定信息,如:远程客户端的IP地址和用户名.这里我们 ...

  3. [转] Python Traceback详解

    追莫名其妙的bugs利器-mark- 转自:https://www.jianshu.com/p/a8cb5375171a   Python Traceback详解   刚接触Python的时候,简单的 ...

  4. Python Collections详解

    Python Collections详解 collections模块在内置数据结构(list.tuple.dict.set)的基础上,提供了几个额外的数据结构:ChainMap.Counter.deq ...

  5. python多线程详解

    目录 python多线程详解 一.线程介绍 什么是线程 为什么要使用多线程 二.线程实现 threading模块 自定义线程 守护线程 主线程等待子线程结束 多线程共享全局变量 互斥锁 递归锁 信号量 ...

  6. python 数据类型详解

    python数据类型详解 参考网址:http://www.cnblogs.com/linjiqin/p/3608541.html 目录1.字符串2.布尔类型3.整数4.浮点数5.数字6.列表7.元组8 ...

  7. python线程详解

    #线程状态 #线程同步(锁)#多线程的优势在于可以同时运行多个任务,至少感觉起来是这样,但是当线程需要共享数据时,可能存在数据不同步的问题. #threading模块#常用方法:'''threadin ...

  8. python数据类型详解(全面)

    python数据类型详解 目录1.字符串2.布尔类型3.整数4.浮点数5.数字6.列表7.元组8.字典9.日期 1.字符串1.1.如何在Python中使用字符串a.使用单引号(')用单引号括起来表示字 ...

  9. Python 日志输出中添加上下文信息

    Python日志输出中添加上下文信息 除了传递给日志记录函数的参数(如msg)外,有时候我们还想在日志输出中包含一些额外的上下文信息.比如,在一个网络应用中,可能希望在日志中记录客户端的特定信息,如: ...

随机推荐

  1. Android 弹性布局 FlexboxLayout了解一下

    原文链接:https://mp.weixin.qq.com/s/Mi3cK7xujmEMI_rc51-r4g RelativeLayout.LinearLayout等常用布局相信大家早已耳熟能详,今天 ...

  2. odoo11 model+Recordset 基础未完待续

    Model 一个模型代表了一个业务对象 本质上是一个类,包含了同django flask一样的数据字段 所有定义在模型中的方法都可以被模型本身的直接调用 现在编程范式有所改变,不应该直接访问模型,而是 ...

  3. 用户 'XXX\SERVERNAME$' 登录失败。 原因: 找不到与提供的名称匹配的登录名。 [客户端: ]

    一工厂的中控服务器遇到了下面Alert提示,'XXX\SERVERNAME$'  XXX表示对应的域名, SERVERNAME$(脱敏处理,SERVERNAME为具体的服务器名称+$),而且如下所示, ...

  4. SQL SERVER 查看占用tempDB

    use tempdb go t1.session_id, t1.internal_objects_alloc_page_count, t1.user_objects_alloc_page_count, ...

  5. 给html标签加上鼠标划过小手样式

    给html标签加上鼠标划过小手样式 方法:给当前标签增加样式 style="cursor:pointer;" eg:增加返回箭头样式,给箭头增加小手 <span onclic ...

  6. firefox浏览器 插件--【维基百科+谷歌翻译】高级应用之 带图翻译

    [维基词典+谷歌翻译]插件地址: https://addons.mozilla.org/zh-CN/firefox/addon/google-dictionary-and-google-t/?src= ...

  7. php学习----文件系统

    PHP文件系统之读取文件内容 PHP具有丰富的文件操作函数,最简单的读取文件的函数为file_get_contents,可以将整个文件全部读取到一个字符串中. $content = file_get_ ...

  8. LeetCode算法题-Linked List Cycle(Java实现)

    这是悦乐书的第176次更新,第178篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第35题(顺位题号是141).给定一个链表,确定它是否有一个循环. 本次解题使用的开发工 ...

  9. Git版本管理工具常用命令说明

    Git常用命令 $ touch README.md 创建一个README.md文件 $ git init  创建本地仓库(repository),将会在文件夹下创建一个 .git 文件夹,.git 文 ...

  10. 设计模式のProxyPattern(代理模式)----结构模式

    一.产生背景 在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上.在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会 ...