Skip to content

Commit ea45743

Browse files
Added api gateway request id, moved request id logging to app.py (#252)
1 parent a45b390 commit ea45743

5 files changed

Lines changed: 101 additions & 47 deletions

File tree

src/eligibility_signposting_api/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from eligibility_signposting_api.common.error_handler import handle_exception
1212
from eligibility_signposting_api.common.request_validator import validate_request_params
1313
from eligibility_signposting_api.config.config import config
14+
from eligibility_signposting_api.logging.logs_helper import log_request_ids
1415
from eligibility_signposting_api.logging.logs_manager import add_request_id_to_logs, init_logging
1516
from eligibility_signposting_api.views import eligibility_blueprint
1617

@@ -25,6 +26,7 @@ def main() -> None: # pragma: no cover
2526

2627

2728
@add_request_id_to_logs()
29+
@log_request_ids()
2830
@validate_request_params()
2931
def lambda_handler(event: LambdaEvent, context: LambdaContext) -> dict[str, Any]: # pragma: no cover
3032
"""Run the Flask app as an AWS Lambda."""
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import logging
2+
from collections.abc import Callable
3+
from functools import wraps
4+
from typing import Any
5+
6+
from mangum.types import LambdaContext, LambdaEvent
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def log_request_ids() -> Callable:
12+
def decorator(func: Callable) -> Callable:
13+
@wraps(func)
14+
def wrapper(event: LambdaEvent, context: LambdaContext) -> dict[str, Any] | None:
15+
gateway_request_id = event.get("requestContext", {}).get("requestId")
16+
headers = event.get("headers", {})
17+
logger.info(
18+
"request trace metadata",
19+
extra={
20+
"x_request_id": headers.get("X-Request-ID"),
21+
"x_correlation_id": headers.get("X-Correlation-ID"),
22+
"gateway_request_id": gateway_request_id,
23+
},
24+
)
25+
return func(event, context)
26+
27+
return wrapper
28+
29+
return decorator

src/eligibility_signposting_api/views/eligibility.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,6 @@
2929

3030
@eligibility_blueprint.before_request
3131
def before_request() -> None:
32-
logger.info(
33-
"request details",
34-
extra={
35-
"X-Request-ID": request.headers.get("X-Request-ID"),
36-
"X-Correlation-ID": request.headers.get("X-Correlation-ID"),
37-
},
38-
)
3932
AuditContext.add_request_details(request)
4033

4134

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import logging
2+
from http import HTTPStatus
3+
from unittest.mock import Mock
4+
5+
import pytest
6+
from mangum.types import LambdaContext
7+
8+
9+
@pytest.fixture
10+
def lambda_context():
11+
context = Mock(spec=LambdaContext)
12+
context.aws_request_id = "test-request-id"
13+
return context
14+
15+
16+
@pytest.mark.parametrize(
17+
("headers", "gateway_request_id", "expected_extra"),
18+
[
19+
(
20+
{"X-Request-ID": "req-123", "X-Correlation-ID": "corr-abc"},
21+
"gw-id-999",
22+
{
23+
"x_request_id": "req-123",
24+
"x_correlation_id": "corr-abc",
25+
"gateway_request_id": "gw-id-999",
26+
},
27+
),
28+
(
29+
{}, # No headers
30+
"gw-id-000",
31+
{
32+
"x_request_id": None,
33+
"x_correlation_id": None,
34+
"gateway_request_id": "gw-id-000",
35+
},
36+
),
37+
(
38+
{"X-Request-ID": "req-local"},
39+
None, # No requestContext (non-Gateway trigger)
40+
{
41+
"x_request_id": "req-local",
42+
"x_correlation_id": None,
43+
"gateway_request_id": None,
44+
},
45+
),
46+
],
47+
)
48+
def test_log_request_ids_decorator_logs_metadata(headers, gateway_request_id, expected_extra, lambda_context, caplog):
49+
from eligibility_signposting_api.app import log_request_ids
50+
51+
event = {"headers": headers}
52+
if gateway_request_id is not None:
53+
event["requestContext"] = {"requestId": gateway_request_id}
54+
55+
@log_request_ids()
56+
def test_handler(event, context): # noqa : ARG001
57+
logger = logging.getLogger("test_logger")
58+
logger.info("Inside test handler")
59+
return HTTPStatus.OK
60+
61+
with caplog.at_level(logging.INFO):
62+
test_handler(event, lambda_context)
63+
64+
for record in caplog.records:
65+
if record.message == "request trace metadata":
66+
for key, val in expected_extra.items():
67+
assert getattr(record, key) == val
68+
break
69+
else:
70+
pytest.fail("'request trace metadata' log not found")

tests/unit/views/test_eligibility.py

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -586,46 +586,6 @@ def test_build_response_include_values_that_are_not_null(client: FlaskClient):
586586
assert action["urlLabel"] == "GP contact"
587587

588588

589-
@pytest.mark.parametrize(
590-
("headers", "expected_request_id"),
591-
[
592-
({"X-Request-ID": "test-request-id-123"}, "test-request-id-123"),
593-
(
594-
{"X-Request-ID": ""},
595-
"",
596-
),
597-
(
598-
{}, # No headers provided
599-
None,
600-
),
601-
],
602-
)
603-
def test_request_id_from_header_logging_variants(
604-
app: Flask, client: FlaskClient, caplog, headers: dict[str, str], expected_request_id: str
605-
):
606-
"""
607-
This test checks that the x-request-ID is logged so that it can be used to correlate logs
608-
with that of the logs from api-gateway
609-
"""
610-
with (
611-
get_app_container(app).override.service(EligibilityService, new=FakeEligibilityService()),
612-
get_app_container(app).override.service(AuditService, new=FakeAuditService()),
613-
):
614-
with caplog.at_level(logging.INFO):
615-
response = client.get("/patient-check/12345", headers=headers)
616-
617-
request_id_logged = False
618-
for record in caplog.records:
619-
request_id = getattr(record, "X-Request-ID", None)
620-
621-
if request_id == expected_request_id:
622-
request_id_logged = True
623-
break
624-
625-
assert request_id_logged
626-
assert response.status_code == HTTPStatus.OK
627-
628-
629589
def test_get_or_default_query_params_with_no_args(app: Flask):
630590
with app.test_request_context("/patient-check"):
631591
result = get_or_default_query_params()

0 commit comments

Comments
 (0)