Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions infrastructure/modules/lambda/lambda.tf
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {
KINESIS_AUDIT_STREAM_TO_S3 = var.kinesis_audit_stream_to_s3_name
ENV = var.environment
LOG_LEVEL = var.log_level
ENABLE_XRAY_PATCHING = var.enable_xray_patching
}
}

Expand Down
5 changes: 5 additions & 0 deletions infrastructure/modules/lambda/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@ variable "log_level" {
description = "log level"
type = string
}

variable "enable_xray_patching"{
description = "flag to enable xray tracing, which puts an entry for dynamodb, s3 and firehose in trace map"
type = string
}
1 change: 1 addition & 0 deletions infrastructure/stacks/api-layer/lambda.tf
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ module "eligibility_signposting_lambda_function" {
eligibility_status_table_name = module.eligibility_status_table.table_name
kinesis_audit_stream_to_s3_name = module.eligibility_audit_firehose_delivery_stream.firehose_stream_name
log_level = "INFO"
enable_xray_patching = "true"
stack_name = local.stack_name
}
25 changes: 19 additions & 6 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ python-json-logger = "^3.3.0"
fhir-resources = "^8.0.0"
python-dateutil = "^2.9.0"
pyhamcrest = "^2.1.0"
aws-xray-sdk = "2.14.0"

[tool.poetry.group.dev.dependencies]
ruff = "^0.11.13"
Expand Down
15 changes: 11 additions & 4 deletions src/eligibility_signposting_api/app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import logging
import os
from typing import Any

import wireup.integration.flask
from asgiref.wsgi import WsgiToAsgi
from aws_xray_sdk.core import patch_all
from flask import Flask
from mangum import Mangum
from mangum.types import LambdaContext, LambdaEvent
Expand All @@ -11,10 +13,14 @@
from eligibility_signposting_api.common.error_handler import handle_exception
from eligibility_signposting_api.common.request_validator import validate_request_params
from eligibility_signposting_api.config.config import config
from eligibility_signposting_api.logging.logs_helper import log_request_ids
from eligibility_signposting_api.logging.logs_manager import add_request_id_to_logs, init_logging
from eligibility_signposting_api.logging.logs_helper import log_request_ids_from_headers
from eligibility_signposting_api.logging.logs_manager import add_lambda_request_id_to_logger, init_logging
from eligibility_signposting_api.logging.tracing_helper import tracing_setup
from eligibility_signposting_api.views import eligibility_blueprint

if os.getenv("ENABLE_XRAY_PATCHING"):
patch_all()

init_logging()
logger = logging.getLogger(__name__)

Expand All @@ -25,8 +31,9 @@ def main() -> None: # pragma: no cover
app.run(debug=config()["log_level"] == logging.DEBUG)


@add_request_id_to_logs()
@log_request_ids()
@add_lambda_request_id_to_logger()
@tracing_setup()
@log_request_ids_from_headers()
@validate_request_params()
def lambda_handler(event: LambdaEvent, context: LambdaContext) -> dict[str, Any]: # pragma: no cover
"""Run the Flask app as an AWS Lambda."""
Expand Down
10 changes: 7 additions & 3 deletions src/eligibility_signposting_api/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def config() -> dict[str, Any]:
rules_bucket_name = BucketName(os.getenv("RULES_BUCKET_NAME", "test-rules-bucket"))
audit_bucket_name = BucketName(os.getenv("AUDIT_BUCKET_NAME", "test-audit-bucket"))
aws_default_region = AwsRegion(os.getenv("AWS_DEFAULT_REGION", "eu-west-1"))
enable_xray_patching = bool(os.getenv("ENABLE_XRAY_PATCHING", "false"))
kinesis_audit_stream_to_s3 = AwsKinesisFirehoseStreamName(
os.getenv("KINESIS_AUDIT_STREAM_TO_S3", "test_kinesis_audit_stream_to_s3")
)
Expand All @@ -39,19 +40,22 @@ def config() -> dict[str, Any]:
"audit_bucket_name": audit_bucket_name,
"firehose_endpoint": None,
"kinesis_audit_stream_to_s3": kinesis_audit_stream_to_s3,
"enable_xray_patching": enable_xray_patching,
"log_level": log_level,
}

