日志是记录软件运行时发生事件的一种手段。事件有由一个开发者定义的重要程度;这个重要程度也可以叫做等级或者严重性。
何时使用日志
一些常见任务的最佳工具
任务 | 最佳工具 |
---|---|
展示普通用途的命令行脚本或程序的控制台输出 | print() |
报告出现在程序正常操作中的事件(比如用于状态监控或错误侦查) | logging.info(或者 logging.debug 用于诊断目的的详细输出) |
发布一个特定运行事件的警告 | logging.warning() |
报告一个特定运行事件的错误 | 抛出异常 |
报告一个抑制错误而不抛出异常(比如长期运行的服务进程的错误处理器) | logging.error(), logging.exception() 或者 logging.critical() |
日志的级别和适用情况
级别 | 适用情况 |
---|---|
DEBUG | 详细信息,通常只在诊断问题时对其感兴趣 |
INFO | 确认工作正常 |
WARNING | 表示发生了意料之外的事或者在不远的将来会有问题(比如磁盘空间低)。软件依然正常工作 |
ERROR | 由于一个更加严重的问题,软件不能执行某些功能 |
CRITICAL | 严重的错误,表示程序可能不能继续运行 |
组件
logging 库提供了以下组件:日志记录器、处理器、过滤器和格式化器。
- 日志记录器暴露应用程序代码可以直接使用的接口
- 处理器发送日志(由日志记录器创建)到对应的目的地
- 过滤器筛选日志
- 格式化器决定最终输出的日志的格式
通过调用 Logger 类的实例来记录日志。每个实例都有一个名字,并且它们使用以点号作为分隔符的命名空间等级制度。比如,名字为 scan
的记录器是 scan.text
、scan.html
、scan.pdf
的父亲。记录器的名字可以是任意值并且指明了日志信息产生的区域。
命名记录器的一个好的惯例是使用模块级别的记录器,在每个要记录日志的模块中,以一下方式命名:
logger = logging.getLogger(__name__)
这意味着记录器的名字追踪了 包/模块 的等级制度,并且可以很容易地从记录器名字中发现日志的来源。
记录器等级制度中的根叫做根记录器。那是 logging.debug()
、logging.info()
等函数使用的记录器。
目的地
可以将信息记录到不同的目的地。目的地由处理器提供。在 logging 库中支持将信息记录到文件、HTTP GET/POST 地址、基于 SMTP 的 email等,详细见 Useful Handlers。你也可以自定义日志目的地如果自带的处理器类不能满足你的特定需求。
日志记录默认是没有目的地(处理器)的。当你调用 logging
的 debug()
等函数时,它们会检查处理器是否设置了目的地;如果没有设置,它会自动调用 logging.basicConfig()
来设置。
basicConfig
logging.basicConfig
只有第一次设置才会生效,即第一次之后的设置不会覆盖第一次设置。
logging.basicConfig
默认情况下给 root logger 添加一个默认格式的、目的地为控制台的处理器。
import logging
root = logging.getLogger()
root.handlers
[]
logging.basicConfig()
root.handlers
[<logging.StreamHandler object at 0x1010b68d0>]
流程图
记录器
记录器有三种工作:
- 暴露函数给应用代码
- 基于记录器级别和记录器上的过滤器决定哪些日志有效
- 将日志信息传递到相应的处理器
如果提供了名字,getLogger()
返回对应名字的记录器示例的引用,否则,返回 root
。每个实例都有一个名字,并且它们使用以点号作为分隔符的命名空间等级制度。
import logging
foo = logging.getLogger('foo')
foo_bar = logging.getLogger('foo.bar')
foo_bar.parent.name == 'foo'
True
使用相同的名字多次调用 getLogger()
会返回同样的记录器对象:
logging.getLogger('foo') == foo
True
记录器有一个有效级别的概念。如果一个记录器没有设置级别,那么记录器使用它父亲的有效级别。如果父亲没有设置级别,父亲的父亲会被测试,以此类推 -- 所有的祖先都会被搜索直到获取一个级别。根记录器总是有一个级别(默认是 WARNING
)。只有大于等于有效级别的日志才会被放行。
孩子记录器会将信息传递给它父亲的处理器。所以没有必要为一个应用的所有记录器设置处理器。设置一个最高级别的记录器然后创建需要的孩子处理器就足够了。也可以通过设置记录器的 propagate
属性来关闭传播。
处理器
处理器负责分发日志到对应的目的地。记录器对象可以有 0 或多个处理器对象。比如,一个应用可能想要发送所有的日志到一个日志文件,所有错误或以上级别的日志到标准输出,所有 critical 的日志到邮件。这个场景需要三个单独的处理器,每个处理器负责发送特定级别的信息到特定的目的地。
import logging
logger = logging.getLogger('example')
logger.setLevel(logging.DEBUG)
file_handler = logging.FileHandler('message.log')
file_handler.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.ERROR)
email_handler = logging.handlers.SMTPHandler(('smtp.163.com', 255), '发件人名称', ['username@163.com', ], '主题', ('username@163.com', '授权码'))
email_handler.setLevel(logging.CRITICAL)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
logger.addHandler(email_handler)
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')
流程是
- 日志级别是否大于等于日志器级别,如果是,那么进入下一步,否则,停止
- 依次将
LogRecord
传递到file_handler
、console_handler
、email_handler
,如果日志级别大于等于记录器的日志级别,那么由记录器将其送达目的地。
由于记录器的级别设置为 DEBUG 级别,所以所有日志都能进入记录器。文件处理器的级别设置为 DEBUG 级别,所以所有日志都会记录到文件。控制台处理器的级别设置为 ERROR,所以只会显示 ERROR 及以上级别的日志。邮件处理器也是同理。
其他
抛出异常与记录日志
抛出异常:报告一个关于特定运行事件的错误
记录日志:报告错误但不抛出异常(比如在一个长期运行的服务进程的错误处理器)
什么时候记录日志,什么时候抛出异常?
Setry vs Logging
来自 https://sentry.io/vs/logging/
Logging 大部分是 info,之后少部分是 errors。Sentry 专注于异常 -- 捕捉应用的崩溃。Sentry 并不能代替 logs,并且 sentry 应该只储存错误或者崩溃。
Logging 想要捕捉你所有的数据,并且给你最大的可审核性。当 computing rollups and transformations,sentry 会丢失很多数据。比如,sentry 不会储存已存在错误的详细情况。这意味着 sentry 不能保证让你能精确地定位一个历史错误。因为这个原因,当你想要追踪事件时,你应该将它们储存在你的 logging 设施中。
单例模式
在 logging 中用到的有:
basicConfig
,只有第一次设置才会生效getLogger()
使用相同的name
会返回同样的实例
整个系统只需要一个对象进行操作。比如系统在任意时间都只需要一个 logger,如果有多个 logger 实例,那可能会出现日志重复等情况。
library logger
Configuring Logging for a Library 中认为应用的开发者知道他们的目标观众以及什么日志处理器最适合他们的应用:如果你在应用层之下添加了日志处理器,你很大程度上干涉了应用开发者分发适合他们需求的日志的能力。
通过给你的库增加 NullHandler
(自从 Python 3.1),你可以阻止你的库的日志输出。
import logging
logging.getLogger('foo').addHandler(logging.NullHandler())
当你自己需要在你的库中显示日志时,可以使用一个函数来临时使日志生效,比如 requests 中的
# env/lib/python2.7/site-packages/pip/_vendor/requests/packages/urllib3/__init__.py:57
def add_stderr_logger(level=logging.DEBUG):
"""
Helper for quickly adding a StreamHandler to the logger. Useful for
debugging.
Returns the handler after adding it.
"""
# This method needs to be in this __init__.py to get the __name__ correct
# even if urllib3 is vendored within another package.
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
logger.addHandler(handler)
logger.setLevel(level)
logger.debug('Added a stderr logging handler to logger: %s', __name__)
return handler
Logger.error or Logger.exception
Logger.exception() creates a log message similar to Logger.error(). The difference is that Logger.exception() dumps a stack trace along with it. Call this method only from an exception handler.
Exception handler 的意思是要在 except
中调用:
try:
1 / 0
except:
logging.exception('msg')
ERROR:root:msg
Traceback (most recent call last):
File "<ipython-input-9-63e73c36224b>", line 2, in <module>
1 / 0
ZeroDivisionError: integer division or modulo by zero
如果不在 except
中调用会 raise Empty
logging.exception('msg')
ERROR:root:msg
Traceback (most recent call last):
File "/Applications/PyCharm.app/Contents/helpers/pydev/pydevconsole.py", line 198, in process_exec_queue
code_fragment = interpreter.exec_queue.get(block=True, timeout=1/20.) # 20 calls/second
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/Queue.py", line 176, in get
raise Empty
Empty