awslambdaric/lambda_runtime_log_utils.py (107 lines of code) (raw):
"""
Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
"""
import json
import logging
import traceback
from enum import IntEnum
_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
_RESERVED_FIELDS = {
"name",
"msg",
"args",
"levelname",
"levelno",
"pathname",
"filename",
"module",
"exc_info",
"exc_text",
"stack_info",
"lineno",
"funcName",
"created",
"msecs",
"relativeCreated",
"thread",
"threadName",
"processName",
"process",
"aws_request_id",
"_frame_type",
}
class LogFormat(IntEnum):
JSON = 0b0
TEXT = 0b1
@classmethod
def from_str(cls, value: str):
if value and value.upper() == "JSON":
return cls.JSON.value
return cls.TEXT.value
def _get_log_level_from_env_var(log_level):
return {None: "", "TRACE": "DEBUG"}.get(log_level, log_level).upper()
_JSON_FRAME_TYPES = {
logging.NOTSET: 0xA55A0002.to_bytes(4, "big"),
logging.DEBUG: 0xA55A000A.to_bytes(4, "big"),
logging.INFO: 0xA55A000E.to_bytes(4, "big"),
logging.WARNING: 0xA55A0012.to_bytes(4, "big"),
logging.ERROR: 0xA55A0016.to_bytes(4, "big"),
logging.CRITICAL: 0xA55A001A.to_bytes(4, "big"),
}
_TEXT_FRAME_TYPES = {
logging.NOTSET: 0xA55A0003.to_bytes(4, "big"),
logging.DEBUG: 0xA55A000B.to_bytes(4, "big"),
logging.INFO: 0xA55A000F.to_bytes(4, "big"),
logging.WARNING: 0xA55A0013.to_bytes(4, "big"),
logging.ERROR: 0xA55A0017.to_bytes(4, "big"),
logging.CRITICAL: 0xA55A001B.to_bytes(4, "big"),
}
_DEFAULT_FRAME_TYPE = _TEXT_FRAME_TYPES[logging.NOTSET]
_json_encoder = json.JSONEncoder(ensure_ascii=False)
_encode_json = _json_encoder.encode
def _format_log_level(record: logging.LogRecord) -> int:
return min(50, max(0, record.levelno)) // 10 * 10
class JsonFormatter(logging.Formatter):
def __init__(self):
super().__init__(datefmt=_DATETIME_FORMAT)
@staticmethod
def __format_stacktrace(exc_info):
if not exc_info:
return None
return traceback.format_tb(exc_info[2])
@staticmethod
def __format_exception_name(exc_info):
if not exc_info:
return None
return exc_info[0].__name__
@staticmethod
def __format_exception(exc_info):
if not exc_info:
return None
return str(exc_info[1])
@staticmethod
def __format_location(record: logging.LogRecord):
if not record.exc_info:
return None
return f"{record.pathname}:{record.funcName}:{record.lineno}"
def format(self, record: logging.LogRecord) -> str:
record.levelno = _format_log_level(record)
record.levelname = logging.getLevelName(record.levelno)
record._frame_type = _JSON_FRAME_TYPES.get(
record.levelno, _JSON_FRAME_TYPES[logging.NOTSET]
)
result = {
"timestamp": self.formatTime(record, self.datefmt),
"level": record.levelname,
"message": record.getMessage(),
"logger": record.name,
"stackTrace": self.__format_stacktrace(record.exc_info),
"errorType": self.__format_exception_name(record.exc_info),
"errorMessage": self.__format_exception(record.exc_info),
"requestId": getattr(record, "aws_request_id", None),
"location": self.__format_location(record),
}
result.update(
(key, value)
for key, value in record.__dict__.items()
if key not in _RESERVED_FIELDS and key not in result
)
result = {k: v for k, v in result.items() if v is not None}
return _encode_json(result) + "\n"