捕获 structlog 中的所有 stdout/stderr 以生成 JSON 日志

Capture all stdout/stderr within structlog to generate JSON logs

我目前正在尝试摆脱 print() 的束缚,开始使用集中式日志 collection,使用 ELK 堆栈和 structlog 模块生成结构化 json 日志行。这对于我使用 loggingHelper 模块自己编写的模块来说工作得很好,我可以导入并使用

logger = Logger()

在其他模块和脚本中。这是 loggingHelper 模块 class:

class Logger:
    """
    Wrapper Class to import within other modules and scripts
    All the config and log binding (script
    """
    def __init__(self):
        self.__log = None
        logging.basicConfig(level=logging.DEBUG, format='%(message)s')
        structlog.configure(logger_factory=LoggerFactory(),
                            processors=[structlog.stdlib.add_log_level,
                            structlog.processors.TimeStamper(fmt="iso"),
                            structlog.processors.JSONRenderer()])
        logger = structlog.get_logger()
        main_script = os.path.basename(sys.argv[0]) if sys.argv[0] else None
        frame = inspect.stack()[1]
        log_invocation = os.path.basename(frame[0].f_code.co_filename)
        user = getpass.getuser()

        """
        Who executed the __main__, what was the executed __main__ file, 
        where did the log event happen?
        """
        self.__log = logger.bind(executedScript = main_script,
                                 logBirth = log_invocation,
                                 executingUser = user)

    def info(self, msg, **kwargs):
        self.__log.info(msg, **kwargs)

    def debug(self, msg, **kwargs):
        self.__log.debug(msg, **kwargs)

    def error(self, msg, **kwargs):
        self.__log.error(msg, **kwargs)

    def warn(self, msg, **kwargs):
        self.__log.warning(msg, **kwargs)

这会生成格式良好的输出(每行一个 JSON),filebeat 能够读取并转发到 Elasticsearch。 但是,third-party 图书馆员完全粉碎了 well-formatted 日志。

{"executingUser": "xyz", "logBirth": "efood.py", "executedScript": "logAlot.py", "context": "SELECT displayname FROM point_of_sale WHERE name = '123'", "level": "debug", "timestamp": "2019-03-15T12:52:42.792398Z", "message": "querying local"}
{"executingUser": "xyz", "logBirth": "efood.py", "executedScript": "logAlot.py", "level": "debug", "timestamp": "2019-03-15T12:52:42.807922Z", "message": "query successful: got 0 rows"}
building service object
auth version used is: v4
Traceback (most recent call last):
  File "logAlot.py", line 26, in <module>
    ef.EfoodDataControllerMerchantCenter().get_displayname(123)
  File "/home/xyz/src/toolkit/commons/connectors/efood.py", line 1126, in get_displayname
    return efc.select_from_local(q)['displayname'].values[0]
IndexError: index 0 is out of bounds for axis 0 with size 0

如您所见,来自第三方库 (googleapiclient) 的信息级别和错误级别消息均在未经过日志记录处理器的情况下打印出来。

使用我编写的 loggingHelper 模块捕获和格式化在一个脚本执行过程中发生的一切的最佳方式(也是最 pythonic 的方式)是什么?这甚至是最佳实践吗?

编辑:目前,记录器确实会写入 stdout 本身,然后使用 >> 和 2>&1 将其重定向到 crontab 中的文件。如果我想重定向由 third-party 库日志记录写入 stdout/stderr 的所有内容,这对我来说是不好的做法,因为这会导致循环,对吗?因此,我的目标不是重定向,而是捕获日志处理器中的所有内容。相应地更改了标题。

此外,这里是我要实现的目标的粗略概述。我对与此不同的一般批评和建议持开放态度。

第一件事:你不应该在你的 class 初始化程序中做任何记录器配置(logging.basicConfiglogging.dictConfig 等)- 日志记录配置应该完成一次 并且只有一次 在进程启动时。 logging 模块的重点是完全解耦日志记录调用

第二点:我不是 structlog 专家(这是一种轻描淡写的说法 - 这实际上是我第一次听说这个包)但是你得到的结果是你的代码所期望的片段:只有您自己的代码使用 structlog,所有其他库(stdlib 或第 3 部分)仍将使用 stdlib 记录器并发出纯文本日志。

根据我在 structlog 文档中看到的内容,它似乎提供了一些方法 wrap the stdlib's loggers using the structlog.stdlib.LoggerFactory and add specific formatters to have a more consistant output. I have not tested this (yet) and the official doc is a bit sparse and lacking usable practical example (at least I couldn't find any) but this article 似乎有一个更明确的示例(以适应您自己的上下文和用例课程)。

CAVEAT :正如我所说我从未使用过 structlog (我第一次听说这个库)所以我可能误解了一些东西,你当然会必须尝试找出如何正确配置整个东西以使其按预期工作。

附带说明:在类 unix 系统中 stdout 应该用于程序的输出(我的意思是 "expected output" => 程序的实际 结果),而所有错误/报告/调试消息都属于 stderr。除非你有令人信服的理由不这样做,否则你应该尝试并坚持这个约定(至少对于命令行工具,这样你就可以以 unix 方式链接/管道化它们)。

正在配置 logging 模块

As you already figured out, structlog requires configuration of the logging functionality already existing in python.

http://www.structlog.org/en/stable/standard-library.html

logging.basicConfig 支持 streamfilename 选项

https://docs.python.org/3/library/logging.html#logging.basicConfig.

要么您指定一个文件名,记录器将创建一个句柄并指向其所有输出。根据您的设置方式,这可能是您通常重定向到的文件

import logging

logging.basicConfig(level=logging.DEBUG, format='%(message)s', filename='output.txt')

或者您可以将 StringIO 对象传递给构建器,稍后您可以从中读取该对象,然后重定向到您希望的输出目的地

import logging
import io

stream = io.StringIO()

logging.basicConfig(level=logging.DEBUG, format='%(message)s', stream=stream)

More about StringIO can be read here

https://docs.python.org/3/library/io.html#io.TextIOBase

正如 @bruno 在他的回答中指出的那样,不要在 __init__ 中执行此操作,因为您最终可能会在同一段代码中多次调用这段代码过程。