Skip to content

Commit ca438f8

Browse files
author
Artem Krivonos
committed
Add tests for structured logging
1 parent 9d69f32 commit ca438f8

File tree

3 files changed

+145
-21
lines changed

3 files changed

+145
-21
lines changed

awslambdaric/bootstrap.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from .lambda_context import LambdaContext
1414
from .lambda_runtime_client import LambdaRuntimeClient
1515
from .lambda_runtime_exception import FaultException
16+
from .lambda_runtime_log_utils import INVOCATION_LOGGING_CONTEXT, JsonFormatter
1617
from .lambda_runtime_marshaller import to_json
17-
from .lambda_runtime_log_utils import JsonFormatter, INSTANCE_LOGGING_CONTEXT
1818

1919
ERROR_LOG_LINE_TERMINATE = "\r"
2020
ERROR_LOG_IDENT = "\u00a0" # NO-BREAK SPACE U+00A0
@@ -372,6 +372,25 @@ def create_log_sink():
372372
_GLOBAL_AWS_REQUEST_ID = None
373373

374374

375+
def setup_logging(log_format, log_sink):
376+
logging.Formatter.converter = time.gmtime
377+
logger = logging.getLogger()
378+
logger_handler = LambdaLoggerHandler(log_sink)
379+
if log_format == "JSON":
380+
logger_handler.setFormatter(JsonFormatter())
381+
logging._levelToName[logging.CRITICAL] = "FATAL"
382+
else:
383+
logger_handler.setFormatter(
384+
logging.Formatter(
385+
"[%(levelname)s]\t%(asctime)s.%(msecs)03dZ\t%(aws_request_id)s\t%(message)s\n",
386+
"%Y-%m-%dT%H:%M:%S",
387+
)
388+
)
389+
390+
logger_handler.addFilter(LambdaLoggerFilter())
391+
logger.addHandler(logger_handler)
392+
393+
375394
def run(app_root, handler, lambda_runtime_api_addr):
376395
sys.stdout = Unbuffered(sys.stdout)
377396
sys.stderr = Unbuffered(sys.stderr)
@@ -380,23 +399,7 @@ def run(app_root, handler, lambda_runtime_api_addr):
380399
lambda_runtime_client = LambdaRuntimeClient(lambda_runtime_api_addr)
381400

382401
try:
383-
logging.Formatter.converter = time.gmtime
384-
logger = logging.getLogger()
385-
logger_handler = LambdaLoggerHandler(log_sink)
386-
if AWS_LAMBDA_LOG_FORMAT == "JSON":
387-
logger_handler.setFormatter(JsonFormatter())
388-
logging._levelToName[logging.CRITICAL] = "FATAL"
389-
else:
390-
logger_handler.setFormatter(
391-
logging.Formatter(
392-
"[%(levelname)s]\t%(asctime)s.%(msecs)03dZ\t%(aws_request_id)s\t%(message)s\n",
393-
"%Y-%m-%dT%H:%M:%S",
394-
)
395-
)
396-
397-
logger_handler.addFilter(LambdaLoggerFilter())
398-
logger.addHandler(logger_handler)
399-
402+
setup_logging(AWS_LAMBDA_LOG_FORMAT, log_sink)
400403
global _GLOBAL_AWS_REQUEST_ID
401404

402405
request_handler = _get_handler(handler)
@@ -410,7 +413,7 @@ def run(app_root, handler, lambda_runtime_api_addr):
410413

411414
while True:
412415
event_request = lambda_runtime_client.wait_next_invocation()
413-
INSTANCE_LOGGING_CONTEXT.clear()
416+
INVOCATION_LOGGING_CONTEXT.clear()
414417

415418
_GLOBAL_AWS_REQUEST_ID = event_request.invoke_id
416419

awslambdaric/lambda_runtime_log_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from .lambda_runtime_marshaller import to_json
88

