@@ -86,12 +86,42 @@ def _dsm_set_checkpoint(context_json, event_type, arn):
8686def _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
97127def _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
413499def _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