Skip to content

Commit fd813a3

Browse files
committed
chore: update to handle new public DSM API
1 parent f9ba2ed commit fd813a3

2 files changed

Lines changed: 359 additions & 41 deletions

File tree

‎datadog_lambda/tracing.py‎

Lines changed: 135 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,42 @@ def _dsm_set_checkpoint(context_json, event_type, arn):
8686
def _dsm_set_eventbridge_checkpoint(context_json, detail_type):
8787
"""Set a DSM consume checkpoint for an EventBridge event.
8888
89-
Temporary note: richer EventBridge consume checkpoint tagging is being
90-
upstreamed into dd-trace-py. Until that lands, keep this on the same
91-
public consume checkpoint API used by SQS/SNS/Kinesis and omit the
92-
EventBridge exchange tag for now.
89+
When ddtrace >= 4.14 is present the public ``tags`` parameter is used to
90+
attach the ``exchange:`` edge tag (sourced from ``DD_DSM_EXCHANGE_NAME``).
91+
On older installs the checkpoint is still emitted, just without the
92+
exchange tag.
9393
"""
94-
_dsm_set_checkpoint(context_json, "eventbridge", detail_type)
94+
if not config.data_streams_enabled or not detail_type:
95+
return
96+
97+
try:
98+
from ddtrace.data_streams import set_consume_checkpoint
99+
100+
carrier_get = lambda k: context_json and context_json.get(k) # noqa: E731
101+
try:
102+
tags = (
103+
["exchange:" + config.dsm_exchange_name]
104+
if config.dsm_exchange_name
105+
else None
106+
)
107+
set_consume_checkpoint(
108+
"eventbridge",
109+
detail_type,
110+
carrier_get,
111+
manual_checkpoint=False,
112+
tags=tags,
113+
)
114+
except TypeError:
115+
# ddtrace < 4.14 has no `tags` parameter. Retry without it, but
116+
# keep `manual_checkpoint=False` so the checkpoint stays
117+
# consistent with every other consume checkpoint.
118+
set_consume_checkpoint(
119+
"eventbridge", detail_type, carrier_get, manual_checkpoint=False
120+
)
121+
except Exception as e:
122+
logger.debug(
123+
f"DSM:Failed to set consume checkpoint for eventbridge {detail_type}: {e}"
124+
)
95125

96126

