Skip to content

Commit 9efb462

Browse files
kotyara1005Artem Krivonos
and
Artem Krivonos
authored
Add log level into binary messages for TEXT format (#111)
Co-authored-by: Artem Krivonos <[email protected]>
1 parent 6fbad51 commit 9efb462

File tree

3 files changed

+119
-29
lines changed

3 files changed

+119
-29
lines changed

awslambdaric/bootstrap.py

+31-8
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717
_DATETIME_FORMAT,
1818
_DEFAULT_FRAME_TYPE,
1919
_JSON_FRAME_TYPES,
20+
_TEXT_FRAME_TYPES,
2021
JsonFormatter,
2122
LogFormat,
23+
_format_log_level,
24+
_get_log_level_from_env_var,
2225
)
2326
from .lambda_runtime_marshaller import to_json
2427

2528
ERROR_LOG_LINE_TERMINATE = "\r"
2629
ERROR_LOG_IDENT = "\u00a0" # NO-BREAK SPACE U+00A0
2730
_AWS_LAMBDA_LOG_FORMAT = LogFormat.from_str(os.environ.get("AWS_LAMBDA_LOG_FORMAT"))
28-
_AWS_LAMBDA_LOG_LEVEL = os.environ.get("AWS_LAMBDA_LOG_LEVEL", "").upper()
31+
_AWS_LAMBDA_LOG_LEVEL = _get_log_level_from_env_var(
32+
os.environ.get("AWS_LAMBDA_LOG_LEVEL")
33+
)
2934

3035

3136
def _get_handler(handler):
@@ -122,7 +127,7 @@ def log_error(error_result, log_sink):
122127
)
123128

124129
else:
125-
_ERROR_FRAME_TYPE = _DEFAULT_FRAME_TYPE
130+
_ERROR_FRAME_TYPE = _TEXT_FRAME_TYPES[logging.ERROR]
126131

127132
def log_error(error_result, log_sink):
128133
error_description = "[ERROR]"
@@ -296,8 +301,22 @@ def __init__(self, log_sink):
296301

297302
def emit(self, record):
298303
msg = self.format(record)
304+
self.log_sink.log(msg)
305+
299306

300-
self.log_sink.log(msg, frame_type=getattr(record, "_frame_type", None))
307+
class LambdaLoggerHandlerWithFrameType(logging.Handler):
308+
def __init__(self, log_sink):
309+
super().__init__()
310+
self.log_sink = log_sink
311+
312+
def emit(self, record):
313+
self.log_sink.log(
314+
self.format(record),
315+
frame_type=(
316+
getattr(record, "_frame_type", None)
317+
or _TEXT_FRAME_TYPES.get(_format_log_level(record))
318+
),
319+
)
301320

302321

303322
class LambdaLoggerFilter(logging.Filter):
@@ -416,13 +435,14 @@ def create_log_sink():
416435
def _setup_logging(log_format, log_level, log_sink):
417436
logging.Formatter.converter = time.gmtime
418437
logger = logging.getLogger()
419-
logger_handler = LambdaLoggerHandler(log_sink)
438+
439+
if log_format == LogFormat.JSON or log_level:
440+
logger_handler = LambdaLoggerHandlerWithFrameType(log_sink)
441+
else:
442+
logger_handler = LambdaLoggerHandler(log_sink)
443+
420444
if log_format == LogFormat.JSON:
421445
logger_handler.setFormatter(JsonFormatter())
422-
423-
logging.addLevelName(logging.DEBUG, "TRACE")
424-
if log_level in logging._nameToLevel:
425-
logger.setLevel(log_level)
426446
else:
427447
logger_handler.setFormatter(
428448
logging.Formatter(
@@ -431,6 +451,9 @@ def _setup_logging(log_format, log_level, log_sink):
431451
)
432452
)
433453

454+
if log_level in logging._nameToLevel:
455+
logger.setLevel(log_level)
456+
434457
logger_handler.addFilter(LambdaLoggerFilter())
435458
logger.addHandler(logger_handler)
436459

awslambdaric/lambda_runtime_log_utils.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ def from_str(cls, value: str):
4545
return cls.TEXT.value
4646

4747