local_stack_endpoint = "http://localhost:4566"
return {
"aws_access_key_id": AwsAccessKey(os.getenv("AWS_ACCESS_KEY_ID", "dummy_key")),
"aws_default_region": aws_default_region,
"aws_secret_access_key": AwsSecretAccessKey(os.getenv("AWS_SECRET_ACCESS_KEY", "dummy_secret")),
"dynamodb_endpoint": URL(os.getenv("DYNAMODB_ENDPOINT", "http://localhost:4566")),
"dynamodb_endpoint": URL(os.getenv("DYNAMODB_ENDPOINT", local_stack_endpoint)),
"person_table_name": person_table_name,
"s3_endpoint": URL(os.getenv("S3_ENDPOINT", "http://localhost:4566")),
"s3_endpoint": URL(os.getenv("S3_ENDPOINT", local_stack_endpoint)),
"rules_bucket_name": rules_bucket_name,
"audit_bucket_name": audit_bucket_name,
"firehose_endpoint": URL(os.getenv("FIREHOSE_ENDPOINT", "http://localhost:4566")),
"firehose_endpoint": URL(os.getenv("FIREHOSE_ENDPOINT", local_stack_endpoint)),
"kinesis_audit_stream_to_s3": kinesis_audit_stream_to_s3,
"enable_xray_patching": enable_xray_patching,
"log_level": log_level,
}
6 changes: 3 additions & 3 deletions src/eligibility_signposting_api/logging/logs_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
logger = logging.getLogger(__name__)


def log_request_ids() -> Callable:
def log_request_ids_from_headers() -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(event: LambdaEvent, context: LambdaContext) -> dict[str, Any] | None:
gateway_request_id = event.get("requestContext", {}).get("requestId")
headers = event.get("headers", {})
gateway_request_id = (event.get("requestContext") or {}).get("requestId")
headers = event.get("headers") or {}
logger.info(
"request trace metadata",
extra={
Expand Down
2 changes: 1 addition & 1 deletion src/eligibility_signposting_api/logging/logs_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
LOG_FORMAT = "%(asctime)s %(levelname)-8s %(name)s %(module)s.py:%(funcName)s():%(lineno)d %(message)s"


def add_request_id_to_logs() -> Callable:
def add_lambda_request_id_to_logger() -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(event: LambdaEvent, context: LambdaContext) -> dict[str, Any] | None:
Expand Down
21 changes: 21 additions & 0 deletions src/eligibility_signposting_api/logging/tracing_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from collections.abc import Callable
from functools import wraps
from typing import Any

from aws_xray_sdk.core import xray_recorder
from mangum.types import LambdaContext, LambdaEvent


def tracing_setup() -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(event: LambdaEvent, context: LambdaContext) -> dict[str, Any] | None:
xray_recorder.begin_subsegment("Lambda")
try:
return func(event, context)
finally:
xray_recorder.end_subsegment()

return wrapper

return decorator
4 changes: 2 additions & 2 deletions tests/unit/logging/test_logs_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ def lambda_context():
],
)
def test_log_request_ids_decorator_logs_metadata(headers, gateway_request_id, expected_extra, lambda_context, caplog):
from eligibility_signposting_api.app import log_request_ids
from eligibility_signposting_api.app import log_request_ids_from_headers

event = {"headers": headers}
if gateway_request_id is not None:
event["requestContext"] = {"requestId": gateway_request_id}

@log_request_ids()
@log_request_ids_from_headers()
def test_handler(event, context): # noqa : ARG001
logger = logging.getLogger("test_logger")
logger.info("Inside test handler")
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/logging/test_logs_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from eligibility_signposting_api.logging.logs_manager import (
LOG_FORMAT,
EnrichedJsonFormatter,
add_request_id_to_logs,
add_lambda_request_id_to_logger,
request_id_context_var,
)

Expand All @@ -21,7 +21,7 @@ def test_decorator_sets_request_id_in_context():
mock_context = MagicMock()
mock_context.aws_request_id = test_request_id

@add_request_id_to_logs()
@add_lambda_request_id_to_logger()
def decorated_handler(event, context): # noqa : ARG001
return request_id_context_var.get()

Expand All @@ -35,7 +35,7 @@ def test_decorator_preserves_function_return_value():
mock_context = MagicMock()
mock_context.aws_request_id = "any-id"

@add_request_id_to_logs()
@add_lambda_request_id_to_logger()
def decorated_handler(event, context): # noqa : ARG001
return expected_result

Expand All @@ -47,7 +47,7 @@ def decorated_handler(event, context): # noqa : ARG001
def test_request_id_context_is_properly_isolated():
results = {}

@add_request_id_to_logs()
@add_lambda_request_id_to_logger()
def decorated_handler(event, context): # noqa : ARG001
rid = request_id_context_var.get()
results[threading.current_thread().name] = rid
Expand Down Expand Up @@ -86,7 +86,7 @@ def lambda_context():


def test_enriched_json_formatter_adds_all_fields(lambda_context):
@add_request_id_to_logs()
@add_lambda_request_id_to_logger()
def test_handler(event, context): # noqa : ARG001
logger = logging.getLogger("test_logger")
logger.info("Test log inside handler")
Expand Down