97127
def _convert_xray_trace_id(xray_trace_id):
@@ -262,9 +292,17 @@ def extract_context_from_sqs_or_sns_event_or_context(
262292
source_arn = ""
263293
event_type = "sqs" if event_source.equals(EventTypes.SQS) else "sns"
264294

265-
# EventBridge => SQS
295+
# EventBridge => SQS. `dsm_handled` is True when the batch contained at
296+
# least one EventBridge delivery and DSM checkpoints were already set
297+
# per-record; in that case the regular SQS path below must not set another
298+
# checkpoint for the first record or it would be double counted.
299+
dsm_handled = False
266300
try:
267-
context, is_eventbridge_sqs = _extract_context_from_eventbridge_sqs_event(event)
301+
(
302+
context,
303+
is_eventbridge_sqs,
304+
dsm_handled,
305+
) = _extract_context_from_eventbridge_sqs_event(event)
268306
if is_eventbridge_sqs:
269307
if _is_context_complete(context):
270308
return context
@@ -325,7 +363,8 @@ def extract_context_from_sqs_or_sns_event_or_context(
325363
"Failed to extract Step Functions context from SQS/SNS event."
326364
)
327365
context = propagator.extract(dd_data)
328-
_dsm_set_checkpoint(dd_data, event_type, source_arn)
366+
if not dsm_handled:
367+
_dsm_set_checkpoint(dd_data, event_type, source_arn)
329368
return context
330369
else:
331370
# Handle case where trace context is injected into attributes.AWSTraceHeader
@@ -350,12 +389,14 @@ def extract_context_from_sqs_or_sns_event_or_context(
350389
sampling_priority=float(x_ray_context["sampled"]),
351390
)
352391
# Still want to set a DSM checkpoint even if DSM context not propagated
353-
_dsm_set_checkpoint(None, event_type, source_arn)
392+
if not dsm_handled:
393+
_dsm_set_checkpoint(None, event_type, source_arn)
354394
return extract_context_from_lambda_context(lambda_context)
355395
except Exception as e:
356396
logger.debug("The trace extractor returned with error %s", e)
357397
# Still want to set a DSM checkpoint even if DSM context not propagated
358-
_dsm_set_checkpoint(None, event_type, source_arn)
398+
if not dsm_handled:
399+
_dsm_set_checkpoint(None, event_type, source_arn)
359400
return extract_context_from_lambda_context(lambda_context)
360401

361402

@@ -366,53 +407,112 @@ def _extract_context_from_eventbridge_sqs_event(event):
366407
367408
This is only possible if first record in `Records` contains a
368409
`body` field which contains the EventBridge `detail` as a JSON string.
410+
411+
Returns a tuple ``(context, is_eventbridge_sqs, dsm_handled)``:
412+
413+
* ``context`` / ``is_eventbridge_sqs`` describe the trace context and are
414+
derived only from the first record, since that is the record whose trace
415+
context becomes the Lambda's parent.
416+
* ``dsm_handled`` reports whether this function already set DSM checkpoints
417+
for the batch. It is ``True`` whenever *any* record in the batch is an
418+
EventBridge delivery, so the caller must not set its own SQS checkpoint
419+
(which would double count the first record).
369420
"""
370421
records = event.get("Records")
371422
if not records:
372-
return None, False
423+
return None, False, False
373424

374425
first_record = records[0]
375426
dd_context, is_eventbridge_sqs = _extract_eventbridge_sqs_record_context(
376427
first_record
377428
)
429+
430+
# Set a consume checkpoint for every record in the batch whenever the batch
431+
# contains at least one EventBridge delivery. Each record is classified
432+
# independently so a mixed batch (EventBridge deliveries alongside direct
433+
# SQS sends, in either order) uses the correct carrier per record. The
434+
# message is consumed from the SQS queue, so it follows SQS conventions
435+
# (type:sqs, topic:queue ARN).
436+
dsm_handled = _dsm_set_eventbridge_sqs_batch_checkpoints(records)
437+
378438
if not is_eventbridge_sqs:
379-
return None, False
439+
return None, False, dsm_handled
380440

381-
# The event has been confirmed as EventBridge -> SQS. Set a consume
382-
# checkpoint for every record in the batch. The message is consumed from
383-
# the SQS queue, so it follows SQS conventions (type:sqs, topic:queue ARN).
384-
if config.data_streams_enabled:
385-
for record in records:
441+
if is_step_function_event(dd_context):
442+
try:
443+
return (
444+
extract_context_from_step_functions(dd_context, None),
445+
True,
446+
dsm_handled,
447+
)
448+
except Exception:
449+
logger.debug(
450+
"Failed to extract Step Functions context from EventBridge to SQS event."
451+
)
452+
453+
return propagator.extract(dd_context), True, dsm_handled
454+
455+
456+
def _dsm_set_eventbridge_sqs_batch_checkpoints(records):
457+
"""Set a per-record SQS DSM consume checkpoint for an EventBridge -> SQS
458+
batch.
459+
460+
Returns ``True`` when the batch contains at least one EventBridge delivery
461+
(and checkpoints were therefore this function's responsibility), otherwise
462+
``False`` so the caller can fall back to its regular SQS checkpoint path.
463+
Each record is classified independently: EventBridge records use the
464+
carrier embedded in ``body.detail._datadog`` while other records fall back
465+
to the SQS ``messageAttributes._datadog`` carrier.
466+
"""
467+
if not config.data_streams_enabled:
468+
return False
469+
470+
record_carriers = []
471+
batch_has_eventbridge = False
472+
for record in records:
473+
record_context, is_eventbridge_record = _extract_eventbridge_sqs_record_context(
474+
record
475+
)
476+
if is_eventbridge_record:
477+
batch_has_eventbridge = True
478+
else:
386479
try:
387-
record_context, is_eventbridge_record = (
388-
_extract_eventbridge_sqs_record_context(record)
389-
)
390-
if not is_eventbridge_record:
391-
record_context = _extract_sqs_record_message_attribute_context(
392-
record
393-
)
394-
_dsm_set_checkpoint(
395-
record_context, "sqs", record.get("eventSourceARN", "")
396-
)
480+
record_context = _extract_sqs_record_message_attribute_context(record)
397481
except Exception:
398-
logger.debug(
399-
"Failed to set DSM checkpoint for an EventBridge to SQS record."
400-
)
482+
record_context = None
483+
record_carriers.append(record_context)
401484

402-
if is_step_function_event(dd_context):
485+
if not batch_has_eventbridge:
486+
return False
487+
488+
for record, record_context in zip(records, record_carriers):
403489
try:
404-
return extract_context_from_step_functions(dd_context, None), True
490+
_dsm_set_checkpoint(record_context, "sqs", record.get("eventSourceARN", ""))
405491
except Exception:
406492
logger.debug(
407-
"Failed to extract Step Functions context from EventBridge to SQS event."
493+
"Failed to set DSM checkpoint for an EventBridge to SQS record."
408494
)
409495

410-
return propagator.extract(dd_context), True
496+
return True
411497

412498

413499
def _extract_eventbridge_sqs_record_context(record):
500+
"""Classify a single SQS record as an EventBridge delivery and return its
501+
DSM carrier.
502+
503+
Returns a tuple ``(dd_context, is_eventbridge)``. A record is only treated
504+
as EventBridge when its ``body`` is a JSON object carrying the EventBridge
505+
envelope fields (``detail`` object plus ``detail-type`` and ``source``). A
506+
non-JSON or non-envelope body is not an error here: it simply means the
507+
record is a regular SQS message, so the caller can fall back to the SQS
508+
message attribute carrier instead.
509+
"""
414510
body_str = record.get("body")
415-
body = json.loads(body_str)
511+
try:
512+
body = json.loads(body_str)
513+
except (ValueError, TypeError):
514+
return None, False
515+
416516
if not isinstance(body, dict):
417517
return None, False
418518

0 commit comments

Comments
 (0)