Получение журналов дважды в лямбда-функции AWS
Я пытаюсь создать централизованный модуль для настройки моего средства форматирования журналов, которое будет совместно использоваться несколькими модулями Python в моей функции лямбда-выражения. В конечном итоге эта функция будет работать на AWS Greengrass на локальном локальном устройстве.
По какой-то причине, когда я добавляю свой собственный обработчик для форматирования сообщений, журналы выводятся дважды - один раз на правильном уровне журнала и второй раз на неправильном уровне.
Если я использую стандартный Python Logger без настройки каких-либо обработчиков, он работает нормально, например
main.py
:
import logging
logging.debug("test1")
cloudwatch logs
:
12:28:42 [DEBUG]-main.py:38,test1
Моя цель - иметь один кодировщик в моем коде, который будет форматировать эти сообщения журнала в 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()
Когда лямбда-функция теперь запущена, я получаю отладочное сообщение дважды в журналах наблюдения за облаком следующим образом:
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. Это может пролить некоторый свет на происходящее.
Рекомендации:
Настройка глобального форматера: Как определить логгер в python один раз для всей программы?
Очистка обработчиков лямбда-журналов по умолчанию: использование журналирования python с AWS Lambda
Создание глобального регистратора: Python: модуль журналирования - глобально
2 ответа
AWS Lambda также настраивает обработчик для корневого регистратора и всего, что записано в stdout
захватывается и регистрируется как уровень INFO
, Таким образом, ваше сообщение регистрируется дважды:
- С помощью обработчика AWS Lambda на корневом регистраторе (поскольку сообщения журнала распространяются от вложенных дочерних регистраторов к корню), и этот регистратор имеет собственный настроенный формат.
- Регистратором AWS Lambda stdout-to-INFO.
Вот почему все сообщения начинаются с (asctime) [(levelname)]-(module):(lineno),
Информация; root logger сконфигурирован для вывода сообщений в этом формате, а информация, записанная в stdout, является просто еще одной %(message)
участие в этом выходе.
Просто не устанавливайте обработчик, когда вы находитесь в среде AWS, или отключите распространение вывода на корневой обработчик и живите так, чтобы все ваши сообщения записывались как INFO
сообщения от AWS; в последнем случае ваш собственный форматер может включать levelname
Уровень информации в выводе.
Вы можете отключить распространение журнала с помощью logger.propagate = False
в этот момент ваше сообщение будет передано только вашему обработчику, а не корневому обработчику.
Другой вариант - просто положиться на конфигурацию корневого регистратора AWS. Согласно этому превосходному посту обратного инжиниринга, корневой логгер настроен на
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)
Это заменяет time.localtime
конвертер включен logging.Formatter
с time.gmtime
(поэтому временные метки используют UTC, а не locatime), задает пользовательский обработчик, который гарантирует, что сообщения отправляются в инфраструктуру 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 вполне может рассчитывать на использование определенного формата. Вывод, который вы показываете в своем вопросе, не включает компонент даты (%Y-%m-%dT
часть formatter.datefmt
строка), что, вероятно, означает, что формат был проанализирован и представлен вам в представлении данных веб-приложения.
Я не уверен, является ли это причиной вашей проблемы, но по умолчанию регистраторы 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'))
Если вы отправляете сообщение журнала в регистраторе, и оно не сбрасывается на основании минимального уровня регистраторов, то сообщение передается обработчикам регистратора и его родительскому регистратору. Смотрите эту блок-схему для получения дополнительной информации.
Если у этого родительского регистратора (или любого другого регистратора выше в иерархии) также есть обработчики, то они также вызываются.
Я подозреваю, что в вашем случае, либо корневой регистратор или main
logger каким-то образом заканчивается подключением некоторых обработчиков, что приводит к дублированию сообщений. Чтобы избежать этого, вы можете установить propagate
в вашем логгере False
или только прикрепите ваши обработчики к корневому логгеру.