高度意见化的日志配置器
项目描述
围绕structlog和stdlib logger的高度意见化封装/配置。
仅适用于Python 3.7+。要使用contextvar,最低要求Python 3.7.1。
为什么
每个项目都始于日志配置的负担。我们希望交互式调试时具有颜色,在本地开发中将输出重定向到文件时使用纯文本,在生产环境中运行具有集中日志收集系统的JSON。最后,我喜欢structlog,但大多数库都没有使用它,因此我需要以兼容的方式配置这两个库。
这个库正是这样做的 - 按照上述描述配置日志。它对structlog和标准库日志都这样做。
有意见吗?
是的,因为它仅仅是将其他伟大人士编写的优秀工具配置为按照我个人的喜好行事。
例如,我更喜欢在JSON输出中不将structlog的键/值参数作为单独的属性渲染,因为我认为将它们作为文本消息的一部分读取更方便,即使在如Graylog这样的集中日志UI中也是如此 - 将它们作为单独的字段处理将需要我启用百万字段列,因为每个日志消息都有自己的上下文;而我使用的是指标,而不是日志,进行更广泛的分析。
用法
import uberlogging
uberlogging.configure()
这就是全部。你已经准备好了。只需导入structlog或stdlib logging,创建你的记录器并开始编写你的应用程序。
import structlog
logger = structlog.get_logger("main")
logger.info("Rocky road", to="Dublin")
定义环境变量UBERLOGGING_FORCE_TEXT=1以在非TTY流中强制文本输出。在将输出重定向到本地环境运行应用程序时很有用。
格式化
Structlog的上下文(传递给日志调用中的键/值对)以<key1>=<value1> <key2>=<value2>(或为空字符串)的形式呈现,并且可作为{context}格式化变量使用。如果非空,它将被填充4个空格(是的,它不是通用的,但我觉得默认配置下非常方便)。
如果您使用contextvars,它们将以类似的方式呈现,并可作为{contextvars}格式化变量使用。同样,它可以是单空格或4个空格填充,具体取决于当前日志记录是否存在非空的structlog上下文。请参阅下文关于contextvars的专用部分。
环境覆盖
有时人们想要以自己的方式处理事情,而这并不意味着改变实际的代码。为了解决这个问题,uberlogging提供通过环境变量来控制其配置的能力。
- UBERLOGGING_FORCE_TEXT
定义为非空值以强制文本(不是JSON)输出。颜色会自动检测。
- UBERLOGGING_FORCE_TEXT_COLOR
与上面相同,但颜色始终启用。
- UBERLOGGING_FORCE_TEXT_NO_COLOR
与上面相同,但颜色始终禁用。
- UBERLOGGING_MESSAGE_FORMAT
覆盖日志消息格式的字符串。例如:"{asctime} {levelname} {message}"。请注意,仅支持“{”样式。
上下文日志
Structlog的logger.bind(request_id="foo")非常适合简单的事情,但在有多层请求处理时,传递相同的绑定日志实例是a).麻烦的,并且b).要求所有处理请求的内容都使用相同的日志。
我一直缺少Python中的log4cxx 嵌套诊断上下文,现在有了contextvars,我们终于可以实现这一点。最好的部分是它在线程和asyncio代码中都能工作!
如果您从未听说过contextvars,请阅读官方的文档。简而言之,它“某种程度上”取代了线程局部存储,并在asyncio中得到本地支持,即它是线程安全的,也是并发安全的。
要在uberlogging中使用contextvars,您需要
在您的代码中某个地方创建一个contextvar
将此contextvar传递给uberlogging.configure()
设置contextvar值,每当您喜欢时,所有后续的日志消息都将将其值作为contextvar额外部分的一部分呈现
以下是一个示例
import asyncio
from contextvars import ContextVar
import structlog
import uberlogging
ctx_request_id: ContextVar = ContextVar("request_id")
logger = structlog.get_logger(__name__)
async def handle_request(request_id: str) -> None:
ctx_request_id.set(request_id)
logger.info("Handling request") # Will produce "Handling request request_id=<request_id>
async def server():
logger.info("Main server handling two requests")
t1 = asyncio.create_task(handle_request("Zf1glE"))
t2 = asyncio.create_task(handle_request("YcEf73"))
await asyncio.wait((t1, t2))
logger.info("Main server done")
if __name__ == "__main__":
uberlogging.configure(contextvars=(ctx_request_id,))
asyncio.run(server())
此代码将生成以下内容
2019-10-07T13:41:17.669 __main__ INFO ## Main server handling two requests ctx.server:17 2019-10-07T13:41:17.669 __main__ INFO ## Handling request request_id='Zf1glE' ctx.handle_request:13 2019-10-07T13:41:17.669 __main__ INFO ## Handling request request_id='YcEf73' ctx.handle_request:13 2019-10-07T13:41:17.669 __main__ INFO ## Main server done ctx.server:21
请注意,在请求处理器内部的日志调用中没有提及任何request_id - 它是由日志格式化程序从上下文注入的。
测试在哪里?
没有测试,只有最后期限:)不过,确实有一个demo.sh脚本,现在足够好了,因为这个库不会看到很多开发。
开发
echo 'layout pipenv' > .envrc
direnv allow # will take a while
make bootstrap