diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml index 894397730..f70c7bc12 100644 --- a/api/openapi.generated.yml +++ b/api/openapi.generated.yml @@ -587,6 +587,10 @@ components: - object allOf: - $ref: '#/components/schemas/ValidationIssue' + internal_request_id: + type: string + description: An internal tracking ID + example: 550e8400-e29b-41d4-a716-446655440000 OpportunitySorting: type: object properties: diff --git a/api/src/api/response.py b/api/src/api/response.py index f85fc0f7c..52f4ebd22 100644 --- a/api/src/api/response.py +++ b/api/src/api/response.py @@ -3,6 +3,7 @@ from typing import Any, Optional, Tuple import apiflask +import flask from src.api.schemas.extension import MarshmallowErrorContainer from src.pagination.pagination_models import PaginationInfo @@ -92,6 +93,7 @@ def restructure_error_response(error: apiflask.exceptions.HTTPError) -> Tuple[di # we rename detail to data so success and error responses are consistent "data": error.detail, "status_code": error.status_code, + "internal_request_id": getattr(flask.g, "internal_request_id", None), } validation_errors: list[ValidationErrorDetail] = [] diff --git a/api/src/api/schemas/response_schema.py b/api/src/api/schemas/response_schema.py index 928ef19ab..3f267ec3b 100644 --- a/api/src/api/schemas/response_schema.py +++ b/api/src/api/schemas/response_schema.py @@ -52,3 +52,9 @@ class ErrorResponseSchema(Schema): errors = fields.List( fields.Nested(ValidationIssueSchema()), metadata={"example": []}, dump_default=[] ) + internal_request_id = fields.String( + metadata={ + "description": "An internal tracking ID", + "example": "550e8400-e29b-41d4-a716-446655440000", + } + ) diff --git a/api/src/logging/flask_logger.py b/api/src/logging/flask_logger.py index 4a848a48a..92dd005d6 100644 --- a/api/src/logging/flask_logger.py +++ b/api/src/logging/flask_logger.py @@ -154,14 +154,17 @@ def _add_global_context_info_to_log_record(record: logging.LogRecord) -> bool: def _get_request_context_info(request: flask.Request) -> dict: + internal_request_id = str(uuid.uuid4()) + flask.g.internal_request_id = internal_request_id + data = { "request.id": request.headers.get("x-amzn-requestid", ""), "request.method": request.method, "request.path": request.path, "request.url_rule": str(request.url_rule), - # A backup ID in case the x-amzn-requestid isn't passed in - # doesn't help with tracing across systems, but at least links within a request - "request.internal_id": str(uuid.uuid4()), + # This ID is used to group all logs for a given request + # and is returned in the API response for any 4xx/5xx scenarios + "request.internal_id": internal_request_id, } # Add query parameter data in the format request.query. = diff --git a/api/tests/src/api/test_healthcheck.py b/api/tests/src/api/test_healthcheck.py index 70e699b99..56029b0b2 100644 --- a/api/tests/src/api/test_healthcheck.py +++ b/api/tests/src/api/test_healthcheck.py @@ -18,3 +18,4 @@ def err_method(*args): response = client.get("/health") assert response.status_code == 503 assert response.get_json()["message"] == "Service Unavailable" + assert response.get_json()["internal_request_id"] is not None