@@ -72,44 +72,53 @@ class Sqs extends BaseAwsSdkPlugin {
7272 } )
7373 }
7474
75+ /**
76+ * Start the consumer (`aws.response`) span for a receive. The first message carrying trace
77+ * context becomes the parent; every additional one fans in as a span link.
78+ *
79+ * @param {{ request: object, response: object, needsFinish?: boolean, currentStore?: object } } ctx
80+ * @returns {object | undefined } The store to activate for the consumer span, else the parent store.
81+ */
7582 #startResponseSpan ( ctx ) {
7683 const { request, response } = ctx
77- const contextExtraction = this . responseExtract ( request . params , request . operation , response )
84+ const carriers = this . responseExtract ( request . params , request . operation , response )
7885
7986 let store = this . _parentMap . get ( request )
8087 let span
81- let parsedMessageAttributes
82- let parsedFirstBody
83- let firstBodyChecked = false
84- if ( contextExtraction !== undefined ) {
85- parsedFirstBody = contextExtraction . parsedBody
86- firstBodyChecked = contextExtraction . bodyChecked === true
87- if ( contextExtraction . datadogContext !== undefined ) {
88- parsedMessageAttributes = contextExtraction . parsedAttributes
89- // request:start records requestTags only after the isEnabled gate, so an absent entry
90- // means this consumer is disabled — gate on it instead of paying isEnabled again here.
91- const requestTags = this . requestTags . get ( request )
92- if ( requestTags !== undefined ) {
93- ctx . needsFinish = true
94- const options = {
95- childOf : contextExtraction . datadogContext ,
96- meta : {
97- ...requestTags ,
98- 'span.kind' : 'server' ,
99- } ,
100- integrationName : 'aws-sdk' ,
88+
89+ if ( carriers !== undefined ) {
90+ // request:start records requestTags only after the isEnabled gate, so an absent entry
91+ // means this consumer is disabled — gate on it instead of paying isEnabled again here.
92+ const requestTags = this . requestTags . get ( request )
93+ if ( requestTags !== undefined ) {
94+ // A receive can return messages from many producers; fanning the extra ones in as span
95+ // links is the shape dd-trace-java and dd-trace-py use for batch SQS receives.
96+ for ( const carrier of carriers ) {
97+ if ( carrier === undefined ) continue
98+ const datadogContext = this . tracer . extract ( 'text_map' , carrier )
99+ // A DSM-only carrier (a non-first sendMessageBatch entry when batchPropagationEnabled
100+ // is off) extracts to null; span.addLink dereferences the context and would throw on it.
101+ if ( datadogContext === null ) continue
102+ if ( span === undefined ) {
103+ ctx . needsFinish = true
104+ span = this . startSpan ( 'aws.response' , {
105+ childOf : datadogContext ,
106+ meta : {
107+ ...requestTags ,
108+ 'span.kind' : 'server' ,
109+ } ,
110+ integrationName : 'aws-sdk' ,
111+ } , ctx )
112+ store = ctx . currentStore
113+ } else {
114+ span . addLink ( { context : datadogContext } )
101115 }
102- span = this . startSpan ( 'aws.response' , options , ctx )
103- store = ctx . currentStore
104116 }
105117 }
106118 }
107119
108120 // Extract DSM context after, as we might not have a parent-child but may have a DSM context.
109- this . responseExtractDSMContext (
110- request . operation , request . params , response , span ?? null ,
111- { parsedAttributes : parsedMessageAttributes , parsedFirstBody, firstBodyChecked }
112- )
121+ this . responseExtractDSMContext ( request . operation , request . params , response , span ?? null , carriers )
113122
114123 return store
115124 }
@@ -183,54 +192,49 @@ class Sqs extends BaseAwsSdkPlugin {
183192 return tags
184193 }
185194
195+ /**
196+ * Parse the trace-context carrier of every received message, in message order.
197+ * Entries are `undefined` for messages that carry no `_datadog` context.
198+ *
199+ * @param {{ MaxNumberOfMessages?: number } } params
200+ * @param {string } operation
201+ * @param {{ Messages?: object[] } } response
202+ * @returns {Array<Record<string, string> | undefined> | undefined }
203+ */
186204 responseExtract ( params , operation , response ) {
187205 if ( operation !== 'receiveMessage' ) return
188- if ( params . MaxNumberOfMessages && params . MaxNumberOfMessages !== 1 ) return
189- if ( ! response || ! response . Messages || ! response . Messages [ 0 ] ) return
206+ if ( ! response ?. Messages ?. length ) return
190207
191- let message = response . Messages [ 0 ]
192- let parsedBody
208+ return response . Messages . map ( message => this . parseMessageCarrier ( message ) )
209+ }
193210
211+ /**
212+ * Resolve the trace-context carrier for a single received message. The
213+ * `MessageAttributes._datadog` text map (direct SQS or SNS to SQS) wins;
214+ * otherwise the EventBridge envelope, optionally wrapped in an SNS
215+ * `Notification` (see getEventBridgeContext). Checking MessageAttributes first
216+ * avoids parsing a large SNS `Message` just to rule out an EventBridge envelope.
217+ *
218+ * @param {object } message A single `response.Messages` entry.
219+ * @returns {Record<string, string> | undefined }
220+ */
221+ parseMessageCarrier ( message ) {
222+ let parsedBody
194223 if ( message . Body ) {
195224 try {
196225 parsedBody = JSON . parse ( message . Body )
197226 } catch {
198- // SQS to SQS
227+ // Opaque, non-JSON body ( SQS to SQS).
199228 }
200229 // SNS to SQS
201230 if ( parsedBody ?. Type === 'Notification' ) {
202231 message = parsedBody
203232 }
204233 }
205234
206- // Check MessageAttributes first (common direct-SQS/SNS path): avoids parsing
207- // the body (e.g. a large SNS `Message`) just to rule out an EventBridge
208- // envelope. Precedence matches responseExtractDSMContext.
209235 const datadogAttribute = message . MessageAttributes ?. _datadog
210- if ( datadogAttribute ) {
211- const parsedAttributes = this . parseDatadogAttributes ( datadogAttribute )
212- if ( parsedAttributes ) {
213- return {
214- datadogContext : this . tracer . extract ( 'text_map' , parsedAttributes ) ,
215- parsedAttributes,
216- parsedBody,
217- bodyChecked : true ,
218- }
219- }
220- }
221-
222- // Then the EventBridge envelope (optionally via SNS); see getEventBridgeContext.
223- const eventBridgeContext = getEventBridgeContext ( parsedBody )
224- if ( eventBridgeContext ) {
225- return {
226- datadogContext : this . tracer . extract ( 'text_map' , eventBridgeContext ) ,
227- parsedAttributes : eventBridgeContext ,
228- parsedBody,
229- bodyChecked : true ,
230- }
231- }
232-
233- return { parsedBody, bodyChecked : true }
236+ const carrier = datadogAttribute ? this . parseDatadogAttributes ( datadogAttribute ) : undefined
237+ return carrier ?? getEventBridgeContext ( parsedBody )
234238 }
235239
236240 parseDatadogAttributes ( attributes ) {
@@ -247,51 +251,39 @@ class Sqs extends BaseAwsSdkPlugin {
247251 }
248252 }
249253
250- responseExtractDSMContext ( operation , params , response , span , kwargs = { } ) {
251- let { parsedAttributes } = kwargs
252- const { parsedFirstBody, firstBodyChecked } = kwargs
254+ /**
255+ * @param {string } operation
256+ * @param {{ QueueUrl: string } } params
257+ * @param {{ Messages?: object[] } } response
258+ * @param {import('../../../dd-trace/src/opentracing/span') | null } span
259+ * @param {Array<Record<string, string> | undefined> } [carriers] Per-message carriers already
260+ * parsed by `responseExtract`; reused so each message body is parsed once. When omitted, the
261+ * carriers are parsed here.
262+ */
263+ responseExtractDSMContext ( operation , params , response , span , carriers ) {
253264 if ( ! this . config . dsmEnabled ) return
254265 if ( operation !== 'receiveMessage' ) return
255- if ( ! response || ! response . Messages || ! response . Messages [ 0 ] ) return
266+ if ( ! response ? .Messages ?. length ) return
256267
268+ const messages = response . Messages
257269 // Only attribute payloadSize to the span when there is a single message.
258- span = response . Messages . length > 1 ? null : span
270+ span = messages . length > 1 ? null : span
259271
260272 // QueueUrl is the same for the whole receive batch.
261273 const queue = params . QueueUrl . slice ( params . QueueUrl . lastIndexOf ( '/' ) + 1 )
262274
263- for ( let i = 0 ; i < response . Messages . length ; i ++ ) {
264- let message = response . Messages [ i ]
265- if ( ! parsedAttributes ) {
266- let body
267- // responseExtract already parsed message[0]; reuse that result instead of re-parsing.
268- if ( i === 0 && firstBodyChecked ) {
269- body = parsedFirstBody
270- } else if ( message . Body ) {
271- try {
272- body = JSON . parse ( message . Body )
273- } catch {
274- // SQS to SQS
275- }
276- }
277- // SNS to SQS
278- if ( body ?. Type === 'Notification' ) {
279- message = body
280- }
281- // MessageAttributes for direct SQS/SNS; else the EventBridge envelope.
282- parsedAttributes = message . MessageAttributes ?. _datadog
283- ? this . parseDatadogAttributes ( message . MessageAttributes . _datadog )
284- : getEventBridgeContext ( body )
275+ for ( let i = 0 ; i < messages . length ; i ++ ) {
276+ const message = messages [ i ]
277+ const carrier = carriers === undefined ? this . parseMessageCarrier ( message ) : carriers [ i ]
278+ if ( carrier ) {
279+ // Inert for EventBridge until its producer emits a pathway (separate
280+ // change) — no `dd-pathway-ctx-base64` to decode yet; SQS/SNS decode now.
281+ this . tracer . decodeDataStreamsContext ( carrier )
285282 }
286283 const payloadSize = getHeadersSize ( {
287284 Body : message . Body ,
288285 MessageAttributes : message . MessageAttributes ,
289286 } )
290- if ( parsedAttributes ) {
291- // Inert for EventBridge until its producer emits a pathway (separate
292- // change) — no `dd-pathway-ctx-base64` to decode yet; SQS/SNS decode now.
293- this . tracer . decodeDataStreamsContext ( parsedAttributes )
294- }
295287 this . tracer
296288 . setCheckpoint ( [ 'direction:in' , `topic:${ queue } ` , 'type:sqs' ] , span , payloadSize )
297289 }
0 commit comments