Skip to content

Commit 114a670

Browse files
siwachabhiAbhimanyu Siwachclaude
authored
feat: implement AWS Lambda compatible JSON logging (#76)
* feat: implement AWS Lambda compatible JSON logging Replace RequestContextFormatter with AWS Lambda JSON format: - Add structured JSON output with timestamp, level, message, logger fields - Include requestId and sessionId when available - Add exception handling with errorType, errorMessage, stackTrace, location - Update exception logging to use logger.exception() for proper capture - Update tests to validate JSON format structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: eliminate duplicate exception logging in handler chain Change _invoke_handler exception logging from ERROR to DEBUG level to prevent duplicate logs when exceptions bubble up to _handle_invocation. The outer handler provides better context with timing information. Before: Two identical ERROR logs for same exception After: Single ERROR log with duration metrics 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: enable debug mode in integration test to resolve timeout Add debug=True to BedrockAgentCoreApp in test_simple_agent.py to enable verbose logging that will help identify the root cause of the 5-minute timeout in CI integration tests. This provides better visibility into server startup issues. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Abhimanyu Siwach <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent df70612 commit 114a670

File tree

3 files changed

+62
-28
lines changed

3 files changed

+62
-28
lines changed

src/bedrock_agentcore/runtime/app.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,34 @@ class RequestContextFormatter(logging.Formatter):
3737
"""Formatter including request and session IDs."""
3838

3939
def format(self, record):
40-
"""Format log record with request and session ID context."""
41-
request_id = BedrockAgentCoreContext.get_request_id()
42-
session_id = BedrockAgentCoreContext.get_session_id()
40+
"""Format log record as AWS Lambda JSON."""
41+
import json
42+
from datetime import datetime
4343

44-
parts = []
44+
log_entry = {
45+
"timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
46+
"level": record.levelname,
47+
"message": record.getMessage(),
48+
"logger": record.name,
49+
}
50+
51+
request_id = BedrockAgentCoreContext.get_request_id()
4552
if request_id:
46-
parts.append(f"[rid:{request_id}]")
53+
log_entry["requestId"] = request_id
54+
55+
session_id = BedrockAgentCoreContext.get_session_id()
4756
if session_id:
48-
parts.append(f"[sid:{session_id}]")
57+
log_entry["sessionId"] = session_id
58+
59+
if record.exc_info:
60+
import traceback
61+
62+
log_entry["errorType"] = record.exc_info[0].__name__
63+
log_entry["errorMessage"] = str(record.exc_info[1])
64+
log_entry["stackTrace"] = traceback.format_exception(*record.exc_info)
65+
log_entry["location"] = f"{record.pathname}:{record.funcName}:{record.lineno}"
4966

50-
record.request_id = " ".join(parts) + " " if parts else ""
51-
return super().format(record)
67+
return json.dumps(log_entry, ensure_ascii=False)
5268

5369

5470
class BedrockAgentCoreApp(Starlette):
@@ -78,7 +94,7 @@ def __init__(self, debug: bool = False, lifespan: Optional[Lifespan] = None):
7894
self.logger = logging.getLogger("bedrock_agentcore.app")
7995
if not self.logger.handlers:
8096
handler = logging.StreamHandler()
81-
formatter = RequestContextFormatter("%(asctime)s - %(name)s - %(levelname)s - %(request_id)s%(message)s")
97+
formatter = RequestContextFormatter()
8298
handler.setFormatter(formatter)
8399
self.logger.addHandler(handler)
84100
self.logger.setLevel(logging.DEBUG if self.debug else logging.INFO)
@@ -128,11 +144,9 @@ async def wrapper(*args, **kwargs):
128144
duration = time.time() - start_time
129145
self.logger.info("Async task completed: %s (%.3fs)", func.__name__, duration)
130146
return result
131-
except Exception as e:
147+
except Exception:
132148
duration = time.time() - start_time
133-
self.logger.error(
134-
"Async task failed: %s (%.3fs) - %s: %s", func.__name__, duration, type(e).__name__, e
135-
)
149+
self.logger.exception("Async task failed: %s (%.3fs)", func.__name__, duration)
136150
raise
137151
finally:
138152
self.complete_async_task(task_id)
@@ -333,8 +347,8 @@ def _handle_ping(self, request):
333347
status = self.get_current_ping_status()
334348
self.logger.debug("Ping request - status: %s", status.value)
335349
return JSONResponse({"status": status.value, "time_of_last_update": int(self._last_status_update_time)})
336-
except Exception as e:
337-
self.logger.error("Ping endpoint failed: %s: %s", type(e).__name__, e)
350+
except Exception:
351+
self.logger.exception("Ping endpoint failed")
338352
return JSONResponse({"status": PingStatus.HEALTHY.value, "time_of_last_update": int(time.time())})
339353

340354
def run(self, port: int = 8080, host: Optional[str] = None):
@@ -365,9 +379,9 @@ async def _invoke_handler(self, handler, request_context, takes_context, payload
365379
loop = asyncio.get_event_loop()
366380
ctx = contextvars.copy_context()
367381
return await loop.run_in_executor(None, ctx.run, handler, *args)
368-
except Exception as e:
382+
except Exception:
369383
handler_name = getattr(handler, "__name__", "unknown")
370-
self.logger.error("Handler '%s' execution failed: %s: %s", handler_name, type(e).__name__, e)
384+
self.logger.debug("Handler '%s' execution failed", handler_name)
371385
raise
372386

373387
def _handle_task_action(self, payload: dict) -> Optional[JSONResponse]:
@@ -413,7 +427,7 @@ def _handle_task_action(self, payload: dict) -> Optional[JSONResponse]:
413427
return JSONResponse({"error": f"Unknown action: {action}"}, status_code=400)
414428

415429
except Exception as e:
416-
self.logger.error("Debug action '%s' failed: %s: %s", action, type(e).__name__, e)
430+
self.logger.exception("Debug action '%s' failed", action)
417431
return JSONResponse({"error": "Debug action failed", "details": str(e)}, status_code=500)
418432

419433
async def _stream_with_error_handling(self, generator):
@@ -422,7 +436,7 @@ async def _stream_with_error_handling(self, generator):
422436
async for value in generator:
423437
yield self._convert_to_sse(value)
424438
except Exception as e:
425-
self.logger.error("Error in async streaming: %s: %s", type(e).__name__, e)
439+
self.logger.exception("Error in async streaming")
426440
error_event = {
427441
"error": str(e),
428442
"error_type": type(e).__name__,
@@ -477,7 +491,7 @@ def _sync_stream_with_error_handling(self, generator):
477491
for value in generator:
478492
yield self._convert_to_sse(value)
479493
except Exception as e:
480-
self.logger.error("Error in sync streaming: %s: %s", type(e).__name__, e)
494+
self.logger.exception("Error in sync streaming")
481495
error_event = {
482496
"error": str(e),
483497
"error_type": type(e).__name__,

tests/bedrock_agentcore/runtime/test_app.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,43 +1464,57 @@ class TestRequestContextFormatter:
14641464

14651465
def test_request_context_formatter_with_both_ids(self):
14661466
"""Test formatter with both request and session IDs."""
1467+
import json
14671468
import logging
14681469

14691470
from bedrock_agentcore.runtime.app import RequestContextFormatter
14701471
from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
14711472

1472-
formatter = RequestContextFormatter("%(request_id)s%(message)s")
1473+
formatter = RequestContextFormatter()
14731474

14741475
BedrockAgentCoreContext.set_request_context("req-123", "sess-456")
14751476
record = logging.LogRecord("test", logging.INFO, "", 1, "Test message", (), None)
14761477
formatted = formatter.format(record)
14771478

1478-
assert "[rid:req-123] [sid:sess-456] Test message" == formatted
1479+
log_data = json.loads(formatted)
1480+
assert log_data["message"] == "Test message"
1481+
assert log_data["level"] == "INFO"
1482+
assert log_data["logger"] == "test"
1483+
assert log_data["requestId"] == "req-123"
1484+
assert log_data["sessionId"] == "sess-456"
1485+
assert "timestamp" in log_data
14791486

14801487
def test_request_context_formatter_with_only_request_id(self):
14811488
"""Test formatter with only request ID."""
1489+
import json
14821490
import logging
14831491

14841492
from bedrock_agentcore.runtime.app import RequestContextFormatter
14851493
from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
14861494

1487-
formatter = RequestContextFormatter("%(request_id)s%(message)s")
1495+
formatter = RequestContextFormatter()
14881496

14891497
BedrockAgentCoreContext.set_request_context("req-789", None)
14901498
record = logging.LogRecord("test", logging.INFO, "", 1, "Test message", (), None)
14911499
formatted = formatter.format(record)
14921500

1493-
assert "[rid:req-789] Test message" == formatted
1494-
assert "[sid:" not in formatted
1501+
log_data = json.loads(formatted)
1502+
assert log_data["message"] == "Test message"
1503+
assert log_data["level"] == "INFO"
1504+
assert log_data["logger"] == "test"
1505+
assert log_data["requestId"] == "req-789"
1506+
assert "sessionId" not in log_data
1507+
assert "timestamp" in log_data
14951508

14961509
def test_request_context_formatter_with_no_ids(self):
14971510
"""Test formatter with no IDs set."""
14981511
import contextvars
1512+
import json
14991513
import logging
15001514

15011515
from bedrock_agentcore.runtime.app import RequestContextFormatter
15021516

1503-
formatter = RequestContextFormatter("%(request_id)s%(message)s")
1517+
formatter = RequestContextFormatter()
15041518

15051519
# Run in fresh context to ensure no IDs are set
15061520
ctx = contextvars.Context()
@@ -1510,4 +1524,10 @@ def format_in_new_context():
15101524
return formatter.format(record)
15111525

15121526
formatted = ctx.run(format_in_new_context)
1513-
assert formatted == "Test message"
1527+
log_data = json.loads(formatted)
1528+
assert log_data["message"] == "Test message"
1529+
assert log_data["level"] == "INFO"
1530+
assert log_data["logger"] == "test"
1531+
assert "requestId" not in log_data
1532+
assert "sessionId" not in log_data
1533+
assert "timestamp" in log_data

tests_integ/runtime/test_simple_agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def setup(self):
1515
from bedrock_agentcore import BedrockAgentCoreApp
1616
from strands import Agent
1717
18-
app = BedrockAgentCoreApp()
18+
app = BedrockAgentCoreApp(debug=True)
1919
agent = Agent()
2020
2121
@app.entrypoint

0 commit comments

Comments
 (0)