Skip to content

Commit 66fdc28

Browse files
committed
fix: extract SNS dsm context in mixed eventbridge/SNS sqs batches
When EventBridge and SNS are both routed to the same SQS queue and their messages are batched into a single event payload, the SNS DSM context was dropped. _dsm_set_eventbridge_sqs_batch_checkpoints classifies every record in the batch and falls back to _extract_sqs_record_message_attribute_context for non-EventBridge records. That helper only read record.messageAttributes and never unwrapped an SNS notification carried in the SQS body (an SNS => SQS subscription without raw message delivery), so the SNS carrier resolved to None. Because the batch contained an EventBridge delivery, dsm_handled was True for the whole batch and the SNS-aware fallback path further down in extract_context_from_sqs_or_sns_event_or_context never ran. Teach the helper to unwrap the SNS envelope the same way the main SQS/SNS extraction path already does, so each record in a mixed batch is classified correctly regardless of whether it is a direct SQS send or an SNS delivery. The shared attribute decoding and envelope detection are factored out into _decode_dd_message_attribute and _parse_sns_notification_from_sqs_body.
1 parent fd813a3 commit 66fdc28

2 files changed

Lines changed: 96 additions & 2 deletions

File tree

‎datadog_lambda/tracing.py‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,51 @@ def _extract_eventbridge_sqs_record_context(record):
526526

527527

528528
def _extract_sqs_record_message_attribute_context(record):
529-
msg_attributes = record.get("messageAttributes") or {}
530-
dd_payload = msg_attributes.get("_datadog")
529+
"""Return the ``_datadog`` carrier for a non-EventBridge SQS record.
530+
531+
A record in an SQS batch may itself be an SNS notification (an SNS => SQS
532+
subscription without raw message delivery), in which case the attributes
533+
live in the SNS envelope inside ``body`` rather than in the record's own
534+
``messageAttributes``. Both shapes are handled here so that a mixed batch
535+
(EventBridge deliveries alongside SNS deliveries on the same queue) does
536+
not lose the SNS DSM context.
537+
"""
538+
msg_attributes = record.get("messageAttributes")
539+
540+
if msg_attributes is None:
541+
sns_record = _parse_sns_notification_from_sqs_body(record) or {}
542+
msg_attributes = sns_record.get("MessageAttributes") or {}
543+
544+
return _decode_dd_message_attribute(msg_attributes.get("_datadog"))
545+
546+
547+
def _parse_sns_notification_from_sqs_body(record):
548+
"""Return the SNS envelope carried in an SQS record's ``body``, if any.
549+
550+
Returns ``None`` when the body is not an SNS notification, which simply
551+
means the record is a direct SQS send.
552+
"""
553+
try:
554+
body = json.loads(record.get("body"))
555+
except (ValueError, TypeError):
556+
return None
557+
558+
if (
559+
isinstance(body, dict)
560+
and body.get("Type", "") == "Notification"
561+
and "TopicArn" in body
562+
):
563+
return body
564+
565+
return None
566+
567+
568+
def _decode_dd_message_attribute(dd_payload):
569+
"""Decode a ``_datadog`` SQS/SNS message attribute into a carrier dict.
570+
571+
SQS uses ``dataType`` with ``stringValue``/``binaryValue`` while SNS uses
572+
``Type`` with ``Value``; both are supported.
573+
"""
531574
if not dd_payload:
532575
return None
533576

‎tests/test_tracing.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4103,6 +4103,57 @@ def test_eventbridge_sqs_direct_first_record_still_checkpoints_eventbridge(self)
41034103
self.assertEqual((second_args[0], second_args[1]), ("sqs", arn2))
41044104
self.assertEqual(second_args[2]("dd-pathway-ctx-base64"), "eb-ctx")
41054105

4106+
def test_eventbridge_sqs_mixed_batch_with_sns_record_keeps_sns_context(self):
4107+
# EventBridge and SNS can both be routed to the same SQS queue and then
4108+
# batched into a single event payload. The SNS record's DSM context
4109+
# lives in the SNS envelope inside `body` (SNS => SQS subscription
4110+
# without raw message delivery), not in `messageAttributes`, and must
4111+
# still be extracted even though the batch contains an EventBridge
4112+
# delivery.
4113+
arn1 = "arn:aws:sqs:us-east-1:123456789012:eb-queue"
4114+
arn2 = "arn:aws:sqs:us-east-1:123456789012:sns-queue"
4115+
sns_dd_data = {
4116+
"x-datadog-trace-id": "12345",
4117+
"x-datadog-parent-id": "67890",
4118+
"x-datadog-sampling-priority": "1",
4119+
"dd-pathway-ctx-base64": "sns-ctx",
4120+
}
4121+
sns_body = {
4122+
"Type": "Notification",
4123+
"TopicArn": "arn:aws:sns:us-east-1:123456789012:my-topic",
4124+
"Message": "hello from sns",
4125+
"MessageAttributes": {
4126+
"_datadog": {
4127+
"Type": "String",
4128+
"Value": json.dumps(sns_dd_data),
4129+
}
4130+
},
4131+
}
4132+
event = {
4133+
"Records": [
4134+
self._eventbridge_sqs_record(arn1, "eb-ctx"),
4135+
{
4136+
"eventSourceARN": arn2,
4137+
"eventSource": "aws:sqs",
4138+
"body": json.dumps(sns_body),
4139+
},
4140+
]
4141+
}
4142+
4143+
extract_context_from_sqs_or_sns_event_or_context(
4144+
event, self.lambda_context, parse_event_source(event)
4145+
)
4146+
4147+
self.assertEqual(self.mock_checkpoint.call_count, 2)
4148+
first_args, _ = self.mock_checkpoint.call_args_list[0]
4149+
second_args, _ = self.mock_checkpoint.call_args_list[1]
4150+
self.assertEqual((first_args[0], first_args[1]), ("sqs", arn1))
4151+
self.assertEqual(first_args[2]("dd-pathway-ctx-base64"), "eb-ctx")
4152+
# The SNS record is consumed from the SQS queue, so it keeps SQS
4153+
# conventions, but its carrier must come from the SNS envelope.
4154+
self.assertEqual((second_args[0], second_args[1]), ("sqs", arn2))
4155+
self.assertEqual(second_args[2]("dd-pathway-ctx-base64"), "sns-ctx")
4156+
41064157
def test_eventbridge_sqs_non_json_body_falls_back_to_sqs_attributes(self):
41074158
# A later direct SQS record can legitimately carry a non-JSON body
41084159
# while its DSM carrier lives in messageAttributes._datadog. The

0 commit comments

Comments
 (0)