dd-trace 5.33.1 → 5.35.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/LICENSE-3rdparty.csv +1 -2
- package/package.json +2 -3
- package/packages/datadog-instrumentations/src/aws-sdk.js +16 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/passport.js +45 -0
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +33 -6
- package/packages/datadog-plugin-dd-trace-api/src/index.js +120 -0
- package/packages/datadog-plugin-graphql/src/execute.js +6 -0
- package/packages/datadog-plugin-graphql/src/utils.js +40 -0
- package/packages/datadog-plugin-graphql/src/validate.js +6 -0
- package/packages/dd-trace/src/appsec/blocking.js +4 -1
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +19 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +2 -1
- package/packages/dd-trace/src/appsec/index.js +17 -0
- package/packages/dd-trace/src/appsec/sdk/set_user.js +9 -0
- package/packages/dd-trace/src/appsec/sdk/user_blocking.js +1 -0
- package/packages/dd-trace/src/appsec/telemetry.js +10 -0
- package/packages/dd-trace/src/appsec/user_tracking.js +32 -6
- package/packages/dd-trace/src/config.js +8 -0
- package/packages/dd-trace/src/debugger/devtools_client/state.js +72 -17
- package/packages/dd-trace/src/id.js +0 -2
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +48 -3
- package/packages/dd-trace/src/plugin_manager.js +12 -1
- package/packages/dd-trace/src/plugins/index.js +1 -0
package/LICENSE-3rdparty.csv
CHANGED
|
@@ -35,6 +35,7 @@ dev,@apollo/server,MIT,Copyright (c) 2016-2020 Apollo Graph, Inc. (Formerly Mete
|
|
|
35
35
|
dev,@types/node,MIT,Copyright Authors
|
|
36
36
|
dev,@eslint/eslintrc,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
|
|
37
37
|
dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
|
|
38
|
+
dev,@msgpack/msgpack,ISC,Copyright 2019 The MessagePack Community
|
|
38
39
|
dev,@stylistic/eslint-plugin-js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
|
|
39
40
|
dev,autocannon,MIT,Copyright 2016 Matteo Collina
|
|
40
41
|
dev,aws-sdk,Apache 2.0,Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
@@ -58,12 +59,10 @@ dev,get-port,MIT,Copyright Sindre Sorhus
|
|
|
58
59
|
dev,glob,ISC,Copyright Isaac Z. Schlueter and Contributors
|
|
59
60
|
dev,globals,MIT,Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
|
60
61
|
dev,graphql,MIT,Copyright 2015 Facebook Inc.
|
|
61
|
-
dev,int64-buffer,MIT,Copyright 2015-2016 Yusuke Kawasaki
|
|
62
62
|
dev,jszip,MIT,Copyright 2015-2016 Stuart Knightley and contributors
|
|
63
63
|
dev,knex,MIT,Copyright (c) 2013-present Tim Griesser
|
|
64
64
|
dev,mkdirp,MIT,Copyright 2010 James Halliday
|
|
65
65
|
dev,mocha,MIT,Copyright 2011-2018 JS Foundation and contributors https://js.foundation
|
|
66
|
-
dev,msgpack-lite,MIT,Copyright 2015 Yusuke Kawasaki
|
|
67
66
|
dev,multer,MIT,Copyright 2014 Hage Yaapa
|
|
68
67
|
dev,nock,MIT,Copyright 2017 Pedro Teixeira and other contributors
|
|
69
68
|
dev,nyc,ISC,Copyright 2015 Contributors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.35.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"@apollo/server": "^4.11.0",
|
|
119
119
|
"@eslint/eslintrc": "^3.1.0",
|
|
120
120
|
"@eslint/js": "^9.11.1",
|
|
121
|
+
"@msgpack/msgpack": "^3.0.0-beta3",
|
|
121
122
|
"@stylistic/eslint-plugin-js": "^2.8.0",
|
|
122
123
|
"@types/node": "^16.0.0",
|
|
123
124
|
"autocannon": "^4.5.2",
|
|
@@ -142,12 +143,10 @@
|
|
|
142
143
|
"glob": "^7.1.6",
|
|
143
144
|
"globals": "^15.10.0",
|
|
144
145
|
"graphql": "0.13.2",
|
|
145
|
-
"int64-buffer": "^0.1.9",
|
|
146
146
|
"jszip": "^3.5.0",
|
|
147
147
|
"knex": "^2.4.2",
|
|
148
148
|
"mkdirp": "^3.0.1",
|
|
149
149
|
"mocha": "^10",
|
|
150
|
-
"msgpack-lite": "^0.1.26",
|
|
151
150
|
"multer": "^1.4.5-lts.1",
|
|
152
151
|
"nock": "^11.3.3",
|
|
153
152
|
"nyc": "^15.1.0",
|
|
@@ -40,6 +40,18 @@ function wrapRequest (send) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function wrapDeserialize (deserialize, channelSuffix) {
|
|
44
|
+
const headersCh = channel(`apm:aws:response:deserialize:${channelSuffix}`)
|
|
45
|
+
|
|
46
|
+
return function (response) {
|
|
47
|
+
if (headersCh.hasSubscribers) {
|
|
48
|
+
headersCh.publish({ headers: response.headers })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return deserialize.apply(this, arguments)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
function wrapSmithySend (send) {
|
|
44
56
|
return function (command, ...args) {
|
|
45
57
|
const cb = args[args.length - 1]
|
|
@@ -61,6 +73,10 @@ function wrapSmithySend (send) {
|
|
|
61
73
|
const responseStartChannel = channel(`apm:aws:response:start:${channelSuffix}`)
|
|
62
74
|
const responseFinishChannel = channel(`apm:aws:response:finish:${channelSuffix}`)
|
|
63
75
|
|
|
76
|
+
if (typeof command.deserialize === 'function') {
|
|
77
|
+
shimmer.wrap(command, 'deserialize', deserialize => wrapDeserialize(deserialize, channelSuffix))
|
|
78
|
+
}
|
|
79
|
+
|
|
64
80
|
return innerAr.runInAsyncScope(() => {
|
|
65
81
|
startCh.publish({
|
|
66
82
|
serviceIdentifier,
|
|
@@ -102,6 +102,7 @@ module.exports = {
|
|
|
102
102
|
oracledb: () => require('../oracledb'),
|
|
103
103
|
openai: () => require('../openai'),
|
|
104
104
|
paperplane: () => require('../paperplane'),
|
|
105
|
+
passport: () => require('../passport'),
|
|
105
106
|
'passport-http': () => require('../passport-http'),
|
|
106
107
|
'passport-local': () => require('../passport-local'),
|
|
107
108
|
pg: () => require('../pg'),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const shimmer = require('../../datadog-shimmer')
|
|
4
|
+
const { channel, addHook } = require('./helpers/instrument')
|
|
5
|
+
|
|
6
|
+
const onPassportDeserializeUserChannel = channel('datadog:passport:deserializeUser:finish')
|
|
7
|
+
|
|
8
|
+
function wrapDone (done) {
|
|
9
|
+
return function wrappedDone (err, user) {
|
|
10
|
+
if (!err && user) {
|
|
11
|
+
const abortController = new AbortController()
|
|
12
|
+
|
|
13
|
+
onPassportDeserializeUserChannel.publish({ user, abortController })
|
|
14
|
+
|
|
15
|
+
if (abortController.signal.aborted) return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return done.apply(this, arguments)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function wrapDeserializeUser (deserializeUser) {
|
|
23
|
+
return function wrappedDeserializeUser (fn, req, done) {
|
|
24
|
+
if (typeof fn === 'function') return deserializeUser.apply(this, arguments)
|
|
25
|
+
|
|
26
|
+
if (typeof req === 'function') {
|
|
27
|
+
done = req
|
|
28
|
+
arguments[1] = wrapDone(done)
|
|
29
|
+
} else {
|
|
30
|
+
arguments[2] = wrapDone(done)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return deserializeUser.apply(this, arguments)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addHook({
|
|
38
|
+
name: 'passport',
|
|
39
|
+
file: 'lib/authenticator.js',
|
|
40
|
+
versions: ['>=0.3.0']
|
|
41
|
+
}, Authenticator => {
|
|
42
|
+
shimmer.wrap(Authenticator.prototype, 'deserializeUser', wrapDeserializeUser)
|
|
43
|
+
|
|
44
|
+
return Authenticator
|
|
45
|
+
})
|
|
@@ -24,11 +24,23 @@ const PROVIDER = {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
class Generation {
|
|
27
|
-
constructor ({
|
|
27
|
+
constructor ({
|
|
28
|
+
message = '',
|
|
29
|
+
finishReason = '',
|
|
30
|
+
choiceId = '',
|
|
31
|
+
role,
|
|
32
|
+
inputTokens,
|
|
33
|
+
outputTokens
|
|
34
|
+
} = {}) {
|
|
28
35
|
// stringify message as it could be a single generated message as well as a list of embeddings
|
|
29
36
|
this.message = typeof message === 'string' ? message : JSON.stringify(message) || ''
|
|
30
37
|
this.finishReason = finishReason || ''
|
|
31
38
|
this.choiceId = choiceId || undefined
|
|
39
|
+
this.role = role
|
|
40
|
+
this.usage = {
|
|
41
|
+
inputTokens,
|
|
42
|
+
outputTokens
|
|
43
|
+
}
|
|
32
44
|
}
|
|
33
45
|
}
|
|
34
46
|
|
|
@@ -202,9 +214,12 @@ function extractTextAndResponseReason (response, provider, modelName) {
|
|
|
202
214
|
if (generations.length > 0) {
|
|
203
215
|
const generation = generations[0]
|
|
204
216
|
return new Generation({
|
|
205
|
-
message: generation.message,
|
|
217
|
+
message: generation.message.content,
|
|
206
218
|
finishReason: generation.finish_reason,
|
|
207
|
-
choiceId: shouldSetChoiceIds ? generation.id : undefined
|
|
219
|
+
choiceId: shouldSetChoiceIds ? generation.id : undefined,
|
|
220
|
+
role: generation.message.role,
|
|
221
|
+
inputTokens: body.usage?.prompt_tokens,
|
|
222
|
+
outputTokens: body.usage?.completion_tokens
|
|
208
223
|
})
|
|
209
224
|
}
|
|
210
225
|
}
|
|
@@ -214,7 +229,9 @@ function extractTextAndResponseReason (response, provider, modelName) {
|
|
|
214
229
|
return new Generation({
|
|
215
230
|
message: completion.data?.text,
|
|
216
231
|
finishReason: completion?.finishReason,
|
|
217
|
-
choiceId: shouldSetChoiceIds ? completion?.id : undefined
|
|
232
|
+
choiceId: shouldSetChoiceIds ? completion?.id : undefined,
|
|
233
|
+
inputTokens: body.usage?.prompt_tokens,
|
|
234
|
+
outputTokens: body.usage?.completion_tokens
|
|
218
235
|
})
|
|
219
236
|
}
|
|
220
237
|
return new Generation()
|
|
@@ -226,7 +243,12 @@ function extractTextAndResponseReason (response, provider, modelName) {
|
|
|
226
243
|
const results = body.results || []
|
|
227
244
|
if (results.length > 0) {
|
|
228
245
|
const result = results[0]
|
|
229
|
-
return new Generation({
|
|
246
|
+
return new Generation({
|
|
247
|
+
message: result.outputText,
|
|
248
|
+
finishReason: result.completionReason,
|
|
249
|
+
inputTokens: body.inputTextTokenCount,
|
|
250
|
+
outputTokens: result.tokenCount
|
|
251
|
+
})
|
|
230
252
|
}
|
|
231
253
|
break
|
|
232
254
|
}
|
|
@@ -252,7 +274,12 @@ function extractTextAndResponseReason (response, provider, modelName) {
|
|
|
252
274
|
break
|
|
253
275
|
}
|
|
254
276
|
case PROVIDER.META: {
|
|
255
|
-
return new Generation({
|
|
277
|
+
return new Generation({
|
|
278
|
+
message: body.generation,
|
|
279
|
+
finishReason: body.stop_reason,
|
|
280
|
+
inputTokens: body.prompt_token_count,
|
|
281
|
+
outputTokens: body.generation_token_count
|
|
282
|
+
})
|
|
256
283
|
}
|
|
257
284
|
case PROVIDER.MISTRAL: {
|
|
258
285
|
const mistralGenerations = body.outputs || []
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Plugin = require('../../dd-trace/src/plugins/plugin')
|
|
4
|
+
const telemetryMetrics = require('../../dd-trace/src/telemetry/metrics')
|
|
5
|
+
const apiMetrics = telemetryMetrics.manager.namespace('tracers')
|
|
6
|
+
|
|
7
|
+
// api ==> here
|
|
8
|
+
const objectMap = new WeakMap()
|
|
9
|
+
|
|
10
|
+
const injectionEnabledTag =
|
|
11
|
+
`injection_enabled:${process.env.DD_INJECTION_ENABLED ? 'yes' : 'no'}`
|
|
12
|
+
|
|
13
|
+
module.exports = class DdTraceApiPlugin extends Plugin {
|
|
14
|
+
static get id () {
|
|
15
|
+
return 'dd-trace-api'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
constructor (...args) {
|
|
19
|
+
super(...args)
|
|
20
|
+
|
|
21
|
+
const tracer = this._tracer
|
|
22
|
+
|
|
23
|
+
this.addSub('datadog-api:v1:tracerinit', ({ proxy }) => {
|
|
24
|
+
const proxyVal = proxy()
|
|
25
|
+
objectMap.set(proxyVal, tracer)
|
|
26
|
+
objectMap.set(proxyVal.appsec, tracer.appsec)
|
|
27
|
+
objectMap.set(proxyVal.dogstatsd, tracer.dogstatsd)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const handleEvent = (name) => {
|
|
31
|
+
const counter = apiMetrics.count('dd_trace_api.called', [
|
|
32
|
+
`name:${name.replaceAll(':', '.')}`,
|
|
33
|
+
'api_version:v1',
|
|
34
|
+
injectionEnabledTag
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
// For v1, APIs are 1:1 with their internal equivalents, so we can just
|
|
38
|
+
// call the internal method directly. That's what we do here unless we
|
|
39
|
+
// want to override. As the API evolves, this may change.
|
|
40
|
+
this.addSub(`datadog-api:v1:${name}`, ({ self, args, ret, proxy, revProxy }) => {
|
|
41
|
+
counter.inc()
|
|
42
|
+
|
|
43
|
+
if (name.includes(':')) {
|
|
44
|
+
name = name.split(':').pop()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (objectMap.has(self)) {
|
|
48
|
+
self = objectMap.get(self)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < args.length; i++) {
|
|
52
|
+
if (objectMap.has(args[i])) {
|
|
53
|
+
args[i] = objectMap.get(args[i])
|
|
54
|
+
}
|
|
55
|
+
if (typeof args[i] === 'function') {
|
|
56
|
+
const orig = args[i]
|
|
57
|
+
args[i] = (...fnArgs) => {
|
|
58
|
+
for (let j = 0; j < fnArgs.length; j++) {
|
|
59
|
+
if (revProxy && revProxy[j]) {
|
|
60
|
+
const proxyVal = revProxy[j]()
|
|
61
|
+
objectMap.set(proxyVal, fnArgs[j])
|
|
62
|
+
fnArgs[j] = proxyVal
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// TODO do we need to apply(this, ...) here?
|
|
66
|
+
return orig(...fnArgs)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
ret.value = self[name](...args)
|
|
73
|
+
if (proxy) {
|
|
74
|
+
const proxyVal = proxy()
|
|
75
|
+
objectMap.set(proxyVal, ret.value)
|
|
76
|
+
ret.value = proxyVal
|
|
77
|
+
} else if (ret.value && typeof ret.value === 'object') {
|
|
78
|
+
throw new TypeError(`Objects need proxies when returned via API (${name})`)
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
ret.error = e
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// handleEvent('configure')
|
|
87
|
+
handleEvent('startSpan')
|
|
88
|
+
handleEvent('wrap')
|
|
89
|
+
handleEvent('trace')
|
|
90
|
+
handleEvent('inject')
|
|
91
|
+
handleEvent('extract')
|
|
92
|
+
handleEvent('getRumData')
|
|
93
|
+
handleEvent('profilerStarted')
|
|
94
|
+
handleEvent('context:toTraceId')
|
|
95
|
+
handleEvent('context:toSpanId')
|
|
96
|
+
handleEvent('context:toTraceparent')
|
|
97
|
+
handleEvent('span:context')
|
|
98
|
+
handleEvent('span:setTag')
|
|
99
|
+
handleEvent('span:addTags')
|
|
100
|
+
handleEvent('span:finish')
|
|
101
|
+
handleEvent('span:addLink')
|
|
102
|
+
handleEvent('scope')
|
|
103
|
+
handleEvent('scope:activate')
|
|
104
|
+
handleEvent('scope:active')
|
|
105
|
+
handleEvent('scope:bind')
|
|
106
|
+
handleEvent('appsec:blockRequest')
|
|
107
|
+
handleEvent('appsec:isUserBlocked')
|
|
108
|
+
handleEvent('appsec:setUser')
|
|
109
|
+
handleEvent('appsec:trackCustomEvent')
|
|
110
|
+
handleEvent('appsec:trackUserLoginFailureEvent')
|
|
111
|
+
handleEvent('appsec:trackUserLoginSuccessEvent')
|
|
112
|
+
handleEvent('dogstatsd:decrement')
|
|
113
|
+
handleEvent('dogstatsd:distribution')
|
|
114
|
+
handleEvent('dogstatsd:flush')
|
|
115
|
+
handleEvent('dogstatsd:gauge')
|
|
116
|
+
handleEvent('dogstatsd:histogram')
|
|
117
|
+
handleEvent('dogstatsd:increment')
|
|
118
|
+
handleEvent('use')
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
4
|
+
const { extractErrorIntoSpanEvent } = require('./utils')
|
|
4
5
|
|
|
5
6
|
let tools
|
|
6
7
|
|
|
@@ -34,6 +35,11 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
34
35
|
finish ({ res, args }) {
|
|
35
36
|
const span = this.activeSpan
|
|
36
37
|
this.config.hooks.execute(span, args, res)
|
|
38
|
+
if (res?.errors) {
|
|
39
|
+
for (const err of res.errors) {
|
|
40
|
+
extractErrorIntoSpanEvent(this._tracerConfig, span, err)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
37
43
|
super.finish()
|
|
38
44
|
}
|
|
39
45
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function extractErrorIntoSpanEvent (config, span, exc) {
|
|
2
|
+
const attributes = {}
|
|
3
|
+
|
|
4
|
+
if (exc.name) {
|
|
5
|
+
attributes.type = exc.name
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (exc.stack) {
|
|
9
|
+
attributes.stacktrace = exc.stack
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (exc.locations) {
|
|
13
|
+
attributes.locations = []
|
|
14
|
+
for (const location of exc.locations) {
|
|
15
|
+
attributes.locations.push(`${location.line}:${location.column}`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (exc.path) {
|
|
20
|
+
attributes.path = exc.path.map(String)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (exc.message) {
|
|
24
|
+
attributes.message = exc.message
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (config.graphqlErrorExtensions) {
|
|
28
|
+
for (const ext of config.graphqlErrorExtensions) {
|
|
29
|
+
if (exc.extensions?.[ext]) {
|
|
30
|
+
attributes[`extensions.${ext}`] = exc.extensions[ext].toString()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
span.addEvent('dd.graphql.query.error', attributes, Date.now())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
extractErrorIntoSpanEvent
|
|
40
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
4
|
+
const { extractErrorIntoSpanEvent } = require('./utils')
|
|
4
5
|
|
|
5
6
|
class GraphQLValidatePlugin extends TracingPlugin {
|
|
6
7
|
static get id () { return 'graphql' }
|
|
@@ -21,6 +22,11 @@ class GraphQLValidatePlugin extends TracingPlugin {
|
|
|
21
22
|
finish ({ document, errors }) {
|
|
22
23
|
const span = this.activeSpan
|
|
23
24
|
this.config.hooks.validate(span, document, errors)
|
|
25
|
+
if (errors) {
|
|
26
|
+
for (const err of errors) {
|
|
27
|
+
extractErrorIntoSpanEvent(this._tracerConfig, span, err)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
24
30
|
super.finish()
|
|
25
31
|
}
|
|
26
32
|
}
|
|
@@ -115,7 +115,10 @@ function block (req, res, rootSpan, abortController, actionParameters = defaultB
|
|
|
115
115
|
res.removeHeader(headerName)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
res.writeHead(statusCode, headers)
|
|
118
|
+
res.writeHead(statusCode, headers)
|
|
119
|
+
|
|
120
|
+
// this is needed to call the original end method, since express-session replaces it
|
|
121
|
+
res.constructor.prototype.end.call(res, body)
|
|
119
122
|
|
|
120
123
|
responseBlockedSet.add(res)
|
|
121
124
|
|
|
@@ -14,6 +14,7 @@ module.exports = {
|
|
|
14
14
|
incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
|
|
15
15
|
incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
|
|
16
16
|
passportVerify: dc.channel('datadog:passport:verify:finish'),
|
|
17
|
+
passportUser: dc.channel('datadog:passport:deserializeUser:finish'),
|
|
17
18
|
queryParser: dc.channel('datadog:query:read:finish'),
|
|
18
19
|
setCookieChannel: dc.channel('datadog:iast:set-cookie'),
|
|
19
20
|
nextBodyParsed: dc.channel('apm:next:body-parsed'),
|
|
@@ -2,14 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
const InjectionAnalyzer = require('./injection-analyzer')
|
|
4
4
|
const { CODE_INJECTION } = require('../vulnerabilities')
|
|
5
|
+
const { INSTRUMENTED_SINK } = require('../telemetry/iast-metric')
|
|
6
|
+
const { storage } = require('../../../../../datadog-core')
|
|
7
|
+
const { getIastContext } = require('../iast-context')
|
|
5
8
|
|
|
6
9
|
class CodeInjectionAnalyzer extends InjectionAnalyzer {
|
|
7
10
|
constructor () {
|
|
8
11
|
super(CODE_INJECTION)
|
|
12
|
+
this.evalInstrumentedInc = false
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
onConfigure () {
|
|
12
|
-
this.addSub('datadog:eval:call', ({ script }) =>
|
|
16
|
+
this.addSub('datadog:eval:call', ({ script }) => {
|
|
17
|
+
if (!this.evalInstrumentedInc) {
|
|
18
|
+
const store = storage.getStore()
|
|
19
|
+
const iastContext = getIastContext(store)
|
|
20
|
+
const tags = INSTRUMENTED_SINK.formatTags(CODE_INJECTION)
|
|
21
|
+
|
|
22
|
+
for (const tag of tags) {
|
|
23
|
+
INSTRUMENTED_SINK.inc(iastContext, tag)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.evalInstrumentedInc = true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.analyze(script)
|
|
30
|
+
})
|
|
13
31
|
this.addSub('datadog:vm:run-script:start', ({ code }) => this.analyze(code))
|
|
14
32
|
this.addSub('datadog:vm:source-text-module:start', ({ code }) => this.analyze(code))
|
|
15
33
|
}
|
|
@@ -22,7 +22,8 @@ const EXCLUDED_LOCATIONS = getNodeModulesPaths(
|
|
|
22
22
|
'sqreen/lib/package-reader/index.js',
|
|
23
23
|
'ws/lib/websocket-server.js',
|
|
24
24
|
'google-gax/build/src/grpc.js',
|
|
25
|
-
'cookie-signature/index.js'
|
|
25
|
+
'cookie-signature/index.js',
|
|
26
|
+
'express-session/index.js'
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
const EXCLUDED_PATHS_FROM_STACK = [
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
incomingHttpRequestStart,
|
|
11
11
|
incomingHttpRequestEnd,
|
|
12
12
|
passportVerify,
|
|
13
|
+
passportUser,
|
|
13
14
|
queryParser,
|
|
14
15
|
nextBodyParsed,
|
|
15
16
|
nextQueryParsed,
|
|
@@ -67,6 +68,7 @@ function enable (_config) {
|
|
|
67
68
|
incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
|
|
68
69
|
incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
|
|
69
70
|
passportVerify.subscribe(onPassportVerify) // possible optimization: only subscribe if collection mode is enabled
|
|
71
|
+
passportUser.subscribe(onPassportDeserializeUser)
|
|
70
72
|
queryParser.subscribe(onRequestQueryParsed)
|
|
71
73
|
nextBodyParsed.subscribe(onRequestBodyParsed)
|
|
72
74
|
nextQueryParsed.subscribe(onRequestQueryParsed)
|
|
@@ -197,6 +199,20 @@ function onPassportVerify ({ framework, login, user, success, abortController })
|
|
|
197
199
|
handleResults(results, store.req, store.req.res, rootSpan, abortController)
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
function onPassportDeserializeUser ({ user, abortController }) {
|
|
203
|
+
const store = storage.getStore()
|
|
204
|
+
const rootSpan = store?.req && web.root(store.req)
|
|
205
|
+
|
|
206
|
+
if (!rootSpan) {
|
|
207
|
+
log.warn('[ASM] No rootSpan found in onPassportDeserializeUser')
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const results = UserTracking.trackUser(user, rootSpan)
|
|
212
|
+
|
|
213
|
+
handleResults(results, store.req, store.req.res, rootSpan, abortController)
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
function onRequestQueryParsed ({ req, res, query, abortController }) {
|
|
201
217
|
if (!query || typeof query !== 'object') return
|
|
202
218
|
|
|
@@ -310,6 +326,7 @@ function disable () {
|
|
|
310
326
|
if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator)
|
|
311
327
|
if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(incomingHttpEndTranslator)
|
|
312
328
|
if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
|
|
329
|
+
if (passportUser.hasSubscribers) passportUser.unsubscribe(onPassportDeserializeUser)
|
|
313
330
|
if (queryParser.hasSubscribers) queryParser.unsubscribe(onRequestQueryParsed)
|
|
314
331
|
if (nextBodyParsed.hasSubscribers) nextBodyParsed.unsubscribe(onRequestBodyParsed)
|
|
315
332
|
if (nextQueryParsed.hasSubscribers) nextQueryParsed.unsubscribe(onRequestQueryParsed)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { getRootSpan } = require('./utils')
|
|
4
4
|
const log = require('../../log')
|
|
5
|
+
const waf = require('../waf')
|
|
6
|
+
const addresses = require('../addresses')
|
|
5
7
|
|
|
6
8
|
function setUserTags (user, rootSpan) {
|
|
7
9
|
for (const k of Object.keys(user)) {
|
|
@@ -22,6 +24,13 @@ function setUser (tracer, user) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
setUserTags(user, rootSpan)
|
|
27
|
+
rootSpan.setTag('_dd.appsec.user.collection_mode', 'sdk')
|
|
28
|
+
|
|
29
|
+
waf.run({
|
|
30
|
+
persistent: {
|
|
31
|
+
[addresses.USER_ID]: '' + user.id
|
|
32
|
+
}
|
|
33
|
+
})
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
module.exports = {
|
|
@@ -23,6 +23,7 @@ function checkUserAndSetUser (tracer, user) {
|
|
|
23
23
|
if (rootSpan) {
|
|
24
24
|
if (!rootSpan.context()._tags['usr.id']) {
|
|
25
25
|
setUserTags(user, rootSpan)
|
|
26
|
+
rootSpan.setTag('_dd.appsec.user.collection_mode', 'sdk')
|
|
26
27
|
}
|
|
27
28
|
} else {
|
|
28
29
|
log.warn('[ASM] Root span not available in isUserBlocked')
|
|
@@ -186,6 +186,15 @@ function incrementMissingUserLoginMetric (framework, eventType) {
|
|
|
186
186
|
}).inc()
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
function incrementMissingUserIdMetric (framework, eventType) {
|
|
190
|
+
if (!enabled) return
|
|
191
|
+
|
|
192
|
+
appsecMetrics.count('instrum.user_auth.missing_user_id', {
|
|
193
|
+
framework,
|
|
194
|
+
event_type: eventType
|
|
195
|
+
}).inc()
|
|
196
|
+
}
|
|
197
|
+
|
|
189
198
|
function getRequestMetrics (req) {
|
|
190
199
|
if (req) {
|
|
191
200
|
const store = getStore(req)
|
|
@@ -203,6 +212,7 @@ module.exports = {
|
|
|
203
212
|
incrementWafUpdatesMetric,
|
|
204
213
|
incrementWafRequestsMetric,
|
|
205
214
|
incrementMissingUserLoginMetric,
|
|
215
|
+
incrementMissingUserIdMetric,
|
|
206
216
|
|
|
207
217
|
getRequestMetrics
|
|
208
218
|
}
|
|
@@ -53,6 +53,8 @@ function obfuscateIfNeeded (str) {
|
|
|
53
53
|
function getUserId (user) {
|
|
54
54
|
if (!user) return
|
|
55
55
|
|
|
56
|
+
// should we iterate on user keys instead to be case insensitive ?
|
|
57
|
+
// but if we iterate over user then we're missing the inherited props ?
|
|
56
58
|
for (const field of USER_ID_FIELDS) {
|
|
57
59
|
let id = user[field]
|
|
58
60
|
|
|
@@ -73,11 +75,6 @@ function getUserId (user) {
|
|
|
73
75
|
function trackLogin (framework, login, user, success, rootSpan) {
|
|
74
76
|
if (!collectionMode || collectionMode === 'disabled') return
|
|
75
77
|
|
|
76
|
-
if (!rootSpan) {
|
|
77
|
-
log.error('[ASM] No rootSpan found in AppSec trackLogin')
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
|
|
81
78
|
if (typeof login !== 'string') {
|
|
82
79
|
log.error('[ASM] Invalid login provided to AppSec trackLogin')
|
|
83
80
|
|
|
@@ -162,7 +159,36 @@ function trackLogin (framework, login, user, success, rootSpan) {
|
|
|
162
159
|
return waf.run({ persistent })
|
|
163
160
|
}
|
|
164
161
|
|
|
162
|
+
function trackUser (user, rootSpan) {
|
|
163
|
+
if (!collectionMode || collectionMode === 'disabled') return
|
|
164
|
+
|
|
165
|
+
const userId = getUserId(user)
|
|
166
|
+
if (!userId) {
|
|
167
|
+
log.error('[ASM] No valid user ID found in AppSec trackUser')
|
|
168
|
+
telemetry.incrementMissingUserIdMetric('passport', 'authenticated_request')
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
rootSpan.setTag('_dd.appsec.usr.id', userId)
|
|
173
|
+
|
|
174
|
+
const isSdkCalled = rootSpan.context()._tags['_dd.appsec.user.collection_mode'] === 'sdk'
|
|
175
|
+
// do not override SDK
|
|
176
|
+
if (!isSdkCalled) {
|
|
177
|
+
rootSpan.addTags({
|
|
178
|
+
'usr.id': userId,
|
|
179
|
+
'_dd.appsec.user.collection_mode': collectionMode
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return waf.run({
|
|
183
|
+
persistent: {
|
|
184
|
+
[addresses.USER_ID]: userId
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
165
190
|
module.exports = {
|
|
166
191
|
setCollectionMode,
|
|
167
|
-
trackLogin
|
|
192
|
+
trackLogin,
|
|
193
|
+
trackUser
|
|
168
194
|
}
|
|
@@ -482,6 +482,7 @@ class Config {
|
|
|
482
482
|
this._setValue(defaults, 'flushInterval', 2000)
|
|
483
483
|
this._setValue(defaults, 'flushMinSpans', 1000)
|
|
484
484
|
this._setValue(defaults, 'gitMetadataEnabled', true)
|
|
485
|
+
this._setValue(defaults, 'graphqlErrorExtensions', [])
|
|
485
486
|
this._setValue(defaults, 'grpc.client.error.statuses', GRPC_CLIENT_ERROR_STATUSES)
|
|
486
487
|
this._setValue(defaults, 'grpc.server.error.statuses', GRPC_SERVER_ERROR_STATUSES)
|
|
487
488
|
this._setValue(defaults, 'headerTags', [])
|
|
@@ -521,6 +522,7 @@ class Config {
|
|
|
521
522
|
this._setValue(defaults, 'lookup', undefined)
|
|
522
523
|
this._setValue(defaults, 'inferredProxyServicesEnabled', false)
|
|
523
524
|
this._setValue(defaults, 'memcachedCommandEnabled', false)
|
|
525
|
+
this._setValue(defaults, 'middlewareTracingEnabled', true)
|
|
524
526
|
this._setValue(defaults, 'openAiLogsEnabled', false)
|
|
525
527
|
this._setValue(defaults, 'openai.spanCharLimit', 128)
|
|
526
528
|
this._setValue(defaults, 'peerServiceMapping', {})
|
|
@@ -669,9 +671,11 @@ class Config {
|
|
|
669
671
|
DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED,
|
|
670
672
|
DD_TRACE_GIT_METADATA_ENABLED,
|
|
671
673
|
DD_TRACE_GLOBAL_TAGS,
|
|
674
|
+
DD_TRACE_GRAPHQL_ERROR_EXTENSIONS,
|
|
672
675
|
DD_TRACE_HEADER_TAGS,
|
|
673
676
|
DD_TRACE_LEGACY_BAGGAGE_ENABLED,
|
|
674
677
|
DD_TRACE_MEMCACHED_COMMAND_ENABLED,
|
|
678
|
+
DD_TRACE_MIDDLEWARE_TRACING_ENABLED,
|
|
675
679
|
DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP,
|
|
676
680
|
DD_TRACE_PARTIAL_FLUSH_MIN_SPANS,
|
|
677
681
|
DD_TRACE_PEER_SERVICE_MAPPING,
|
|
@@ -804,6 +808,7 @@ class Config {
|
|
|
804
808
|
this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION)
|
|
805
809
|
// Requires an accompanying DD_APM_OBFUSCATION_MEMCACHED_KEEP_COMMAND=true in the agent
|
|
806
810
|
this._setBoolean(env, 'memcachedCommandEnabled', DD_TRACE_MEMCACHED_COMMAND_ENABLED)
|
|
811
|
+
this._setBoolean(env, 'middlewareTracingEnabled', DD_TRACE_MIDDLEWARE_TRACING_ENABLED)
|
|
807
812
|
this._setBoolean(env, 'openAiLogsEnabled', DD_OPENAI_LOGS_ENABLED)
|
|
808
813
|
this._setValue(env, 'openai.spanCharLimit', maybeInt(DD_OPENAI_SPAN_CHAR_LIMIT))
|
|
809
814
|
this._envUnprocessed.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT
|
|
@@ -895,6 +900,7 @@ class Config {
|
|
|
895
900
|
this._setString(env, 'version', DD_VERSION || tags.version)
|
|
896
901
|
this._setBoolean(env, 'inferredProxyServicesEnabled', DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED)
|
|
897
902
|
this._setString(env, 'aws.dynamoDb.tablePrimaryKeys', DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS)
|
|
903
|
+
this._setArray(env, 'graphqlErrorExtensions', DD_TRACE_GRAPHQL_ERROR_EXTENSIONS)
|
|
898
904
|
}
|
|
899
905
|
|
|
900
906
|
_applyOptions (options) {
|
|
@@ -986,6 +992,7 @@ class Config {
|
|
|
986
992
|
this._setString(opts, 'llmobs.mlApp', options.llmobs?.mlApp)
|
|
987
993
|
this._setBoolean(opts, 'logInjection', options.logInjection)
|
|
988
994
|
this._setString(opts, 'lookup', options.lookup)
|
|
995
|
+
this._setBoolean(opts, 'middlewareTracingEnabled', options.middlewareTracingEnabled)
|
|
989
996
|
this._setBoolean(opts, 'openAiLogsEnabled', options.openAiLogsEnabled)
|
|
990
997
|
this._setValue(opts, 'peerServiceMapping', options.peerServiceMapping)
|
|
991
998
|
this._setBoolean(opts, 'plugins', options.plugins)
|
|
@@ -1020,6 +1027,7 @@ class Config {
|
|
|
1020
1027
|
this._setBoolean(opts, 'traceId128BitLoggingEnabled', options.traceId128BitLoggingEnabled)
|
|
1021
1028
|
this._setString(opts, 'version', options.version || tags.version)
|
|
1022
1029
|
this._setBoolean(opts, 'inferredProxyServicesEnabled', options.inferredProxyServicesEnabled)
|
|
1030
|
+
this._setBoolean(opts, 'graphqlErrorExtensions', options.graphqlErrorExtensions)
|
|
1023
1031
|
|
|
1024
1032
|
// For LLMObs, we want the environment variable to take precedence over the options.
|
|
1025
1033
|
// This is reliant on environment config being set before options.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const session = require('./session')
|
|
4
4
|
|
|
5
|
+
const WINDOWS_DRIVE_LETTER_REGEX = /[a-zA-Z]/
|
|
6
|
+
|
|
5
7
|
const scriptIds = []
|
|
6
8
|
const scriptUrls = new Map()
|
|
7
9
|
|
|
@@ -10,26 +12,79 @@ module.exports = {
|
|
|
10
12
|
breakpoints: new Map(),
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
|
-
* Find the
|
|
14
|
-
*
|
|
15
|
-
* Algorithm: Find the sortest url that ends in the requested path.
|
|
16
|
-
*
|
|
17
|
-
* Will identify the correct script as long as Node.js doesn't load a module from a `node_modules` folder outside the
|
|
18
|
-
* project root. If so, there's a risk that this path is shorter than the expected path inside the project root.
|
|
19
|
-
* Example of mismatch where path = `index.js`:
|
|
20
|
-
*
|
|
21
|
-
* Expected match: /www/code/my-projects/demo-project1/index.js
|
|
22
|
-
* Actual shorter match: /www/node_modules/dd-trace/index.js
|
|
23
|
-
*
|
|
24
|
-
* To fix this, specify a more unique file path, e.g `demo-project1/index.js` instead of `index.js`
|
|
15
|
+
* Find the script to inspect based on a partial or absolute path. Handles both Windows and POSIX paths.
|
|
25
16
|
*
|
|
26
|
-
* @param {string} path
|
|
27
|
-
* @returns {[string, string] |
|
|
17
|
+
* @param {string} path - Partial or absolute path to match against loaded scripts
|
|
18
|
+
* @returns {[string, string, string | undefined] | null} - Array containing [url, scriptId, sourceMapURL]
|
|
19
|
+
* or null if no match
|
|
28
20
|
*/
|
|
29
21
|
findScriptFromPartialPath (path) {
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
if (!path) return null // This shouldn't happen, but better safe than sorry
|
|
23
|
+
|
|
24
|
+
path = path.toLowerCase()
|
|
25
|
+
|
|
26
|
+
const bestMatch = new Array(3)
|
|
27
|
+
let maxMatchLength = -1
|
|
28
|
+
|
|
29
|
+
for (const [url, scriptId, sourceMapURL] of scriptIds) {
|
|
30
|
+
let i = url.length - 1
|
|
31
|
+
let j = path.length - 1
|
|
32
|
+
let matchLength = 0
|
|
33
|
+
let lastBoundaryPos = -1
|
|
34
|
+
let atBoundary = false
|
|
35
|
+
|
|
36
|
+
// Compare characters from the end
|
|
37
|
+
while (i >= 0 && j >= 0) {
|
|
38
|
+
const urlChar = url[i].toLowerCase()
|
|
39
|
+
const pathChar = path[j]
|
|
40
|
+
|
|
41
|
+
// Check if both characters is a path boundary
|
|
42
|
+
const isBoundary = (urlChar === '/' || urlChar === '\\') && (pathChar === '/' || pathChar === '\\' ||
|
|
43
|
+
(j === 1 && pathChar === ':' && WINDOWS_DRIVE_LETTER_REGEX.test(path[0])))
|
|
44
|
+
|
|
45
|
+
// If both are boundaries, or if characters match exactly
|
|
46
|
+
if (isBoundary || urlChar === pathChar) {
|
|
47
|
+
if (isBoundary) {
|
|
48
|
+
atBoundary = true
|
|
49
|
+
lastBoundaryPos = matchLength
|
|
50
|
+
} else {
|
|
51
|
+
atBoundary = false
|
|
52
|
+
}
|
|
53
|
+
matchLength++
|
|
54
|
+
i--
|
|
55
|
+
j--
|
|
56
|
+
} else {
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If we've matched the entire path pattern, ensure it starts at a path boundary
|
|
62
|
+
if (j === -1) {
|
|
63
|
+
if (i >= 0) {
|
|
64
|
+
// If there are more characters in the URL, the next one must be a slash
|
|
65
|
+
if (url[i] === '/' || url[i] === '\\') {
|
|
66
|
+
atBoundary = true
|
|
67
|
+
lastBoundaryPos = matchLength
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
atBoundary = true
|
|
71
|
+
lastBoundaryPos = matchLength
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If we found a valid match and it's better than our previous best
|
|
76
|
+
if (atBoundary && (
|
|
77
|
+
lastBoundaryPos > maxMatchLength ||
|
|
78
|
+
(lastBoundaryPos === maxMatchLength && url.length < bestMatch[0].length) // Prefer shorter paths
|
|
79
|
+
)) {
|
|
80
|
+
maxMatchLength = lastBoundaryPos
|
|
81
|
+
bestMatch[0] = url
|
|
82
|
+
bestMatch[1] = scriptId
|
|
83
|
+
bestMatch[2] = sourceMapURL
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return maxMatchLength > -1 ? bestMatch : null
|
|
33
88
|
},
|
|
34
89
|
|
|
35
90
|
getStackFromCallFrames (callFrames) {
|
|
@@ -15,7 +15,6 @@ let batch = 0
|
|
|
15
15
|
// Internal representation of a trace or span ID.
|
|
16
16
|
class Identifier {
|
|
17
17
|
constructor (value, radix = 16) {
|
|
18
|
-
this._isUint64BE = true // msgpack-lite compatibility
|
|
19
18
|
this._buffer = radix === 16
|
|
20
19
|
? createBuffer(value)
|
|
21
20
|
: fromString(value, radix)
|
|
@@ -31,7 +30,6 @@ class Identifier {
|
|
|
31
30
|
return this._buffer
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
// msgpack-lite compatibility
|
|
35
33
|
toArray () {
|
|
36
34
|
if (this._buffer.length === 8) {
|
|
37
35
|
return this._buffer
|
|
@@ -8,7 +8,9 @@ const {
|
|
|
8
8
|
parseModelId
|
|
9
9
|
} = require('../../../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils')
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const ENABLED_OPERATIONS = ['invokeModel']
|
|
12
|
+
|
|
13
|
+
const requestIdsToTokens = {}
|
|
12
14
|
|
|
13
15
|
class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
14
16
|
constructor () {
|
|
@@ -18,7 +20,7 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
18
20
|
const request = response.request
|
|
19
21
|
const operation = request.operation
|
|
20
22
|
// avoids instrumenting other non supported runtime operations
|
|
21
|
-
if (!
|
|
23
|
+
if (!ENABLED_OPERATIONS.includes(operation)) {
|
|
22
24
|
return
|
|
23
25
|
}
|
|
24
26
|
const { modelProvider, modelName } = parseModelId(request.params.modelId)
|
|
@@ -30,6 +32,17 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
30
32
|
const span = storage.getStore()?.span
|
|
31
33
|
this.setLLMObsTags({ request, span, response, modelProvider, modelName })
|
|
32
34
|
})
|
|
35
|
+
|
|
36
|
+
this.addSub('apm:aws:response:deserialize:bedrockruntime', ({ headers }) => {
|
|
37
|
+
const requestId = headers['x-amzn-requestid']
|
|
38
|
+
const inputTokenCount = headers['x-amzn-bedrock-input-token-count']
|
|
39
|
+
const outputTokenCount = headers['x-amzn-bedrock-output-token-count']
|
|
40
|
+
|
|
41
|
+
requestIdsToTokens[requestId] = {
|
|
42
|
+
inputTokensFromHeaders: inputTokenCount && parseInt(inputTokenCount),
|
|
43
|
+
outputTokensFromHeaders: outputTokenCount && parseInt(outputTokenCount)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
setLLMObsTags ({ request, span, response, modelProvider, modelName }) {
|
|
@@ -52,7 +65,39 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
52
65
|
})
|
|
53
66
|
|
|
54
67
|
// add I/O tags
|
|
55
|
-
this._tagger.tagLLMIO(
|
|
68
|
+
this._tagger.tagLLMIO(
|
|
69
|
+
span,
|
|
70
|
+
requestParams.prompt,
|
|
71
|
+
[{ content: textAndResponseReason.message, role: textAndResponseReason.role }]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// add token metrics
|
|
75
|
+
const { inputTokens, outputTokens, totalTokens } = extractTokens({
|
|
76
|
+
requestId: response.$metadata.requestId,
|
|
77
|
+
usage: textAndResponseReason.usage
|
|
78
|
+
})
|
|
79
|
+
this._tagger.tagMetrics(span, {
|
|
80
|
+
inputTokens,
|
|
81
|
+
outputTokens,
|
|
82
|
+
totalTokens
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractTokens ({ requestId, usage }) {
|
|
88
|
+
const {
|
|
89
|
+
inputTokensFromHeaders,
|
|
90
|
+
outputTokensFromHeaders
|
|
91
|
+
} = requestIdsToTokens[requestId] || {}
|
|
92
|
+
delete requestIdsToTokens[requestId]
|
|
93
|
+
|
|
94
|
+
const inputTokens = usage.inputTokens || inputTokensFromHeaders || 0
|
|
95
|
+
const outputTokens = usage.outputTokens || outputTokensFromHeaders || 0
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
inputTokens,
|
|
99
|
+
outputTokens,
|
|
100
|
+
totalTokens: inputTokens + outputTokens
|
|
56
101
|
}
|
|
57
102
|
}
|
|
58
103
|
|
|
@@ -31,6 +31,9 @@ loadChannel.subscribe(({ name }) => {
|
|
|
31
31
|
// Globals
|
|
32
32
|
maybeEnable(require('../../datadog-plugin-fetch/src'))
|
|
33
33
|
|
|
34
|
+
// Always enabled
|
|
35
|
+
maybeEnable(require('../../datadog-plugin-dd-trace-api/src'))
|
|
36
|
+
|
|
34
37
|
function maybeEnable (Plugin) {
|
|
35
38
|
if (!Plugin || typeof Plugin !== 'function') return
|
|
36
39
|
if (!pluginClasses[Plugin.id]) {
|
|
@@ -139,7 +142,8 @@ module.exports = class PluginManager {
|
|
|
139
142
|
memcachedCommandEnabled,
|
|
140
143
|
ciVisibilityTestSessionName,
|
|
141
144
|
ciVisAgentlessLogSubmissionEnabled,
|
|
142
|
-
isTestDynamicInstrumentationEnabled
|
|
145
|
+
isTestDynamicInstrumentationEnabled,
|
|
146
|
+
middlewareTracingEnabled
|
|
143
147
|
} = this._tracerConfig
|
|
144
148
|
|
|
145
149
|
const sharedConfig = {
|
|
@@ -170,6 +174,13 @@ module.exports = class PluginManager {
|
|
|
170
174
|
sharedConfig.clientIpEnabled = clientIpEnabled
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
// For the global setting, we use the name `middlewareTracingEnabled`, but
|
|
178
|
+
// for the plugin-specific setting, we use `middleware`. They mean the same
|
|
179
|
+
// to an individual plugin, so we normalize them here.
|
|
180
|
+
if (middlewareTracingEnabled !== undefined) {
|
|
181
|
+
sharedConfig.middleware = middlewareTracingEnabled
|
|
182
|
+
}
|
|
183
|
+
|
|
173
184
|
return sharedConfig
|
|
174
185
|
}
|
|
175
186
|
}
|
|
@@ -34,6 +34,7 @@ module.exports = {
|
|
|
34
34
|
get couchbase () { return require('../../../datadog-plugin-couchbase/src') },
|
|
35
35
|
get cypress () { return require('../../../datadog-plugin-cypress/src') },
|
|
36
36
|
get dns () { return require('../../../datadog-plugin-dns/src') },
|
|
37
|
+
get 'dd-trace-api' () { return require('../../../datadog-plugin-dd-trace-api/src') },
|
|
37
38
|
get elasticsearch () { return require('../../../datadog-plugin-elasticsearch/src') },
|
|
38
39
|
get express () { return require('../../../datadog-plugin-express/src') },
|
|
39
40
|
get fastify () { return require('../../../datadog-plugin-fastify/src') },
|