在 AWS lambda 函数中获取日志两次

Getting logs twice in AWS lambda function

我正在尝试创建一个集中式模块来设置我的日志格式化程序,以便在我的 lambda 函数内的多个 python 模块之间共享。此功能最终将在本地本地设备上的 AWS Greengrass 上 运行。

出于某种原因,当我添加自己的处理程序来格式化消息时,日志被输出两次 - 一次在正确的日志级别,第二次在错误的级别。

如果我使用标准 python 记录器而不设置任何处理程序,它工作正常,例如

main.py:

import logging

logging.debug("test1")

cloudwatch logs:

12:28:42 [DEBUG]-main.py:38,test1

我的 objective 是在我的代码上有一个格式化程序,它将把这些日志消息格式化为 JSON。然后它们将被摄取到一个集中的日志数据库中。但是,当我这样做时,我收到了两次日志消息。

loghelper.py:

def setup_logging(name):

    formatter = logging.Formatter("%(name)s, %(asctime)s, %(message)s")

    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(formatter)

    logger = logging.getLogger(name)

    if logger.handlers:
        for handler in logger.handlers:
            logger.removeHandler(handler)

    logger.setLevel(logging.DEBUG)
    logger.addHandler(handler)
    return logger

main.py:

import logging

logger = loghelper.setup_logging('main.test_function')

def test_function():
    logger.debug("test function log statement")

test_function()

当 lambda 函数现在 运行 我在 cloud watch 日志中收到两次调试消息,如下所示:

cloudwatch logs:

12:22:53 [DEBUG]-main.py:5, test function log statement
12:22:53 [INFO]-__init__.py:880,main.test_function,2018-06-18 12:22:53,099, test function log statement

注意:

我无法解释这种行为,如有任何关于可能导致这种行为的想法,我将不胜感激。我也不知道第 880 行存在哪个构造函数。这可能会阐明正在发生的事情。

参考文献:

  1. 设置全局格式化程序:

  2. 清除默认的 lambda 日志处理程序:

  3. 创建全局记录器: Python: logging module - globally

我不确定这是否是您的问题的原因,但默认情况下,Python 的记录器将它们的消息向上传播到记录层次结构。您可能知道,Python 记录器以树的形式组织,root 记录器位于顶部,其他记录器位于其下方。在记录器名称中,一个点 (.) 引入了一个新的层次结构级别。所以如果你这样做

logger = logging.getLogger('some_module.some_function`)

那么你实际上有 3 个记录器:

The root logger (`logging.getLogger()`)
    A logger at module level (`logging.getLogger('some_module'))
        A logger at function level (`logging.getLogger('some_module.some_function'))

如果您在记录器上发出一条日志消息并且未根据记录器的最低级别丢弃该消息,则该消息将传递给记录器的处理程序并且传递给它的父级记录器。有关详细信息,请参阅 this flowchart

如果该父记录器(或层次结构中更高层的任何记录器)也有处理程序,那么它们也会被调用。

我怀疑在您的情况下,根记录器或 main 记录器以某种方式最终附加了一些处理程序,这导致了重复的消息。为避免这种情况,您可以将记录器中的 propagate 设置为 False 或仅将处理程序附加到根记录器。

AWS Lambda 还在根记录器上设置了一个处理程序, 任何写入 stdout 的内容都会被捕获并记录为级别 INFO。因此,您的日志消息被捕获了两次:

  • 通过根记录器上的 AWS Lambda 处理程序(作为日志消息 从嵌套子记录器传播 到根),并且此记录器配置了自己的格式。
  • 通过 AWS Lambda stdout-to-INFO 记录器。

这就是为什么消息都以(asctime) [(levelname)]-(module):(lineno),信息开头的原因;根记录器配置为输出具有该格式的消息,写入标准输出的信息只是该输出中的另一个 %(message) 部分。

在 AWS 环境中时不要设置处理程序,,禁止将输出传播到根处理程序,并允许所有消息被记录为INFO 条来自 AWS 的消息;在后一种情况下,您自己的格式化程序可以在输出中包含 levelname 级别信息。

您可以使用 logger.propagate = False 禁用日志传播,此时您的消息只会传递给 您的 处理程序,而不是根处理程序.

另一种选择是仅依赖 AWS 根记录器配置。根据 this excellent reverse engineering blog post 根记录器配置为:

logging.Formatter.converter = time.gmtime
logger = logging.getLogger()
logger_handler = LambdaLoggerHandler()
logger_handler.setFormatter(logging.Formatter(
    '[%(levelname)s]\t%(asctime)s.%(msecs)dZ\t%(aws_request_id)s\t%(message)s\n',
    '%Y-%m-%dT%H:%M:%S'
))
logger_handler.addFilter(LambdaLoggerFilter())
logger.addHandler(logger_handler)

这将 logging.Formatter 上的 time.localtime 转换器替换为 time.gmtime(因此时间戳使用 UTC 而不是本地时间),设置自定义处理程序以确保消息进入 Lambda 基础设施,配置格式,并添加一个过滤器对象,该对象仅向记录添加 aws_request_id 属性(因此上述格式化程序可以包含它)但实际上不过滤任何内容。

您可以通过更新 handler.formatter 对象上的属性来更改该处理程序上的格式化程序:

for handler in logging.getLogger().handlers:
    formatter = handler.formatter
    if formatter is not None and 'aws_request_id' in formatter._fmt:
        # this is the AWS Lambda formatter
        # formatter.datefmt => '%Y-%m-%dT%H:%M:%S'
        # formatter._style._fmt => 
        #    '[%(levelname)s]\t%(asctime)s.%(msecs)dZ'
        #    '\t%(aws_request_id)s\t%(message)s\n'

然后完全放弃您自己的记录器处理程序。你确实要小心这一点; AWS Lambda 基础设施很可能依赖于所使用的特定格式。您在问题中显示的输出不包括日期组件(formatter.datefmt 字符串的 %Y-%m-%dT 部分),这可能意味着格式已被解析并在网络中呈现给您数据的应用视图。