48+
def _get_log_level_from_env_var(log_level):
49+
return {None: "", "TRACE": "DEBUG"}.get(log_level, log_level).upper()
50+
51+
4852
_JSON_FRAME_TYPES = {
4953
logging.NOTSET: 0xA55A0002.to_bytes(4, "big"),
5054
logging.DEBUG: 0xA55A000A.to_bytes(4, "big"),
@@ -53,12 +57,24 @@ def from_str(cls, value: str):
5357
logging.ERROR: 0xA55A0016.to_bytes(4, "big"),
5458
logging.CRITICAL: 0xA55A001A.to_bytes(4, "big"),
5559
}
56-
_DEFAULT_FRAME_TYPE = 0xA55A0003.to_bytes(4, "big")
60+
_TEXT_FRAME_TYPES = {
61+
logging.NOTSET: 0xA55A0003.to_bytes(4, "big"),
62+
logging.DEBUG: 0xA55A000B.to_bytes(4, "big"),
63+
logging.INFO: 0xA55A000F.to_bytes(4, "big"),
64+
logging.WARNING: 0xA55A0013.to_bytes(4, "big"),
65+
logging.ERROR: 0xA55A0017.to_bytes(4, "big"),
66+
logging.CRITICAL: 0xA55A001B.to_bytes(4, "big"),
67+
}
68+
_DEFAULT_FRAME_TYPE = _TEXT_FRAME_TYPES[logging.NOTSET]
5769

5870
_json_encoder = json.JSONEncoder(ensure_ascii=False)
5971
_encode_json = _json_encoder.encode
6072

6173

74+
def _format_log_level(record: logging.LogRecord) -> int:
75+
return min(50, max(0, record.levelno)) // 10 * 10
76+
77+
6278
class JsonFormatter(logging.Formatter):
6379
def __init__(self):
6480
super().__init__(datefmt=_DATETIME_FORMAT)
@@ -90,13 +106,9 @@ def __format_location(record: logging.LogRecord):
90106

91107
return f"{record.pathname}:{record.funcName}:{record.lineno}"
92108

93-
@staticmethod
94-
def __format_log_level(record: logging.LogRecord):
95-
record.levelno = min(50, max(0, record.levelno)) // 10 * 10
96-
record.levelname = logging.getLevelName(record.levelno)
97-
98109
def format(self, record: logging.LogRecord) -> str:
99-
self.__format_log_level(record)
110+
record.levelno = _format_log_level(record)
111+
record.levelname = logging.getLevelName(record.levelno)
100112
record._frame_type = _JSON_FRAME_TYPES.get(
101113
record.levelno, _JSON_FRAME_TYPES[logging.NOTSET]
102114
)

tests/test_bootstrap.py

+69-14
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import awslambdaric.bootstrap as bootstrap
2020
from awslambdaric.lambda_runtime_exception import FaultException
21-
from awslambdaric.lambda_runtime_log_utils import LogFormat
21+
from awslambdaric.lambda_runtime_log_utils import LogFormat, _get_log_level_from_env_var
2222
from awslambdaric.lambda_runtime_marshaller import LambdaMarshaller
2323

2424

@@ -927,7 +927,7 @@ def test_log_error_framed_log_sink(self):
927927
content = f.read()
928928

929929
frame_type = int.from_bytes(content[:4], "big")
930-
self.assertEqual(frame_type, 0xA55A0003)
930+
self.assertEqual(frame_type, 0xA55A0017)
931931

932932
length = int.from_bytes(content[4:8], "big")
933933
self.assertEqual(length, len(expected_logged_error.encode("utf8")))
@@ -973,7 +973,7 @@ def test_log_error_indentation_framed_log_sink(self):
973973
content = f.read()
974974

975975
frame_type = int.from_bytes(content[:4], "big")
976-
self.assertEqual(frame_type, 0xA55A0003)
976+
self.assertEqual(frame_type, 0xA55A0017)
977977

978978
length = int.from_bytes(content[4:8], "big")
979979
self.assertEqual(length, len(expected_logged_error.encode("utf8")))
@@ -1016,7 +1016,7 @@ def test_log_error_empty_stacktrace_line_framed_log_sink(self):
10161016
content = f.read()
10171017

10181018
frame_type = int.from_bytes(content[:4], "big")
1019-
self.assertEqual(frame_type, 0xA55A0003)
1019+
self.assertEqual(frame_type, 0xA55A0017)
10201020

10211021
length = int.from_bytes(content[4:8], "big")
10221022
self.assertEqual(length, len(expected_logged_error))
@@ -1053,7 +1053,7 @@ def test_log_error_invokeId_line_framed_log_sink(self):
10531053
content = f.read()
10541054

10551055
frame_type = int.from_bytes(content[:4], "big")
1056-
self.assertEqual(frame_type, 0xA55A0003)
1056+
self.assertEqual(frame_type, 0xA55A0017)
10571057

