dd-trace 5.108.0 → 5.109.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +22 -1
- package/package.json +2 -1
- package/packages/datadog-instrumentations/src/ai.js +43 -48
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
- package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
- package/packages/datadog-instrumentations/src/electron.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/http/client.js +12 -2
- package/packages/datadog-instrumentations/src/ioredis.js +0 -1
- package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
- package/packages/datadog-instrumentations/src/next.js +34 -0
- package/packages/datadog-instrumentations/src/openai.js +77 -18
- package/packages/datadog-instrumentations/src/redis.js +0 -1
- package/packages/datadog-instrumentations/src/vitest.js +60 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +1 -7
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +44 -27
- package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
- package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
- package/packages/datadog-plugin-fs/src/index.js +1 -0
- package/packages/datadog-plugin-redis/src/index.js +1 -2
- package/packages/datadog-plugin-vitest/src/index.js +4 -1
- package/packages/dd-trace/src/aiguard/channels.js +0 -1
- package/packages/dd-trace/src/aiguard/index.js +11 -49
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
- package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
- package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
- package/packages/dd-trace/src/config/supported-configurations.json +24 -2
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
- package/packages/dd-trace/src/dogstatsd.js +15 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
- package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
- package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto')
|
|
4
|
+
const log = require('../../dd-trace/src/log')
|
|
5
|
+
const TextMapPropagator = require('../../dd-trace/src/opentracing/propagation/text_map')
|
|
6
|
+
|
|
7
|
+
const CHECKPOINT_NAME_PREFIX = '_datadog_'
|
|
8
|
+
|
|
9
|
+
// Propagator that injects only Datadog-style headers (`x-datadog-*`) regardless of the user's
|
|
10
|
+
// `DD_TRACE_PROPAGATION_STYLE_INJECT`. Checkpoints are written and read entirely by Datadog code,
|
|
11
|
+
// so honoring user style preferences would only complicate the payload contract. AWS runs a single
|
|
12
|
+
// tracer, so one lazily-built propagator suffices.
|
|
13
|
+
let datadogOnlyPropagator
|
|
14
|
+
|
|
15
|
+
function getDatadogOnlyPropagator (tracer) {
|
|
16
|
+
if (datadogOnlyPropagator) return datadogOnlyPropagator
|
|
17
|
+
const config = tracer._config
|
|
18
|
+
// Shadow `tracePropagationStyle.inject` while inheriting every other field (x-datadog-tags length
|
|
19
|
+
// cap, etc.) from the live config. Disable `legacyBaggageEnabled` only to keep `ot-baggage-*` out
|
|
20
|
+
// of the checkpoint payload we persist (sensitive-data concern) — not a serverless-wide change.
|
|
21
|
+
const shadowConfig = Object.create(config)
|
|
22
|
+
shadowConfig.tracePropagationStyle = {
|
|
23
|
+
...config.tracePropagationStyle,
|
|
24
|
+
inject: ['datadog'],
|
|
25
|
+
}
|
|
26
|
+
shadowConfig.legacyBaggageEnabled = false
|
|
27
|
+
datadogOnlyPropagator = new TextMapPropagator(shadowConfig)
|
|
28
|
+
return datadogOnlyPropagator
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the Datadog-format headers dict from a span context.
|
|
33
|
+
* Mirrors ddtrace-py HTTPPropagator.inject output so the same payload
|
|
34
|
+
* can be consumed by either language's datadog-lambda wrapper.
|
|
35
|
+
* @param {object} tracer
|
|
36
|
+
* @param {object} span
|
|
37
|
+
* @returns {Record<string, string>}
|
|
38
|
+
*/
|
|
39
|
+
function injectHeaders (tracer, span) {
|
|
40
|
+
const headers = {}
|
|
41
|
+
const propagator = getDatadogOnlyPropagator(tracer)
|
|
42
|
+
const ctx = typeof span?.context === 'function' ? span.context() : span
|
|
43
|
+
propagator.inject(ctx, headers)
|
|
44
|
+
return headers
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Mutate headers in-place to set parent_id to the provided value.
|
|
49
|
+
* @param {Record<string, string>} headers
|
|
50
|
+
* @param {string | number | undefined} parentId
|
|
51
|
+
*/
|
|
52
|
+
function overrideParentId (headers, parentId) {
|
|
53
|
+
if (!parentId) return
|
|
54
|
+
if (headers['x-datadog-trace-id']) {
|
|
55
|
+
headers['x-datadog-parent-id'] = String(parentId)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the current trace context warrants a new checkpoint over the previously-saved one.
|
|
61
|
+
* @param {Record<string, string>} currentHeaders
|
|
62
|
+
* @param {Record<string, string>} previousHeaders
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function needsCheckpointUpdate (currentHeaders, previousHeaders) {
|
|
66
|
+
for (const key of Object.keys(currentHeaders)) {
|
|
67
|
+
if (currentHeaders[key] !== previousHeaders[key]) return true
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Find the checkpoint with the highest N for _datadog_{N} in the event's operations.
|
|
74
|
+
* @param {unknown} event
|
|
75
|
+
* @returns {{ checkpointNumber: number, operation: object } | undefined}
|
|
76
|
+
*/
|
|
77
|
+
function findLastCheckpoint (event) {
|
|
78
|
+
if (!event || typeof event !== 'object') return
|
|
79
|
+
|
|
80
|
+
const operations = event.InitialExecutionState?.Operations
|
|
81
|
+
if (!Array.isArray(operations)) return
|
|
82
|
+
|
|
83
|
+
let highest
|
|
84
|
+
for (const op of operations) {
|
|
85
|
+
const name = op?.Name
|
|
86
|
+
if (typeof name !== 'string' || !name.startsWith(CHECKPOINT_NAME_PREFIX)) continue
|
|
87
|
+
const suffix = name.slice(CHECKPOINT_NAME_PREFIX.length)
|
|
88
|
+
const checkpointNumber = Number.parseInt(suffix, 10)
|
|
89
|
+
if (Number.isNaN(checkpointNumber) || String(checkpointNumber) !== suffix) continue
|
|
90
|
+
|
|
91
|
+
if (!highest || checkpointNumber > highest.checkpointNumber) {
|
|
92
|
+
highest = { checkpointNumber, operation: op }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return highest
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse the JSON payload from a checkpoint STEP operation's Payload or StepDetails.Result.
|
|
101
|
+
* @param {object} op
|
|
102
|
+
* @returns {Record<string, string> | undefined}
|
|
103
|
+
*/
|
|
104
|
+
function parseCheckpointPayload (op) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = op?.Payload ?? op?.StepDetails?.Result
|
|
107
|
+
if (!raw || typeof raw !== 'string') return
|
|
108
|
+
const parsed = JSON.parse(raw)
|
|
109
|
+
return parsed && typeof parsed === 'object' ? parsed : undefined
|
|
110
|
+
} catch {
|
|
111
|
+
log.debug('Failed to parse checkpoint payload')
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Save a _datadog_{number} STEP operation via the SDK's checkpoint manager.
|
|
117
|
+
* Uses a deterministic blake2b hash of (name:arn) as the stepId so the save is
|
|
118
|
+
* idempotent within an execution.
|
|
119
|
+
* @param {object} checkpointManager
|
|
120
|
+
* @param {string} executionArn
|
|
121
|
+
* @param {number} number
|
|
122
|
+
* @param {Record<string, string>} headers
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
async function saveCheckpoint (checkpointManager, executionArn, number, headers) {
|
|
126
|
+
const name = `${CHECKPOINT_NAME_PREFIX}${number}`
|
|
127
|
+
const stepId = crypto
|
|
128
|
+
.createHash('blake2b512')
|
|
129
|
+
.update(`${name}:${executionArn}`)
|
|
130
|
+
.digest('hex')
|
|
131
|
+
.slice(0, 64)
|
|
132
|
+
const payload = JSON.stringify(headers)
|
|
133
|
+
|
|
134
|
+
// Queue START and SUCCEED back-to-back before awaiting. This allows callers
|
|
135
|
+
// to trigger save right before termination without losing the second update.
|
|
136
|
+
const startPromise = checkpointManager.checkpoint(stepId, {
|
|
137
|
+
Id: stepId,
|
|
138
|
+
Action: 'START',
|
|
139
|
+
Type: 'STEP',
|
|
140
|
+
SubType: 'STEP',
|
|
141
|
+
Name: name,
|
|
142
|
+
})
|
|
143
|
+
const succeedPromise = checkpointManager.checkpoint(stepId, {
|
|
144
|
+
Id: stepId,
|
|
145
|
+
Action: 'SUCCEED',
|
|
146
|
+
Type: 'STEP',
|
|
147
|
+
SubType: 'STEP',
|
|
148
|
+
Name: name,
|
|
149
|
+
Payload: payload,
|
|
150
|
+
})
|
|
151
|
+
await Promise.all([
|
|
152
|
+
startPromise,
|
|
153
|
+
succeedPromise,
|
|
154
|
+
])
|
|
155
|
+
log.debug('Saved trace context checkpoint %s', name)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Save a new trace-context checkpoint when the current context differs from
|
|
160
|
+
* the most recent `_datadog_{N}` operation already in the event.
|
|
161
|
+
*
|
|
162
|
+
* Every checkpoint across the durable execution carries the same
|
|
163
|
+
* `x-datadog-parent-id` so all resumed invocations attach to the same anchor:
|
|
164
|
+
* - First checkpoint (no previous): anchor at `firstExecutionSpanId`.
|
|
165
|
+
* - Subsequent: reuse the prior checkpoint's `x-datadog-parent-id` verbatim —
|
|
166
|
+
* that value originated from the first save and is the anchor we've been
|
|
167
|
+
* carrying forward.
|
|
168
|
+
*
|
|
169
|
+
* Caller is responsible for invoking this only when a save is appropriate — i.e.
|
|
170
|
+
* the SDK is about to return Status: PENDING (see PENDING_TERMINATION_REASONS in
|
|
171
|
+
* handler.js). This function does not re-check that.
|
|
172
|
+
*
|
|
173
|
+
* Errors (including a rejected checkpoint-manager call) propagate to the caller, which swallows
|
|
174
|
+
* them at the fire-and-forget boundary.
|
|
175
|
+
*
|
|
176
|
+
* @param {object} tracer
|
|
177
|
+
* @param {object} span - aws.durable.execute span
|
|
178
|
+
* @param {object} durableContext - SDK's DurableContextImpl
|
|
179
|
+
* @param {string | undefined} firstExecutionSpanId - span id of the first
|
|
180
|
+
* invocation's `aws.durable.execute` span. Only consulted on the very
|
|
181
|
+
* first save; ignored once a prior `_datadog_{N}` exists. We anchor at
|
|
182
|
+
* this span (which this integration owns) rather than its parent so the
|
|
183
|
+
* anchor doesn't depend on whatever upstream context happens to be
|
|
184
|
+
* active when `bindStart` fires.
|
|
185
|
+
* @param {unknown} event - raw invocation event (has InitialExecutionState)
|
|
186
|
+
* @returns {Promise<void>}
|
|
187
|
+
*/
|
|
188
|
+
async function saveTraceContextCheckpointIfUpdated (
|
|
189
|
+
tracer, span, durableContext, firstExecutionSpanId, event,
|
|
190
|
+
) {
|
|
191
|
+
const checkpointManager = durableContext.checkpoint ?? durableContext.checkpointManager
|
|
192
|
+
if (typeof checkpointManager?.checkpoint !== 'function') return
|
|
193
|
+
|
|
194
|
+
const currentHeaders = injectHeaders(tracer, span)
|
|
195
|
+
if (currentHeaders['x-datadog-trace-id'] === undefined) return
|
|
196
|
+
|
|
197
|
+
const latest = findLastCheckpoint(event)
|
|
198
|
+
|
|
199
|
+
let newNumber
|
|
200
|
+
if (latest) {
|
|
201
|
+
const previousHeaders = parseCheckpointPayload(latest.operation)
|
|
202
|
+
if (!previousHeaders) return
|
|
203
|
+
|
|
204
|
+
// x-datadog-parent-id reflects the active span at save time and always differs, so exclude it
|
|
205
|
+
// from the comparison. Capture the previous anchor first to carry it forward on a real update.
|
|
206
|
+
// needsCheckpointUpdate only reads currentHeaders' keys, so deleting it from there is enough.
|
|
207
|
+
const anchoredSpanId = previousHeaders['x-datadog-parent-id']
|
|
208
|
+
delete currentHeaders['x-datadog-parent-id']
|
|
209
|
+
if (!needsCheckpointUpdate(currentHeaders, previousHeaders)) return
|
|
210
|
+
|
|
211
|
+
newNumber = latest.checkpointNumber + 1
|
|
212
|
+
overrideParentId(currentHeaders, anchoredSpanId)
|
|
213
|
+
} else {
|
|
214
|
+
newNumber = 0
|
|
215
|
+
if (firstExecutionSpanId) overrideParentId(currentHeaders, firstExecutionSpanId)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const executionArn = event?.DurableExecutionArn || ''
|
|
219
|
+
await saveCheckpoint(checkpointManager, executionArn, newNumber, currentHeaders)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
saveTraceContextCheckpointIfUpdated,
|
|
224
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Populates the replay and operation_id tags for the op the DurableContextImpl is about to
|
|
7
|
+
* run, deriving both from a single `getNextStepId()` call. `aws.durable.replayed` is always
|
|
8
|
+
* set ('true' when the next stepId already has a SUCCEEDED checkpoint entry, i.e. the op will
|
|
9
|
+
* be served from the SDK's checkpoint). `aws.durable.operation_id` — the 16-hex-char MD5 of
|
|
10
|
+
* the stepId, mirroring the SDK's internal calculation — is only added when a stepId exists.
|
|
11
|
+
* @param {Record<string, string>} meta - The span meta/tags object to populate.
|
|
12
|
+
* @param {object} [ctxImpl] - The DurableContextImpl about to run the op.
|
|
13
|
+
* @returns {void}
|
|
14
|
+
*/
|
|
15
|
+
function addOpMeta (meta, ctxImpl) {
|
|
16
|
+
const stepId = ctxImpl?.getNextStepId?.()
|
|
17
|
+
if (!stepId) {
|
|
18
|
+
meta['aws.durable.replayed'] = 'false'
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
const stepData = ctxImpl?._executionContext?.getStepData?.(stepId)
|
|
22
|
+
meta['aws.durable.replayed'] = String(stepData?.Status === 'SUCCEEDED')
|
|
23
|
+
meta['aws.durable.operation_id'] = createHash('md5').update(stepId).digest('hex').slice(0, 16)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The SDK wraps user errors in typed classes (StepError, ChildContextError, etc.); we follow the
|
|
28
|
+
* `.cause` chain to recover the user's original Error. SDK wrappers expose a string `errorType`
|
|
29
|
+
* field, so the loop stops once we leave the wrapper hierarchy.
|
|
30
|
+
* @param {{ error?: unknown }} ctx
|
|
31
|
+
* @returns {unknown} the unwrapped error, or `ctx.error` unchanged when it isn't an Error
|
|
32
|
+
*/
|
|
33
|
+
function unwrapDurableError (ctx) {
|
|
34
|
+
let err = ctx?.error
|
|
35
|
+
if (!(err instanceof Error)) return err
|
|
36
|
+
|
|
37
|
+
while (typeof err.errorType === 'string' && err.cause instanceof Error) {
|
|
38
|
+
err = err.cause
|
|
39
|
+
}
|
|
40
|
+
return err
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { addOpMeta, unwrapDurableError }
|
|
@@ -172,18 +172,12 @@ class BaseAwsSdkPlugin extends ClientPlugin {
|
|
|
172
172
|
})
|
|
173
173
|
|
|
174
174
|
this.addSub(`apm:aws:request:complete:${this.serviceIdentifier}`, ctx => {
|
|
175
|
-
const { response,
|
|
175
|
+
const { response, currentStore } = ctx
|
|
176
176
|
if (!currentStore) return
|
|
177
177
|
const { span } = currentStore
|
|
178
178
|
if (!span) return
|
|
179
179
|
|
|
180
180
|
storage('legacy').run(currentStore, () => {
|
|
181
|
-
// try to extract DSM context from response if no callback exists as extraction normally happens in CB
|
|
182
|
-
if (!cbExists && this.serviceIdentifier === 'sqs') {
|
|
183
|
-
const params = response.request.params
|
|
184
|
-
const operation = response.request.operation
|
|
185
|
-
this.responseExtractDSMContext(operation, params, response.data ?? response, span)
|
|
186
|
-
}
|
|
187
181
|
this.addResponseTags(span, response)
|
|
188
182
|
|
|
189
183
|
if (this._tracerConfig?.DD_TRACE_AWS_ADD_SPAN_POINTERS) {
|
|
@@ -8,11 +8,18 @@ function recordDataAsString (data) {
|
|
|
8
8
|
return Buffer.isBuffer(data) ? data.toString('utf8') : Buffer.from(data).toString('utf8')
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Caps the promise-path iterator→stream cache so abandoned shard iterators
|
|
12
|
+
// (AWS expires them after 5 minutes) can't grow it without bound. Polling loops
|
|
13
|
+
// delete on consume, so their working set is ~the active shard count.
|
|
14
|
+
const MAX_TRACKED_SHARD_ITERATORS = 1000
|
|
15
|
+
|
|
11
16
|
class Kinesis extends BaseAwsSdkPlugin {
|
|
12
17
|
static id = 'kinesis'
|
|
13
18
|
static peerServicePrecursors = ['streamname']
|
|
14
19
|
static isPayloadReporter = true
|
|
15
20
|
|
|
21
|
+
#shardIteratorStreams = new Map()
|
|
22
|
+
|
|
16
23
|
constructor (...args) {
|
|
17
24
|
super(...args)
|
|
18
25
|
|
|
@@ -20,51 +27,107 @@ class Kinesis extends BaseAwsSdkPlugin {
|
|
|
20
27
|
// in the base class
|
|
21
28
|
this.requestTags = new WeakMap()
|
|
22
29
|
|
|
23
|
-
this.addBind('apm:aws:response:start:kinesis', ctx =>
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
this.addBind('apm:aws:response:start:kinesis', ctx => this.#startResponseSpan(ctx))
|
|
31
|
+
|
|
32
|
+
// Promise / event-emitter calls never publish response:start, so create and finish the
|
|
33
|
+
// consumer span from request:complete instead. Callback calls handle it via the bind above.
|
|
34
|
+
this.addSub('apm:aws:request:complete:kinesis', ctx => {
|
|
35
|
+
if (ctx.cbExists) return
|
|
36
|
+
// v2 nests the SDK payload under response.data; v3 spreads the output onto response.
|
|
37
|
+
const response = ctx.response?.data ?? ctx.response
|
|
38
|
+
const responseCtx = { request: ctx.request, response }
|
|
39
|
+
this.#startResponseSpan(responseCtx)
|
|
40
|
+
if (responseCtx.needsFinish) this.finish(responseCtx)
|
|
41
|
+
// The async store that carries streamName to getRecords on the callback path is
|
|
42
|
+
// absent here, so map each shard iterator to its stream for the DSM topic tag.
|
|
43
|
+
if (this.config.dsmEnabled) this.#trackShardStream(ctx.request, response)
|
|
44
|
+
})
|
|
26
45
|
|
|
27
|
-
|
|
46
|
+
this.addSub('apm:aws:response:finish:kinesis', ctx => {
|
|
47
|
+
if (!ctx.needsFinish) return
|
|
48
|
+
this.finish(ctx)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
28
51
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
/**
|
|
53
|
+
* @param {object} ctx Completion context carrying the SDK request and response.
|
|
54
|
+
*/
|
|
55
|
+
#startResponseSpan (ctx) {
|
|
56
|
+
const { request, response } = ctx
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
let span
|
|
37
|
-
const responseExtraction = this.responseExtract(request.params, request.operation, response)
|
|
38
|
-
if (responseExtraction && responseExtraction.maybeChildOf) {
|
|
39
|
-
ctx.needsFinish = true
|
|
40
|
-
const options = {
|
|
41
|
-
childOf: responseExtraction.maybeChildOf,
|
|
42
|
-
meta: {
|
|
43
|
-
...this.requestTags.get(request),
|
|
44
|
-
'span.kind': 'server',
|
|
45
|
-
},
|
|
46
|
-
integrationName: 'aws-sdk',
|
|
47
|
-
}
|
|
48
|
-
span = plugin.startSpan('aws.response', options, ctx)
|
|
49
|
-
store = ctx.currentStore
|
|
50
|
-
}
|
|
58
|
+
let store = this._parentMap.get(request)
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
// if we have either of these operations, we want to store the streamName param
|
|
61
|
+
// since it is not typically available during get/put records requests
|
|
62
|
+
if (request.operation === 'getShardIterator' || request.operation === 'listShards') {
|
|
63
|
+
return this.storeStreamName(request.params, request.operation, store)
|
|
64
|
+
}
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
if (request.operation === 'getRecords') {
|
|
67
|
+
let span
|
|
68
|
+
const responseExtraction = this.responseExtract(request.params, request.operation, response)
|
|
69
|
+
if (responseExtraction && responseExtraction.maybeChildOf) {
|
|
70
|
+
ctx.needsFinish = true
|
|
71
|
+
const options = {
|
|
72
|
+
childOf: responseExtraction.maybeChildOf,
|
|
73
|
+
meta: {
|
|
74
|
+
...this.requestTags.get(request),
|
|
75
|
+
'span.kind': 'server',
|
|
76
|
+
},
|
|
77
|
+
integrationName: 'aws-sdk',
|
|
78
|
+
}
|
|
79
|
+
span = this.startSpan('aws.response', options, ctx)
|
|
80
|
+
store = ctx.currentStore
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
if (this.config.dsmEnabled) {
|
|
84
|
+
// streamName rides the async store on the callback path; the promise path has no
|
|
85
|
+
// such link, so fall back to the iterator the producer returned.
|
|
86
|
+
const streamName = store?.streamName ?? this.#shardIteratorStreams.get(request.params.ShardIterator)
|
|
87
|
+
this.responseExtractDSMContext(request.operation, request.params, response, span || null, { streamName })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
63
90
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
return store
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {object} request SDK request; reads `operation` and `params`.
|
|
96
|
+
* @param {object} response SDK output; reads `ShardIterator` / `NextShardIterator`.
|
|
97
|
+
*/
|
|
98
|
+
#trackShardStream (request, response) {
|
|
99
|
+
if (request.operation === 'getShardIterator') {
|
|
100
|
+
this.#rememberShardStream(response?.ShardIterator, request.params?.StreamName)
|
|
101
|
+
} else if (request.operation === 'getRecords') {
|
|
102
|
+
this.#advanceShardStream(request.params?.ShardIterator, response?.NextShardIterator)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} [iterator] Shard iterator the producer returned.
|
|
108
|
+
* @param {string} [streamName] Stream the iterator belongs to.
|
|
109
|
+
*/
|
|
110
|
+
#rememberShardStream (iterator, streamName) {
|
|
111
|
+
if (!iterator || streamName === undefined) return
|
|
112
|
+
// FIFO-evict the oldest entry (Map keeps insertion order) when the cap is hit; only
|
|
113
|
+
// abandoned iterators get here, so no realistic test drives the cap (eviction ignored).
|
|
114
|
+
/* istanbul ignore if */
|
|
115
|
+
if (this.#shardIteratorStreams.size >= MAX_TRACKED_SHARD_ITERATORS) {
|
|
116
|
+
this.#shardIteratorStreams.delete(this.#shardIteratorStreams.keys().next().value)
|
|
117
|
+
}
|
|
118
|
+
this.#shardIteratorStreams.set(iterator, streamName)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} [consumedIterator] Iterator just passed to getRecords.
|
|
123
|
+
* @param {string} [nextIterator] NextShardIterator for the following poll.
|
|
124
|
+
*/
|
|
125
|
+
#advanceShardStream (consumedIterator, nextIterator) {
|
|
126
|
+
const streamName = this.#shardIteratorStreams.get(consumedIterator)
|
|
127
|
+
if (streamName === undefined) return
|
|
128
|
+
this.#shardIteratorStreams.delete(consumedIterator)
|
|
129
|
+
// carry the stream onto the next iterator so the polling loop keeps its topic
|
|
130
|
+
if (nextIterator) this.#rememberShardStream(nextIterator, streamName)
|
|
68
131
|
}
|
|
69
132
|
|
|
70
133
|
generateTags (params, operation, response) {
|
|
@@ -54,47 +54,64 @@ class Sqs extends BaseAwsSdkPlugin {
|
|
|
54
54
|
// in the base class
|
|
55
55
|
this.requestTags = new WeakMap()
|
|
56
56
|
|
|
57
|
-
this.addBind('apm:aws:response:start:sqs', ctx =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
this.addBind('apm:aws:response:start:sqs', ctx => this.#startResponseSpan(ctx))
|
|
58
|
+
|
|
59
|
+
// No-callback receives (promises, event emitters) never publish response:start, so link and
|
|
60
|
+
// finish the consumer span here instead. Callback paths reach the same logic via the bind above.
|
|
61
|
+
this.addSub('apm:aws:request:complete:sqs', ctx => {
|
|
62
|
+
if (ctx.cbExists) return
|
|
63
|
+
// v2 nests the SDK payload under response.data; v3 spreads the output onto response.
|
|
64
|
+
const responseCtx = { request: ctx.request, response: ctx.response?.data ?? ctx.response }
|
|
65
|
+
this.#startResponseSpan(responseCtx)
|
|
66
|
+
if (responseCtx.needsFinish) this.finish(responseCtx)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
this.addSub('apm:aws:response:finish:sqs', ctx => {
|
|
70
|
+
if (!ctx.needsFinish) return
|
|
71
|
+
this.finish(ctx)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#startResponseSpan (ctx) {
|
|
76
|
+
const { request, response } = ctx
|
|
77
|
+
const contextExtraction = this.responseExtract(request.params, request.operation, response)
|
|
78
|
+
|
|
79
|
+
let store = this._parentMap.get(request)
|
|
80
|
+
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) {
|
|
70
93
|
ctx.needsFinish = true
|
|
71
94
|
const options = {
|
|
72
95
|
childOf: contextExtraction.datadogContext,
|
|
73
96
|
meta: {
|
|
74
|
-
...
|
|
97
|
+
...requestTags,
|
|
75
98
|
'span.kind': 'server',
|
|
76
99
|
},
|
|
77
100
|
integrationName: 'aws-sdk',
|
|
78
101
|
}
|
|
79
|
-
parsedMessageAttributes = contextExtraction.parsedAttributes
|
|
80
102
|
span = this.startSpan('aws.response', options, ctx)
|
|
81
103
|
store = ctx.currentStore
|
|
82
104
|
}
|
|
83
105
|
}
|
|
106
|
+
}
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return store
|
|
92
|
-
})
|
|
108
|
+
// 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
|
+
)
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
if (!ctx.needsFinish) return
|
|
96
|
-
this.finish(ctx)
|
|
97
|
-
})
|
|
114
|
+
return store
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
operationFromRequest (request) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../../dd-trace/src/log')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} BullmqJobShape
|
|
7
|
+
* @property {string} [name]
|
|
8
|
+
* @property {unknown} [data]
|
|
9
|
+
* @property {unknown} [opts]
|
|
10
|
+
* @property {string} [queueName]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {(job: BullmqJobShape) => boolean} BullmqFilter
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a user-provided filter from plugin config. Returns `undefined` when
|
|
19
|
+
* no filter is configured so callers can short-circuit the filtering path on a
|
|
20
|
+
* cheap truthy check. If `producerFilter` is present but not a function, log
|
|
21
|
+
* an error and fall back to no filter.
|
|
22
|
+
*
|
|
23
|
+
* @param {{ producerFilter?: unknown }} config Plugin config that may carry a `producerFilter`.
|
|
24
|
+
* @returns {BullmqFilter | undefined}
|
|
25
|
+
*/
|
|
26
|
+
function getFilter (config) {
|
|
27
|
+
if (typeof config?.producerFilter === 'function') {
|
|
28
|
+
return /** @type {BullmqFilter} */ (config.producerFilter)
|
|
29
|
+
}
|
|
30
|
+
if (config?.producerFilter !== undefined) {
|
|
31
|
+
log.error('Expected `producerFilter` to be a function. Ignoring.')
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { getFilter }
|