Getting Started With Structlog

Why

  • 更简单的日志:不再是散乱的记录而是以发生在上下文中的事件为依据。
  • 数据绑定:在 structlog 中 log 记录其实是字典,所以可以绑定和重绑定键值对到你的 loggers 中。
  • 强大的管道功能:提供简单而且强大的数据处理能力。
  • 格式化:本地开发时的键值对形式高亮显示;容易解析的 JSON 格式;或者其他的一些格式。
  • 输出:内置高亮打印;与标准库灵活结合;无需格式化为 string。

Installation

可以通过 pip 命令快速地安装 structlog:

1
$ pip install structlog

如果想要获得彩色的输出效果,使用以下命令安装:

1
$ pip install structlog colorama

First Log Entry

一个最简单的用例如下:

1
2
3
4
>>> import structlog
>>> log = structlog.get_logger()
>>> log.msg("greeted", whom="world", more_than_a_string=[1, 2, 3])
2016-09-17 10:13.45 greeted more_than_a_string=[1, 2, 3] whom='world'

上述例子可以充分展现 structlog 的优点:

  • 输出是标准化的,而不是直接抛出到用户面前。
  • 所有的 keyword 都使用了 structlog.dev.ConsoleRenderer 格式化。反之,使用 repr() 去把所有的 value 序列化成 strings。因此,很容易给日志添加对自定义对象的支持。

Building a Context

假设有一个 web 应用想要使用上面的 API 打印出相关的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from structlog import get_logger

log = get_logger()


def view(request):
user_agent = request.get("HTTP_USER_AGENT", "UNKNOWN")
peer_ip = request.client_addr
if something:
log.msg("something", user_agent=user_agent, peer_ip=peer_ip)
return "something"
elif something_else:
log.msg("something_else", user_agent=user_agent, peer_ip=peer_ip)
return "something_else"
else:
log.msg("else", user_agent=user_agent, peer_ip=peer_ip)
return "else"

这些调用本身是漂亮而且直至目的的,但是每一次调用都在写重复的代码(参数 user_agent, peer_ip)。为了减少重复代码,你可能想到如下的闭包:

1
2
def log_closure(event):
log.msg(event, user_agent=user_agent, peer_ip=peer_ip)

在 view() 中添加了该函数后问题是否解决了呢?如果参数是逐步增加的呢?你又是否真的想要在你的每一个 view() 中都包含一个闭包?

structlog 提供了一个更好的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from structlog import get_logger

logger = get_logger()


def view(request):
log = logger.bind(
user_agent=request.get("HTTP_USER_AGENT", "UNKNOWN"),
peer_ip=request.client_addr,
)
foo = request.get("foo")
if foo:
log = log.bind(foo=foo)
if something:
log.msg("something")
return "something"
elif something_else:
log.msg("something_else")
return "something_else"
else:
log.msg("else")
return "else"

对 structlog 来说,一条 log 记录就是一个称为 event dict[ionary] 的字典:

  • 你可以预建这个字典的一部分。这些预先保存的 value 称为 context。
  • 在打印一个 event 时,它会与 context 合并然后打印出。
  • 如果你不喜欢预建 context 的概念,不去使用就可以了。基础的键值对 logging 也足够的方便。
  • 推荐的使用值绑定(binding values)方式:为每一个新的 context 创建一个新的 logger。

Manipulating Log Entries in Flight

由于在 structlog 中 log event 是字典,比起简单的 string 也更容易操作。

structlog 中有 processor chains 的概念。一个 processor 就像一个接受 event 字典和两个其他参数然后返回一个新的 event 字典的函数。链(chain)中的下一个 processor 接受这个返回的字典而不是原始的那个字典。

假如你想要在每个 event dict 中添加一个时间戳。它的 processor 如下:

1
2
3
4
>>> import datetime
>>> def timestamper(_, __, event_dict):
... event_dict["time"] = datetime.datetime.now().isoformat()
... return event_dict

接下来你需要配置它:

1
2
3
>>> structlog.configure(processors=[timestamper, structlog.processors.KeyValueRenderer()])
>>> structlog.get_logger().msg("hi")
event='hi' time='2018-01-21T09:37:36.976816'

Rendering

和前面章节你注意到的一样,renderer 也是 processor。值得注意的是,你不一定总是要把 event 字典转换为 string。

假设你想要遵循最佳实践,把 event 字典转换成 JSON。structlog 提供内置的 JSONRenderer:

1
2
3
>>> structlog.configure(processors=[structlog.processors.JSONRenderer()])
>>> structlog.get_logger().msg("hi")
{"event": "hi"}

structlog and Standard Library’s logging

structlog 的应用目的不是打印日志,而是为了包装原本已存在的 loggers 并且添加结构和增量上下文(incremental context building)到其中。所以你可以在任何你喜欢的 logger 中使用 structlog。

原本已存在的 logger 最典型的例子无疑是标准库的 logging 模块。structlog 也提供了一些工具去使得这个最普遍的例子尽量简单:

1
2
3
4
5
6
7
>>> import logging
>>> logging.basicConfig()
>>> from structlog.stdlib import LoggerFactory
>>> structlog.configure(logger_factory=LoggerFactory())
>>> log = structlog.get_logger()
>>> log.warning("it works!", difficulty="easy")
WARNING:structlog...:difficulty='easy' event='it works!'

换句话说,你告诉 structlog 你将使用标准库 logger factory,然后像之前一样调用 get_logger() 。