Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: Stringify non-serializable JSON log values and close init log sink
Address PR review feedback:
- Add default=str to the JSON log encoder so dict messages (and extra
  attributes) containing non-serializable values are stringified instead
  of raising and dropping the log record.
- Close the log sink opened by init_logging deterministically after the
  parent's handler is removed, instead of relying on GC.
  • Loading branch information
vip-amzn committed Sep 3, 2026
commit b9b6e2d38c3938e58a516460bf0a2de9416b3bd5
5 changes: 4 additions & 1 deletion awslambdaric/lambda_multi_concurrent_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def _emit_worker_pool_event(cls, socket_path: str, max_concurrency: int):
"""Emit worker pool DEBUG event once from the parent before forking."""
if socket_path:
Comment thread
vip-amzn marked this conversation as resolved.
Outdated
cls._redirect_output(socket_path)
bootstrap.init_logging()
log_sink = bootstrap.init_logging()
logging.getLogger().debug(
{
"event": WORKER_POOL_INITIALIZING_EVENT,
Expand All @@ -49,6 +49,9 @@ def _emit_worker_pool_event(cls, socket_path: str, max_concurrency: int):
}
)
logging.getLogger().handlers.clear()
# Close the sink deterministically now that its handler is gone
# (no-op for StandardLogSink; releases the fd for framed sinks).
log_sink.__exit__(None, None, None)

@classmethod
def run_concurrent(
Expand Down
5 changes: 4 additions & 1 deletion awslambdaric/lambda_runtime_log_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ def _get_log_level_from_env_var(log_level):
}
_DEFAULT_FRAME_TYPE = _TEXT_FRAME_TYPES[logging.NOTSET]

_json_encoder = json.JSONEncoder(ensure_ascii=False)
# default=str keeps formatting resilient: non-JSON-serializable values in
# dict messages or `extra` attributes are stringified instead of raising and
# dropping the whole log record.
_json_encoder = json.JSONEncoder(ensure_ascii=False, default=str)
_encode_json = _json_encoder.encode


Expand Down
26 changes: 26 additions & 0 deletions tests/test_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,32 @@ def test_init_logging_enables_parent_emission(self, mock_stdout):
self.assertEqual(data["level"], "DEBUG")
self.assertEqual(data["message"]["event"], "runtime_worker_pool_initializing")

@patch("sys.stdout", new_callable=StringIO)
def test_dict_message_with_non_serializable_values_is_not_dropped(
self, mock_stdout
):
import datetime
import decimal

self._setup_json_logging("DEBUG")

logging.getLogger().debug(
{
"event": "custom_event",
"when": datetime.datetime(2026, 9, 3, 12, 0, 0),
"amount": decimal.Decimal("1.5"),
"blob": b"bytes",
}
)

# The record must not be dropped: it serializes with values
# stringified via the encoder's default=str fallback.
data = json.loads(mock_stdout.getvalue())
self.assertEqual(data["message"]["event"], "custom_event")
self.assertEqual(data["message"]["when"], "2026-09-03 12:00:00")
self.assertEqual(data["message"]["amount"], "1.5")
self.assertEqual(data["message"]["blob"], "b'bytes'")


class TestBootstrapModule(unittest.TestCase):
def test_run(self):
Expand Down
4 changes: 4 additions & 0 deletions tests/test_multi_concurrent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ def test_emit_worker_pool_event_sets_up_parent_logging_and_emits(
self.assertEqual(event["workerCount"], 16)
self.assertEqual(event["executionEnvironmentMaxConcurrency"], 16)
mock_logging.getLogger.return_value.handlers.clear.assert_called_once_with()
# Sink is closed deterministically after the handler is removed.
mock_bootstrap.init_logging.return_value.__exit__.assert_called_once_with(
None, None, None
)

@patch("awslambdaric.lambda_multi_concurrent_utils.logging")
@patch("awslambdaric.lambda_multi_concurrent_utils.bootstrap")
Expand Down
Loading