10581058
length = int.from_bytes(content[4:8], "big")
10591059
self.assertEqual(length, len(expected_logged_error))
@@ -1179,14 +1179,13 @@ def test_log_level(self) -> None:
11791179
(LogFormat.JSON, "WARN", logging.WARNING),
11801180
(LogFormat.JSON, "ERROR", logging.ERROR),
11811181
(LogFormat.JSON, "FATAL", logging.CRITICAL),
1182-
# Log level is set only for Json format
1183-
(LogFormat.TEXT, "TRACE", logging.NOTSET),
1184-
(LogFormat.TEXT, "DEBUG", logging.NOTSET),
1185-
(LogFormat.TEXT, "INFO", logging.NOTSET),
1186-
(LogFormat.TEXT, "WARN", logging.NOTSET),
1187-
(LogFormat.TEXT, "ERROR", logging.NOTSET),
1188-
(LogFormat.TEXT, "FATAL", logging.NOTSET),
1189-
("Unknown format", "INFO", logging.NOTSET),
1182+
(LogFormat.TEXT, "TRACE", logging.DEBUG),
1183+
(LogFormat.TEXT, "DEBUG", logging.DEBUG),
1184+
(LogFormat.TEXT, "INFO", logging.INFO),
1185+
(LogFormat.TEXT, "WARN", logging.WARN),
1186+
(LogFormat.TEXT, "ERROR", logging.ERROR),
1187+
(LogFormat.TEXT, "FATAL", logging.CRITICAL),
1188+
("Unknown format", "INFO", logging.INFO),
11901189
# if level is unknown fall back to default
11911190
(LogFormat.JSON, "Unknown level", logging.NOTSET),
11921191
]
@@ -1196,11 +1195,67 @@ def test_log_level(self) -> None:
11961195
logging.getLogger().handlers.clear()
11971196
logging.getLogger().level = logging.NOTSET
11981197

1199-
bootstrap._setup_logging(fmt, log_level, bootstrap.StandardLogSink())
1198+
bootstrap._setup_logging(
1199+
fmt,
1200+
_get_log_level_from_env_var(log_level),
1201+
bootstrap.StandardLogSink(),
1202+
)
12001203

12011204
self.assertEqual(expected_level, logging.getLogger().level)
12021205

12031206

1207+
class TestLambdaLoggerHandlerSetup(unittest.TestCase):
1208+
@classmethod
1209+
def tearDownClass(cls):
1210+
importlib.reload(bootstrap)
1211+
logging.getLogger().handlers.clear()
1212+
logging.getLogger().level = logging.NOTSET
1213+
1214+
def test_handler_setup(self, *_):
1215+
test_cases = [
1216+
(62, 0xA55A0003, 46, {}),
1217+
(133, 0xA55A001A, 117, {"AWS_LAMBDA_LOG_FORMAT": "JSON"}),
1218+
(62, 0xA55A001B, 46, {"AWS_LAMBDA_LOG_LEVEL": "INFO"}),
1219+
]
1220+
1221+
for total_length, header, message_length, env_vars in test_cases:
1222+
with patch.dict(
1223+
os.environ, env_vars, clear=True
1224+
), NamedTemporaryFile() as temp_file:
1225+
importlib.reload(bootstrap)
1226+
logging.getLogger().handlers.clear()
1227+
logging.getLogger().level = logging.NOTSET
1228+
1229+
before = int(time.time_ns() / 1000)
1230+
with bootstrap.FramedTelemetryLogSink(
1231+
os.open(temp_file.name, os.O_CREAT | os.O_RDWR)
1232+
) as ls:
1233+
bootstrap._setup_logging(
1234+
bootstrap._AWS_LAMBDA_LOG_FORMAT,
1235+
bootstrap._AWS_LAMBDA_LOG_LEVEL,
1236+
ls,
1237+
)
1238+
logger = logging.getLogger()
1239+
logger.critical("critical")
1240+
after = int(time.time_ns() / 1000)
1241+
1242+
content = open(temp_file.name, "rb").read()
1243+
self.assertEqual(len(content), total_length)
1244+
1245+
pos = 0
1246+
frame_type = int.from_bytes(content[pos : pos + 4], "big")
1247+
self.assertEqual(frame_type, header)
1248+
pos += 4
1249+
1250+
length = int.from_bytes(content[pos : pos + 4], "big")
1251+
self.assertEqual(length, message_length)
1252+
pos += 4
1253+
1254+
timestamp = int.from_bytes(content[pos : pos + 8], "big")
1255+
self.assertTrue(before <= timestamp)
1256+
self.assertTrue(timestamp <= after)
1257+
1258+
12041259
class TestLogging(unittest.TestCase):
12051260
@classmethod
12061261
def setUpClass(cls) -> None:

0 commit comments

Comments
 (0)