diff --git a/aws_lambda_powertools/utilities/data_classes/sqs_event.py b/aws_lambda_powertools/utilities/data_classes/sqs_event.py index e800fd0aac4..dda63430dc6 100644 --- a/aws_lambda_powertools/utilities/data_classes/sqs_event.py +++ b/aws_lambda_powertools/utilities/data_classes/sqs_event.py @@ -55,6 +55,13 @@ def message_deduplication_id(self) -> Optional[str]: the 5-minute deduplication interval.""" return self.get("MessageDeduplicationId") + @property + def dead_letter_queue_source_arn(self) -> Optional[str]: + """The SQS queue ARN that sent the record to this DLQ. + Only present when a Lambda function is using a DLQ as an event source. + """ + return self.get("DeadLetterQueueSourceArn") + class SQSMessageAttribute(DictWrapper): """The user-specified message attribute value.""" diff --git a/aws_lambda_powertools/utilities/parser/models/sqs.py b/aws_lambda_powertools/utilities/parser/models/sqs.py index 63ea4b76e0e..317b76c3227 100644 --- a/aws_lambda_powertools/utilities/parser/models/sqs.py +++ b/aws_lambda_powertools/utilities/parser/models/sqs.py @@ -15,6 +15,7 @@ class SqsAttributesModel(BaseModel): SentTimestamp: datetime SequenceNumber: Optional[str] = None AWSTraceHeader: Optional[str] = None + DeadLetterQueueSourceArn: Optional[str] = None class SqsMsgAttributeModel(BaseModel): diff --git a/tests/events/sqsDlqTriggerEvent.json b/tests/events/sqsDlqTriggerEvent.json new file mode 100644 index 00000000000..c47ee95aeef --- /dev/null +++ b/tests/events/sqsDlqTriggerEvent.json @@ -0,0 +1,21 @@ +{ + "Records": [ + { + "messageId": "db37cc61-1bb0-4e77-b6f3-7cf87f44a72a", + "receiptHandle": "AQEBl1pqxv+ZHkarVAWZUyWgj2mmqJGLBTo6YFOi/bw1QpBTpJBGJPLOTZrjKztKIbAB8EXkG7zHlbkn+Ze/AHMKKuhST9azHu8LyF4Ffu9uPkZc5xzggXlfFBWH3TUKyV+F5Obaj3esyX8YfM/zfgjbRuu5nc2tfPhvaSYEaTZsdMpzIB5tyKvHxAltLxK7upRHeoT768M9UrFYswarFTBn8piDbnsPsUhi8Q9G4Q4xSI0fLQANmryBsRJIzGQTVxenDad+MJ7XEL+hD3p2DmW+ycvv6WD7bdedqQuroQG8+ca1Dz7s3CBbXw9ZZnUziPa7LH1j1Lky5bAxpNF+BlurRS9pFBnomhwpylrGxtGfaEmUW1G7jnrG97sZNOLOFUykbQgroZPXmjzMBdvtgq9ZmQfCch3LOXN267+PKc56VR4=", + "body": "hello world", + "attributes": { + "DeadLetterQueueSourceArn": "arn:aws:sqs:eu-central-1:123456789012:sqs-redrive-SampleQueue-RNvLCpwGmLi7", + "ApproximateReceiveCount": "2", + "SentTimestamp": "1713185156609", + "SenderId": "AMCXIENQZJOLO23YVJ4VO", + "ApproximateFirstReceiveTimestamp": "1713185156612" + }, + "messageAttributes": {}, + "md5OfBody": "6a204bd89f3c8348afd5c77c717a097a", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:sqs-redrive-SampleDLQ-Emgp9MFSLBZm", + "awsRegion": "eu-central-1" + } + ] +} diff --git a/tests/unit/data_classes/test_sqs_event.py b/tests/unit/data_classes/test_sqs_event.py index 620ba802e95..b1664924c5e 100644 --- a/tests/unit/data_classes/test_sqs_event.py +++ b/tests/unit/data_classes/test_sqs_event.py @@ -44,6 +44,33 @@ def test_seq_trigger_event(): assert record_2.json_body == {"message": "foo1"} +def test_sqs_dlq_trigger_event(): + raw_event = load_event("sqsDlqTriggerEvent.json") + parsed_event = SQSEvent(raw_event) + + records = list(parsed_event.records) + record = records[0] + attributes = record.attributes + + assert len(records) == 1 + assert record.message_id == raw_event["Records"][0]["messageId"] + assert record.receipt_handle == raw_event["Records"][0]["receiptHandle"] + assert record.body == raw_event["Records"][0]["body"] + assert attributes.aws_trace_header is None + raw_attributes = raw_event["Records"][0]["attributes"] + assert attributes.approximate_receive_count == raw_attributes["ApproximateReceiveCount"] + assert attributes.sent_timestamp == raw_attributes["SentTimestamp"] + assert attributes.sender_id == raw_attributes["SenderId"] + assert attributes.approximate_first_receive_timestamp == raw_attributes["ApproximateFirstReceiveTimestamp"] + assert attributes.sequence_number is None + assert attributes.message_group_id is None + assert attributes.message_deduplication_id is None + assert ( + attributes.dead_letter_queue_source_arn + == raw_attributes["DeadLetterQueueSourceArn"] + ) + + def test_decode_nested_s3_event(): raw_event = load_event("s3SqsEvent.json") event = SQSEvent(raw_event) diff --git a/tests/unit/parser/test_sqs.py b/tests/unit/parser/test_sqs.py index 0d948acb39d..acae8c1093f 100644 --- a/tests/unit/parser/test_sqs.py +++ b/tests/unit/parser/test_sqs.py @@ -1,6 +1,7 @@ import pytest from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse +from aws_lambda_powertools.utilities.parser.models import SqsModel from tests.functional.utils import load_event from tests.functional.validator.conftest import sqs_event # noqa: F401 from tests.unit.parser.schemas import MyAdvancedSqsBusiness, MySqsBusiness @@ -85,3 +86,38 @@ def test_handle_sqs_trigger_event_no_envelope(): assert test_attr.stringValue == message_attributes_raw["stringValue"] assert test_attr.binaryValue == message_attributes_raw["binaryValue"] assert test_attr.dataType == message_attributes_raw["dataType"] + + +def test_sqs_dlq_trigger_event(): + raw_event = load_event("sqsDlqTriggerEvent.json") + parsed_event = SqsModel(**raw_event) + + records = parsed_event.Records + record = records[0] + raw_record = raw_event["Records"][0] + assert len(records) == 1 + + assert record.messageId == raw_record["messageId"] + assert record.receiptHandle == raw_record["receiptHandle"] + assert record.body == raw_record["body"] + assert record.eventSource == raw_record["eventSource"] + assert record.eventSourceARN == raw_record["eventSourceARN"] + assert record.awsRegion == raw_record["awsRegion"] + assert record.md5OfBody == raw_record["md5OfBody"] + + attributes = record.attributes + assert attributes.AWSTraceHeader is None + assert attributes.ApproximateReceiveCount == raw_record["attributes"]["ApproximateReceiveCount"] + assert attributes.SequenceNumber is None + assert attributes.MessageGroupId is None + assert attributes.MessageDeduplicationId is None + assert attributes.SenderId == raw_record["attributes"]["SenderId"] + convert_time = int(round(attributes.ApproximateFirstReceiveTimestamp.timestamp() * 1000)) + assert convert_time == int(raw_record["attributes"]["ApproximateFirstReceiveTimestamp"]) + convert_time = int(round(attributes.SentTimestamp.timestamp() * 1000)) + assert convert_time == int(raw_record["attributes"]["SentTimestamp"]) + + assert ( + attributes.DeadLetterQueueSourceArn + == raw_record["attributes"]["DeadLetterQueueSourceArn"] + )