9-
109
RESERVED_FIELDS = {
1110
"name",
1211
"msg",
@@ -57,6 +56,9 @@ def format_exception(exc_info):
5756

5857
@staticmethod
5958
def format_location(record: logging.LogRecord):
59+
if not record.exc_info:
60+
return None
61+
6062
return f"{record.pathname}:{record.funcName}:{record.lineno}"
6163

6264
def format(self, record: logging.LogRecord) -> str:
@@ -81,7 +83,7 @@ def format(self, record: logging.LogRecord) -> str:
8183

8284
result = {k: v for k, v in result.items() if v is not None}
8385

84-
return to_json(result)
86+
return to_json(result) + "\n"
8587

8688

8789
INSTANCE_LOGGING_CONTEXT = dict()

tests/test_bootstrap.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import importlib
66
import json
7+
import logging
78
import os
89
import re
910
import tempfile
@@ -17,6 +18,12 @@
1718
import awslambdaric.bootstrap as bootstrap
1819
from awslambdaric.lambda_runtime_exception import FaultException
1920
from awslambdaric.lambda_runtime_marshaller import LambdaMarshaller
21+
from awslambdaric.lambda_runtime_log_utils import (
22+
lambda_instance_logs_update,
23+
lambda_invocation_logs_update,
24+
INVOCATION_LOGGING_CONTEXT,
25+
INSTANCE_LOGGING_CONTEXT,
26+
)
2027

2128

2229
class TestUpdateXrayEnv(unittest.TestCase):
@@ -1174,6 +1181,118 @@ def test_multiple_frame(self):
11741181
self.assertEqual(content[pos:], b"")
11751182

11761183

1184+
class TestLogging(unittest.TestCase):
1185+
@classmethod
1186+
def setUpClass(cls) -> None:
1187+
bootstrap.setup_logging("JSON", bootstrap.StandardLogSink())
1188+
1189+
def tearDown(self) -> None:
1190+
INVOCATION_LOGGING_CONTEXT.clear()
1191+
INSTANCE_LOGGING_CONTEXT.clear()
1192+
1193+
@patch("sys.stderr", new_callable=StringIO)
1194+
def test_json_formatter(self, mock_stderr):
1195+
logger = logging.getLogger("a.b")
1196+
1197+
test_cases = [
1198+
(
1199+
logging.ERROR,
1200+
"TEST 1",
1201+
{
1202+
"level": "ERROR",
1203+
"logger": "a.b",
1204+
"message": "TEST 1",
1205+
"requestId": "",
1206+
},
1207+
),
1208+
(
1209+
logging.ERROR,
1210+
"test \nwith \nnew \nlines",
1211+
{
1212+
"level": "ERROR",
1213+
"logger": "a.b",
1214+
"message": "test \nwith \nnew \nlines",
1215+
"requestId": "",
1216+
},
1217+
),
1218+
(
1219+
logging.CRITICAL,
1220+
"TEST FATAL",
1221+
{
1222+
"level": "FATAL",
1223+
"logger": "a.b",
1224+
"message": "TEST FATAL",
1225+
"requestId": "",
1226+
},
1227+
),
1228+
]
1229+
for level, msg, expected in test_cases:
1230+
with self.subTest(msg):
1231+
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
1232+
logger.log(level, msg)
1233+
1234+
data = json.loads(mock_stdout.getvalue())
1235+
data.pop("timestamp")
1236+
self.assertEqual(
1237+
data,
1238+
expected,
1239+
)
1240+
self.assertEqual(mock_stderr.getvalue(), "")
1241+
1242+
@patch("sys.stdout", new_callable=StringIO)
1243+
@patch("sys.stderr", new_callable=StringIO)
1244+
def test_exception(self, mock_stderr, mock_stdout):
1245+
try:
1246+
raise ValueError("error message")
1247+
except ValueError:
1248+
logging.getLogger("test.logger").exception("test exception")
1249+
1250+
exception_log = json.loads(mock_stdout.getvalue())
1251+
self.assertIn("location", exception_log)
1252+
self.assertIn("stackTrace", exception_log)
1253+
exception_log.pop("timestamp")
1254+
exception_log.pop("location")
1255+
exception_log.pop("stackTrace")
1256+
1257+
self.assertEqual(
1258+
exception_log,
1259+
{
1260+
"errorMessage": "error message",
1261+
"errorType": "ValueError",
1262+
"level": "ERROR",
1263+
"logger": "test.logger",
1264+
"message": "test exception",
1265+
"requestId": "",
1266+
},
1267+
)
1268+
1269+
self.assertEqual(mock_stderr.getvalue(), "")
1270+
1271+
@patch("sys.stdout", new_callable=StringIO)
1272+
@patch("sys.stderr", new_callable=StringIO)
1273+
def test_log_with_extra_params(self, mock_stderr, mock_stdout):
1274+
lambda_instance_logs_update("instance_key", "instance_value")
1275+
lambda_invocation_logs_update("invocation_key", "invocation_value")
1276+
1277+
logging.getLogger("test.logger").error("test extra params")
1278+
1279+
data = json.loads(mock_stdout.getvalue())
1280+
data.pop("timestamp")
1281+
1282+
self.assertEqual(
1283+
data,
1284+
{
1285+
"instance_key": "instance_value",
1286+
"invocation_key": "invocation_value",
1287+
"level": "ERROR",
1288+
"logger": "test.logger",
1289+
"message": "test extra params",
1290+
"requestId": "",
1291+
},
1292+
)
1293+
self.assertEqual(mock_stderr.getvalue(), "")
1294+
1295+
11771296
class TestBootstrapModule(unittest.TestCase):
11781297
@patch("awslambdaric.bootstrap.handle_event_request")
11791298
@patch("awslambdaric.bootstrap.LambdaRuntimeClient")

0 commit comments

Comments
 (0)