dd-trace 5.11.0 → 5.12.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/package.json +1 -1
- package/packages/datadog-instrumentations/src/helpers/fetch.js +9 -4
- package/packages/datadog-instrumentations/src/openai.js +149 -0
- package/packages/datadog-plugin-fetch/src/index.js +5 -2
- package/packages/datadog-plugin-openai/src/index.js +159 -32
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +1 -9
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +4 -2
- package/packages/dd-trace/src/appsec/remote_config/index.js +5 -5
- package/packages/dd-trace/src/proxy.js +26 -6
package/package.json
CHANGED
|
@@ -7,11 +7,16 @@ exports.createWrapFetch = function createWrapFetch (Request, ch) {
|
|
|
7
7
|
return function (input, init) {
|
|
8
8
|
if (!ch.start.hasSubscribers) return fetch.apply(this, arguments)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const ctx = { req, headers }
|
|
10
|
+
if (input instanceof Request) {
|
|
11
|
+
const ctx = { req: input }
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
return ch.tracePromise(() => fetch.call(this, input, init), ctx)
|
|
14
|
+
} else {
|
|
15
|
+
const req = new Request(input, init)
|
|
16
|
+
const ctx = { req }
|
|
17
|
+
|
|
18
|
+
return ch.tracePromise(() => fetch.call(this, req), ctx)
|
|
19
|
+
}
|
|
15
20
|
}
|
|
16
21
|
}
|
|
17
22
|
}
|
|
@@ -10,6 +10,98 @@ const startCh = channel('apm:openai:request:start')
|
|
|
10
10
|
const finishCh = channel('apm:openai:request:finish')
|
|
11
11
|
const errorCh = channel('apm:openai:request:error')
|
|
12
12
|
|
|
13
|
+
const V4_PACKAGE_SHIMS = [
|
|
14
|
+
{
|
|
15
|
+
file: 'resources/chat/completions.js',
|
|
16
|
+
targetClass: 'Completions',
|
|
17
|
+
baseResource: 'chat.completions',
|
|
18
|
+
methods: ['create']
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
file: 'resources/completions.js',
|
|
22
|
+
targetClass: 'Completions',
|
|
23
|
+
baseResource: 'completions',
|
|
24
|
+
methods: ['create']
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
file: 'resources/embeddings.js',
|
|
28
|
+
targetClass: 'Embeddings',
|
|
29
|
+
baseResource: 'embeddings',
|
|
30
|
+
methods: ['create']
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
file: 'resources/files.js',
|
|
34
|
+
targetClass: 'Files',
|
|
35
|
+
baseResource: 'files',
|
|
36
|
+
methods: ['create', 'del', 'list', 'retrieve']
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
file: 'resources/files.js',
|
|
40
|
+
targetClass: 'Files',
|
|
41
|
+
baseResource: 'files',
|
|
42
|
+
methods: ['retrieveContent'],
|
|
43
|
+
versions: ['>=4.0.0 <4.17.1']
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
file: 'resources/files.js',
|
|
47
|
+
targetClass: 'Files',
|
|
48
|
+
baseResource: 'files',
|
|
49
|
+
methods: ['content'], // replaced `retrieveContent` in v4.17.1
|
|
50
|
+
versions: ['>=4.17.1']
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
file: 'resources/images.js',
|
|
54
|
+
targetClass: 'Images',
|
|
55
|
+
baseResource: 'images',
|
|
56
|
+
methods: ['createVariation', 'edit', 'generate']
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
file: 'resources/fine-tuning/jobs/jobs.js',
|
|
60
|
+
targetClass: 'Jobs',
|
|
61
|
+
baseResource: 'fine_tuning.jobs',
|
|
62
|
+
methods: ['cancel', 'create', 'list', 'listEvents', 'retrieve'],
|
|
63
|
+
versions: ['>=4.34.0'] // file location changed in 4.34.0
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
file: 'resources/fine-tuning/jobs.js',
|
|
67
|
+
targetClass: 'Jobs',
|
|
68
|
+
baseResource: 'fine_tuning.jobs',
|
|
69
|
+
methods: ['cancel', 'create', 'list', 'listEvents', 'retrieve'],
|
|
70
|
+
versions: ['>=4.1.0 <4.34.0']
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
file: 'resources/fine-tunes.js', // deprecated after 4.1.0
|
|
74
|
+
targetClass: 'FineTunes',
|
|
75
|
+
baseResource: 'fine-tune',
|
|
76
|
+
methods: ['cancel', 'create', 'list', 'listEvents', 'retrieve'],
|
|
77
|
+
versions: ['>=4.0.0 <4.1.0']
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
file: 'resources/models.js',
|
|
81
|
+
targetClass: 'Models',
|
|
82
|
+
baseResource: 'models',
|
|
83
|
+
methods: ['del', 'list', 'retrieve']
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
file: 'resources/moderations.js',
|
|
87
|
+
targetClass: 'Moderations',
|
|
88
|
+
baseResource: 'moderations',
|
|
89
|
+
methods: ['create']
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
file: 'resources/audio/transcriptions.js',
|
|
93
|
+
targetClass: 'Transcriptions',
|
|
94
|
+
baseResource: 'audio.transcriptions',
|
|
95
|
+
methods: ['create']
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
file: 'resources/audio/translations.js',
|
|
99
|
+
targetClass: 'Translations',
|
|
100
|
+
baseResource: 'audio.translations',
|
|
101
|
+
methods: ['create']
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
|
|
13
105
|
addHook({ name: 'openai', file: 'dist/api.js', versions: ['>=3.0.0 <4'] }, exports => {
|
|
14
106
|
const methodNames = Object.getOwnPropertyNames(exports.OpenAIApi.prototype)
|
|
15
107
|
methodNames.shift() // remove leading 'constructor' method
|
|
@@ -48,3 +140,60 @@ addHook({ name: 'openai', file: 'dist/api.js', versions: ['>=3.0.0 <4'] }, expor
|
|
|
48
140
|
|
|
49
141
|
return exports
|
|
50
142
|
})
|
|
143
|
+
|
|
144
|
+
for (const shim of V4_PACKAGE_SHIMS) {
|
|
145
|
+
const { file, targetClass, baseResource, methods } = shim
|
|
146
|
+
addHook({ name: 'openai', file, versions: shim.versions || ['>=4'] }, exports => {
|
|
147
|
+
const targetPrototype = exports[targetClass].prototype
|
|
148
|
+
|
|
149
|
+
for (const methodName of methods) {
|
|
150
|
+
shimmer.wrap(targetPrototype, methodName, methodFn => function () {
|
|
151
|
+
if (!startCh.hasSubscribers) {
|
|
152
|
+
return methodFn.apply(this, arguments)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const client = this._client || this.client
|
|
156
|
+
|
|
157
|
+
startCh.publish({
|
|
158
|
+
methodName: `${baseResource}.${methodName}`,
|
|
159
|
+
args: arguments,
|
|
160
|
+
basePath: client.baseURL,
|
|
161
|
+
apiKey: client.apiKey
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const apiProm = methodFn.apply(this, arguments)
|
|
165
|
+
|
|
166
|
+
// wrapping `parse` avoids problematic wrapping of `then` when trying to call
|
|
167
|
+
// `withResponse` in userland code after. This way, we can return the whole `APIPromise`
|
|
168
|
+
shimmer.wrap(apiProm, 'parse', origApiPromParse => function () {
|
|
169
|
+
return origApiPromParse.apply(this, arguments)
|
|
170
|
+
// the original response is wrapped in a promise, so we need to unwrap it
|
|
171
|
+
.then(body => Promise.all([this.responsePromise, body]))
|
|
172
|
+
.then(([{ response, options }, body]) => {
|
|
173
|
+
finishCh.publish({
|
|
174
|
+
headers: response.headers,
|
|
175
|
+
body,
|
|
176
|
+
path: response.url,
|
|
177
|
+
method: options.method
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return body
|
|
181
|
+
})
|
|
182
|
+
.catch(err => {
|
|
183
|
+
errorCh.publish({ err })
|
|
184
|
+
|
|
185
|
+
throw err
|
|
186
|
+
})
|
|
187
|
+
.finally(() => {
|
|
188
|
+
// maybe we don't want to unwrap here in case the promise is re-used?
|
|
189
|
+
// other hand: we want to avoid resource leakage
|
|
190
|
+
shimmer.unwrap(apiProm, 'parse')
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return apiProm
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
return exports
|
|
198
|
+
})
|
|
199
|
+
}
|
|
@@ -17,8 +17,11 @@ class FetchPlugin extends HttpClientPlugin {
|
|
|
17
17
|
|
|
18
18
|
const store = super.bindStart(ctx)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
for (const name in headers) {
|
|
21
|
+
if (!req.headers.has(name)) {
|
|
22
|
+
req.headers.set(name, headers[name])
|
|
23
|
+
}
|
|
24
|
+
}
|
|
22
25
|
|
|
23
26
|
return store
|
|
24
27
|
}
|
|
@@ -84,7 +84,7 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
84
84
|
const tags = {} // The remaining tags are added one at a time
|
|
85
85
|
|
|
86
86
|
// createChatCompletion, createCompletion, createImage, createImageEdit, createTranscription, createTranslation
|
|
87
|
-
if (
|
|
87
|
+
if (payload.prompt) {
|
|
88
88
|
const prompt = payload.prompt
|
|
89
89
|
store.prompt = prompt
|
|
90
90
|
if (typeof prompt === 'string' || (Array.isArray(prompt) && typeof prompt[0] === 'number')) {
|
|
@@ -99,7 +99,7 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// createEdit, createEmbedding, createModeration
|
|
102
|
-
if (
|
|
102
|
+
if (payload.input) {
|
|
103
103
|
const normalized = normalizeStringOrTokenArray(payload.input, false)
|
|
104
104
|
tags['openai.request.input'] = truncateText(normalized)
|
|
105
105
|
store.input = normalized
|
|
@@ -114,41 +114,60 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
114
114
|
|
|
115
115
|
switch (methodName) {
|
|
116
116
|
case 'createFineTune':
|
|
117
|
+
case 'fine_tuning.jobs.create':
|
|
118
|
+
case 'fine-tune.create':
|
|
117
119
|
createFineTuneRequestExtraction(tags, payload)
|
|
118
120
|
break
|
|
119
121
|
|
|
120
122
|
case 'createImage':
|
|
123
|
+
case 'images.generate':
|
|
121
124
|
case 'createImageEdit':
|
|
125
|
+
case 'images.edit':
|
|
122
126
|
case 'createImageVariation':
|
|
127
|
+
case 'images.createVariation':
|
|
123
128
|
commonCreateImageRequestExtraction(tags, payload, store)
|
|
124
129
|
break
|
|
125
130
|
|
|
126
131
|
case 'createChatCompletion':
|
|
132
|
+
case 'chat.completions.create':
|
|
127
133
|
createChatCompletionRequestExtraction(tags, payload, store)
|
|
128
134
|
break
|
|
129
135
|
|
|
130
136
|
case 'createFile':
|
|
137
|
+
case 'files.create':
|
|
131
138
|
case 'retrieveFile':
|
|
139
|
+
case 'files.retrieve':
|
|
132
140
|
commonFileRequestExtraction(tags, payload)
|
|
133
141
|
break
|
|
134
142
|
|
|
135
143
|
case 'createTranscription':
|
|
144
|
+
case 'audio.transcriptions.create':
|
|
136
145
|
case 'createTranslation':
|
|
146
|
+
case 'audio.translations.create':
|
|
137
147
|
commonCreateAudioRequestExtraction(tags, payload, store)
|
|
138
148
|
break
|
|
139
149
|
|
|
140
150
|
case 'retrieveModel':
|
|
151
|
+
case 'models.retrieve':
|
|
141
152
|
retrieveModelRequestExtraction(tags, payload)
|
|
142
153
|
break
|
|
143
154
|
|
|
144
155
|
case 'listFineTuneEvents':
|
|
156
|
+
case 'fine_tuning.jobs.listEvents':
|
|
157
|
+
case 'fine-tune.listEvents':
|
|
145
158
|
case 'retrieveFineTune':
|
|
159
|
+
case 'fine_tuning.jobs.retrieve':
|
|
160
|
+
case 'fine-tune.retrieve':
|
|
146
161
|
case 'deleteModel':
|
|
162
|
+
case 'models.del':
|
|
147
163
|
case 'cancelFineTune':
|
|
164
|
+
case 'fine_tuning.jobs.cancel':
|
|
165
|
+
case 'fine-tune.cancel':
|
|
148
166
|
commonLookupFineTuneRequestExtraction(tags, payload)
|
|
149
167
|
break
|
|
150
168
|
|
|
151
169
|
case 'createEdit':
|
|
170
|
+
case 'edits.create':
|
|
152
171
|
createEditRequestExtraction(tags, payload, store)
|
|
153
172
|
break
|
|
154
173
|
}
|
|
@@ -157,6 +176,10 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
finish ({ headers, body, method, path }) {
|
|
179
|
+
if (headers.constructor.name === 'Headers') {
|
|
180
|
+
headers = Object.fromEntries(headers)
|
|
181
|
+
}
|
|
182
|
+
|
|
160
183
|
const span = this.activeSpan
|
|
161
184
|
const methodName = span._spanContext._tags['resource.name']
|
|
162
185
|
|
|
@@ -165,11 +188,16 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
165
188
|
const fullStore = storage.getStore()
|
|
166
189
|
const store = fullStore.openai
|
|
167
190
|
|
|
191
|
+
if (path.startsWith('https://') || path.startsWith('http://')) {
|
|
192
|
+
// basic checking for if the path was set as a full URL
|
|
193
|
+
// not using a full regex as it will likely be "https://api.openai.com/..."
|
|
194
|
+
path = new URL(path).pathname
|
|
195
|
+
}
|
|
168
196
|
const endpoint = lookupOperationEndpoint(methodName, path)
|
|
169
197
|
|
|
170
198
|
const tags = {
|
|
171
199
|
'openai.request.endpoint': endpoint,
|
|
172
|
-
'openai.request.method': method,
|
|
200
|
+
'openai.request.method': method.toUpperCase(),
|
|
173
201
|
|
|
174
202
|
'openai.organization.id': body.organization_id, // only available in fine-tunes endpoints
|
|
175
203
|
'openai.organization.name': headers['openai-organization'],
|
|
@@ -220,7 +248,7 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
220
248
|
|
|
221
249
|
this.metrics.distribution('openai.request.duration', duration * 1000, tags)
|
|
222
250
|
|
|
223
|
-
if (body &&
|
|
251
|
+
if (body && body.usage) {
|
|
224
252
|
const promptTokens = body.usage.prompt_tokens
|
|
225
253
|
const completionTokens = body.usage.completion_tokens
|
|
226
254
|
this.metrics.distribution('openai.tokens.prompt', promptTokens, tags)
|
|
@@ -228,19 +256,19 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
228
256
|
this.metrics.distribution('openai.tokens.total', promptTokens + completionTokens, tags)
|
|
229
257
|
}
|
|
230
258
|
|
|
231
|
-
if ('x-ratelimit-limit-requests'
|
|
259
|
+
if (headers['x-ratelimit-limit-requests']) {
|
|
232
260
|
this.metrics.gauge('openai.ratelimit.requests', Number(headers['x-ratelimit-limit-requests']), tags)
|
|
233
261
|
}
|
|
234
262
|
|
|
235
|
-
if ('x-ratelimit-remaining-requests'
|
|
263
|
+
if (headers['x-ratelimit-remaining-requests']) {
|
|
236
264
|
this.metrics.gauge('openai.ratelimit.remaining.requests', Number(headers['x-ratelimit-remaining-requests']), tags)
|
|
237
265
|
}
|
|
238
266
|
|
|
239
|
-
if ('x-ratelimit-limit-tokens'
|
|
267
|
+
if (headers['x-ratelimit-limit-tokens']) {
|
|
240
268
|
this.metrics.gauge('openai.ratelimit.tokens', Number(headers['x-ratelimit-limit-tokens']), tags)
|
|
241
269
|
}
|
|
242
270
|
|
|
243
|
-
if ('x-ratelimit-remaining-tokens'
|
|
271
|
+
if (headers['x-ratelimit-remaining-tokens']) {
|
|
244
272
|
this.metrics.gauge('openai.ratelimit.remaining.tokens', Number(headers['x-ratelimit-remaining-tokens']), tags)
|
|
245
273
|
}
|
|
246
274
|
}
|
|
@@ -275,17 +303,18 @@ function createChatCompletionRequestExtraction (tags, payload, store) {
|
|
|
275
303
|
store.messages = payload.messages
|
|
276
304
|
for (let i = 0; i < payload.messages.length; i++) {
|
|
277
305
|
const message = payload.messages[i]
|
|
278
|
-
tags[`openai.request.${i}.content`] = truncateText(message.content)
|
|
279
|
-
tags[`openai.request.${i}.role`] = message.role
|
|
280
|
-
tags[`openai.request.${i}.name`] = message.name
|
|
281
|
-
tags[`openai.request.${i}.finish_reason`] = message.finish_reason
|
|
306
|
+
tags[`openai.request.messages.${i}.content`] = truncateText(message.content)
|
|
307
|
+
tags[`openai.request.messages.${i}.role`] = message.role
|
|
308
|
+
tags[`openai.request.messages.${i}.name`] = message.name
|
|
309
|
+
tags[`openai.request.messages.${i}.finish_reason`] = message.finish_reason
|
|
282
310
|
}
|
|
283
311
|
}
|
|
284
312
|
|
|
285
313
|
function commonCreateImageRequestExtraction (tags, payload, store) {
|
|
286
314
|
// createImageEdit, createImageVariation
|
|
287
|
-
|
|
288
|
-
|
|
315
|
+
const img = payload.file || payload.image
|
|
316
|
+
if (img && typeof img === 'object' && img.path) {
|
|
317
|
+
const file = path.basename(img.path)
|
|
289
318
|
tags['openai.request.image'] = file
|
|
290
319
|
store.file = file
|
|
291
320
|
}
|
|
@@ -305,60 +334,88 @@ function commonCreateImageRequestExtraction (tags, payload, store) {
|
|
|
305
334
|
function responseDataExtractionByMethod (methodName, tags, body, store) {
|
|
306
335
|
switch (methodName) {
|
|
307
336
|
case 'createModeration':
|
|
337
|
+
case 'moderations.create':
|
|
308
338
|
createModerationResponseExtraction(tags, body)
|
|
309
339
|
break
|
|
310
340
|
|
|
311
341
|
case 'createCompletion':
|
|
342
|
+
case 'completions.create':
|
|
312
343
|
case 'createChatCompletion':
|
|
344
|
+
case 'chat.completions.create':
|
|
313
345
|
case 'createEdit':
|
|
346
|
+
case 'edits.create':
|
|
314
347
|
commonCreateResponseExtraction(tags, body, store)
|
|
315
348
|
break
|
|
316
349
|
|
|
317
350
|
case 'listFiles':
|
|
351
|
+
case 'files.list':
|
|
318
352
|
case 'listFineTunes':
|
|
353
|
+
case 'fine_tuning.jobs.list':
|
|
354
|
+
case 'fine-tune.list':
|
|
319
355
|
case 'listFineTuneEvents':
|
|
356
|
+
case 'fine_tuning.jobs.listEvents':
|
|
357
|
+
case 'fine-tune.listEvents':
|
|
320
358
|
commonListCountResponseExtraction(tags, body)
|
|
321
359
|
break
|
|
322
360
|
|
|
323
361
|
case 'createEmbedding':
|
|
362
|
+
case 'embeddings.create':
|
|
324
363
|
createEmbeddingResponseExtraction(tags, body)
|
|
325
364
|
break
|
|
326
365
|
|
|
327
366
|
case 'createFile':
|
|
367
|
+
case 'files.create':
|
|
328
368
|
case 'retrieveFile':
|
|
369
|
+
case 'files.retrieve':
|
|
329
370
|
createRetrieveFileResponseExtraction(tags, body)
|
|
330
371
|
break
|
|
331
372
|
|
|
332
373
|
case 'deleteFile':
|
|
374
|
+
case 'files.del':
|
|
333
375
|
deleteFileResponseExtraction(tags, body)
|
|
334
376
|
break
|
|
335
377
|
|
|
336
378
|
case 'downloadFile':
|
|
379
|
+
case 'files.retrieveContent':
|
|
380
|
+
case 'files.content':
|
|
337
381
|
downloadFileResponseExtraction(tags, body)
|
|
338
382
|
break
|
|
339
383
|
|
|
340
384
|
case 'createFineTune':
|
|
385
|
+
case 'fine_tuning.jobs.create':
|
|
386
|
+
case 'fine-tune.create':
|
|
341
387
|
case 'retrieveFineTune':
|
|
388
|
+
case 'fine_tuning.jobs.retrieve':
|
|
389
|
+
case 'fine-tune.retrieve':
|
|
342
390
|
case 'cancelFineTune':
|
|
391
|
+
case 'fine_tuning.jobs.cancel':
|
|
392
|
+
case 'fine-tune.cancel':
|
|
343
393
|
commonFineTuneResponseExtraction(tags, body)
|
|
344
394
|
break
|
|
345
395
|
|
|
346
396
|
case 'createTranscription':
|
|
397
|
+
case 'audio.transcriptions.create':
|
|
347
398
|
case 'createTranslation':
|
|
399
|
+
case 'audio.translations.create':
|
|
348
400
|
createAudioResponseExtraction(tags, body)
|
|
349
401
|
break
|
|
350
402
|
|
|
351
403
|
case 'createImage':
|
|
404
|
+
case 'images.generate':
|
|
352
405
|
case 'createImageEdit':
|
|
406
|
+
case 'images.edit':
|
|
353
407
|
case 'createImageVariation':
|
|
408
|
+
case 'images.createVariation':
|
|
354
409
|
commonImageResponseExtraction(tags, body)
|
|
355
410
|
break
|
|
356
411
|
|
|
357
412
|
case 'listModels':
|
|
413
|
+
case 'models.list':
|
|
358
414
|
listModelsResponseExtraction(tags, body)
|
|
359
415
|
break
|
|
360
416
|
|
|
361
417
|
case 'retrieveModel':
|
|
418
|
+
case 'models.retrieve':
|
|
362
419
|
retrieveModelResponseExtraction(tags, body)
|
|
363
420
|
break
|
|
364
421
|
}
|
|
@@ -431,15 +488,19 @@ function createFineTuneRequestExtraction (tags, body) {
|
|
|
431
488
|
function commonFineTuneResponseExtraction (tags, body) {
|
|
432
489
|
tags['openai.response.events_count'] = defensiveArrayLength(body.events)
|
|
433
490
|
tags['openai.response.fine_tuned_model'] = body.fine_tuned_model
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
491
|
+
|
|
492
|
+
const hyperparams = body.hyperparams || body.hyperparameters
|
|
493
|
+
const hyperparamsKey = body.hyperparams ? 'hyperparams' : 'hyperparameters'
|
|
494
|
+
|
|
495
|
+
if (hyperparams) {
|
|
496
|
+
tags[`openai.response.${hyperparamsKey}.n_epochs`] = hyperparams.n_epochs
|
|
497
|
+
tags[`openai.response.${hyperparamsKey}.batch_size`] = hyperparams.batch_size
|
|
498
|
+
tags[`openai.response.${hyperparamsKey}.prompt_loss_weight`] = hyperparams.prompt_loss_weight
|
|
499
|
+
tags[`openai.response.${hyperparamsKey}.learning_rate_multiplier`] = hyperparams.learning_rate_multiplier
|
|
439
500
|
}
|
|
440
|
-
tags['openai.response.training_files_count'] = defensiveArrayLength(body.training_files)
|
|
501
|
+
tags['openai.response.training_files_count'] = defensiveArrayLength(body.training_files || body.training_file)
|
|
441
502
|
tags['openai.response.result_files_count'] = defensiveArrayLength(body.result_files)
|
|
442
|
-
tags['openai.response.validation_files_count'] = defensiveArrayLength(body.validation_files)
|
|
503
|
+
tags['openai.response.validation_files_count'] = defensiveArrayLength(body.validation_files || body.validation_file)
|
|
443
504
|
tags['openai.response.updated_at'] = body.updated_at
|
|
444
505
|
tags['openai.response.status'] = body.status
|
|
445
506
|
}
|
|
@@ -528,18 +589,31 @@ function commonCreateResponseExtraction (tags, body, store) {
|
|
|
528
589
|
|
|
529
590
|
store.choices = body.choices
|
|
530
591
|
|
|
531
|
-
for (let
|
|
532
|
-
const choice = body.choices[
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
592
|
+
for (let choiceIdx = 0; choiceIdx < body.choices.length; choiceIdx++) {
|
|
593
|
+
const choice = body.choices[choiceIdx]
|
|
594
|
+
|
|
595
|
+
// logprobs can be nullm and we still want to tag it as 'returned' even when set to 'null'
|
|
596
|
+
const specifiesLogProb = Object.keys(choice).indexOf('logprobs') !== -1
|
|
597
|
+
|
|
598
|
+
tags[`openai.response.choices.${choiceIdx}.finish_reason`] = choice.finish_reason
|
|
599
|
+
tags[`openai.response.choices.${choiceIdx}.logprobs`] = specifiesLogProb ? 'returned' : undefined
|
|
600
|
+
tags[`openai.response.choices.${choiceIdx}.text`] = truncateText(choice.text)
|
|
536
601
|
|
|
537
602
|
// createChatCompletion only
|
|
538
|
-
if (
|
|
603
|
+
if (choice.message) {
|
|
539
604
|
const message = choice.message
|
|
540
|
-
tags[`openai.response.choices.${
|
|
541
|
-
tags[`openai.response.choices.${
|
|
542
|
-
tags[`openai.response.choices.${
|
|
605
|
+
tags[`openai.response.choices.${choiceIdx}.message.role`] = message.role
|
|
606
|
+
tags[`openai.response.choices.${choiceIdx}.message.content`] = truncateText(message.content)
|
|
607
|
+
tags[`openai.response.choices.${choiceIdx}.message.name`] = truncateText(message.name)
|
|
608
|
+
if (message.tool_calls) {
|
|
609
|
+
const toolCalls = message.tool_calls
|
|
610
|
+
for (let toolIdx = 0; toolIdx < toolCalls.length; toolIdx++) {
|
|
611
|
+
tags[`openai.response.choices.${choiceIdx}.message.tool_calls.${toolIdx}.name`] =
|
|
612
|
+
toolCalls[toolIdx].function.name
|
|
613
|
+
tags[`openai.response.choices.${choiceIdx}.message.tool_calls.${toolIdx}.arguments`] =
|
|
614
|
+
toolCalls[toolIdx].function.arguments
|
|
615
|
+
}
|
|
616
|
+
}
|
|
543
617
|
}
|
|
544
618
|
}
|
|
545
619
|
}
|
|
@@ -577,34 +651,62 @@ function truncateText (text) {
|
|
|
577
651
|
function coerceResponseBody (body, methodName) {
|
|
578
652
|
switch (methodName) {
|
|
579
653
|
case 'downloadFile':
|
|
654
|
+
case 'files.retrieveContent':
|
|
655
|
+
case 'files.content':
|
|
580
656
|
return { file: body }
|
|
581
657
|
}
|
|
582
658
|
|
|
583
|
-
|
|
659
|
+
const type = typeof body
|
|
660
|
+
if (type === 'string') {
|
|
661
|
+
try {
|
|
662
|
+
return JSON.parse(body)
|
|
663
|
+
} catch {
|
|
664
|
+
return body
|
|
665
|
+
}
|
|
666
|
+
} else if (type === 'object') {
|
|
667
|
+
return body
|
|
668
|
+
} else {
|
|
669
|
+
return {}
|
|
670
|
+
}
|
|
584
671
|
}
|
|
585
672
|
|
|
586
673
|
// This method is used to replace a dynamic URL segment with an asterisk
|
|
587
674
|
function lookupOperationEndpoint (operationId, url) {
|
|
588
675
|
switch (operationId) {
|
|
589
676
|
case 'deleteModel':
|
|
677
|
+
case 'models.del':
|
|
590
678
|
case 'retrieveModel':
|
|
679
|
+
case 'models.retrieve':
|
|
591
680
|
return '/v1/models/*'
|
|
592
681
|
|
|
593
682
|
case 'deleteFile':
|
|
683
|
+
case 'files.del':
|
|
594
684
|
case 'retrieveFile':
|
|
685
|
+
case 'files.retrieve':
|
|
595
686
|
return '/v1/files/*'
|
|
596
687
|
|
|
597
688
|
case 'downloadFile':
|
|
689
|
+
case 'files.retrieveContent':
|
|
690
|
+
case 'files.content':
|
|
598
691
|
return '/v1/files/*/content'
|
|
599
692
|
|
|
600
693
|
case 'retrieveFineTune':
|
|
694
|
+
case 'fine-tune.retrieve':
|
|
601
695
|
return '/v1/fine-tunes/*'
|
|
696
|
+
case 'fine_tuning.jobs.retrieve':
|
|
697
|
+
return '/v1/fine_tuning/jobs/*'
|
|
602
698
|
|
|
603
699
|
case 'listFineTuneEvents':
|
|
700
|
+
case 'fine-tune.listEvents':
|
|
604
701
|
return '/v1/fine-tunes/*/events'
|
|
702
|
+
case 'fine_tuning.jobs.listEvents':
|
|
703
|
+
return '/v1/fine_tuning/jobs/*/events'
|
|
605
704
|
|
|
606
705
|
case 'cancelFineTune':
|
|
706
|
+
case 'fine-tune.cancel':
|
|
607
707
|
return '/v1/fine-tunes/*/cancel'
|
|
708
|
+
case 'fine_tuning.jobs.cancel':
|
|
709
|
+
return '/v1/fine_tuning/jobs/*/cancel'
|
|
608
710
|
}
|
|
609
711
|
|
|
610
712
|
return url
|
|
@@ -618,12 +720,17 @@ function lookupOperationEndpoint (operationId, url) {
|
|
|
618
720
|
function normalizeRequestPayload (methodName, args) {
|
|
619
721
|
switch (methodName) {
|
|
620
722
|
case 'listModels':
|
|
723
|
+
case 'models.list':
|
|
621
724
|
case 'listFiles':
|
|
725
|
+
case 'files.list':
|
|
622
726
|
case 'listFineTunes':
|
|
727
|
+
case 'fine_tuning.jobs.list':
|
|
728
|
+
case 'fine-tune.list':
|
|
623
729
|
// no argument
|
|
624
730
|
return {}
|
|
625
731
|
|
|
626
732
|
case 'retrieveModel':
|
|
733
|
+
case 'models.retrieve':
|
|
627
734
|
return { id: args[0] }
|
|
628
735
|
|
|
629
736
|
case 'createFile':
|
|
@@ -633,19 +740,30 @@ function normalizeRequestPayload (methodName, args) {
|
|
|
633
740
|
}
|
|
634
741
|
|
|
635
742
|
case 'deleteFile':
|
|
743
|
+
case 'files.del':
|
|
636
744
|
case 'retrieveFile':
|
|
745
|
+
case 'files.retrieve':
|
|
637
746
|
case 'downloadFile':
|
|
747
|
+
case 'files.retrieveContent':
|
|
748
|
+
case 'files.content':
|
|
638
749
|
return { file_id: args[0] }
|
|
639
750
|
|
|
640
751
|
case 'listFineTuneEvents':
|
|
752
|
+
case 'fine_tuning.jobs.listEvents':
|
|
753
|
+
case 'fine-tune.listEvents':
|
|
641
754
|
return {
|
|
642
755
|
fine_tune_id: args[0],
|
|
643
756
|
stream: args[1] // undocumented
|
|
644
757
|
}
|
|
645
758
|
|
|
646
759
|
case 'retrieveFineTune':
|
|
760
|
+
case 'fine_tuning.jobs.retrieve':
|
|
761
|
+
case 'fine-tune.retrieve':
|
|
647
762
|
case 'deleteModel':
|
|
763
|
+
case 'models.del':
|
|
648
764
|
case 'cancelFineTune':
|
|
765
|
+
case 'fine_tuning.jobs.cancel':
|
|
766
|
+
case 'fine-tune.cancel':
|
|
649
767
|
return { fine_tune_id: args[0] }
|
|
650
768
|
|
|
651
769
|
case 'createImageEdit':
|
|
@@ -702,7 +820,16 @@ function normalizeStringOrTokenArray (input, truncate) {
|
|
|
702
820
|
}
|
|
703
821
|
|
|
704
822
|
function defensiveArrayLength (maybeArray) {
|
|
705
|
-
|
|
823
|
+
if (maybeArray) {
|
|
824
|
+
if (Array.isArray(maybeArray)) {
|
|
825
|
+
return maybeArray.length
|
|
826
|
+
} else {
|
|
827
|
+
// case of a singular item (ie body.training_file vs body.training_files)
|
|
828
|
+
return 1
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return undefined
|
|
706
833
|
}
|
|
707
834
|
|
|
708
835
|
module.exports = OpenApiPlugin
|
|
@@ -4,7 +4,7 @@ const TaintedUtils = require('@datadog/native-iast-taint-tracking')
|
|
|
4
4
|
const { IAST_TRANSACTION_ID } = require('../iast-context')
|
|
5
5
|
const iastLog = require('../iast-log')
|
|
6
6
|
|
|
7
|
-
function taintObject (iastContext, object, type
|
|
7
|
+
function taintObject (iastContext, object, type) {
|
|
8
8
|
let result = object
|
|
9
9
|
const transactionId = iastContext?.[IAST_TRANSACTION_ID]
|
|
10
10
|
if (transactionId) {
|
|
@@ -22,9 +22,6 @@ function taintObject (iastContext, object, type, keyTainting, keyType) {
|
|
|
22
22
|
const tainted = TaintedUtils.newTaintedString(transactionId, value, property, type)
|
|
23
23
|
if (!parent) {
|
|
24
24
|
result = tainted
|
|
25
|
-
} else if (keyTainting && key) {
|
|
26
|
-
const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
|
|
27
|
-
parent[taintedProperty] = tainted
|
|
28
25
|
} else {
|
|
29
26
|
parent[key] = tainted
|
|
30
27
|
}
|
|
@@ -34,11 +31,6 @@ function taintObject (iastContext, object, type, keyTainting, keyType) {
|
|
|
34
31
|
for (const key of Object.keys(value)) {
|
|
35
32
|
queue.push({ parent: value, property: property ? `${property}.${key}` : key, value: value[key], key })
|
|
36
33
|
}
|
|
37
|
-
|
|
38
|
-
if (parent && keyTainting && key) {
|
|
39
|
-
const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
|
|
40
|
-
parent[taintedProperty] = value
|
|
41
|
-
}
|
|
42
34
|
}
|
|
43
35
|
} catch (e) {
|
|
44
36
|
iastLog.error(`Error visiting property : ${property}`).errorAndPublish(e)
|
|
@@ -95,12 +95,14 @@ class TaintTrackingPlugin extends SourceIastPlugin {
|
|
|
95
95
|
|
|
96
96
|
_cookiesTaintTrackingHandler (target) {
|
|
97
97
|
const iastContext = getIastContext(storage.getStore())
|
|
98
|
-
|
|
98
|
+
// Prevent tainting cookie names since it leads to taint literal string with same value.
|
|
99
|
+
taintObject(iastContext, target, HTTP_REQUEST_COOKIE_VALUE)
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
taintHeaders (headers, iastContext) {
|
|
103
|
+
// Prevent tainting header names since it leads to taint literal string with same value.
|
|
102
104
|
this.execSource({
|
|
103
|
-
handler: () => taintObject(iastContext, headers, HTTP_REQUEST_HEADER_VALUE
|
|
105
|
+
handler: () => taintObject(iastContext, headers, HTTP_REQUEST_HEADER_VALUE),
|
|
104
106
|
tags: REQ_HEADER_TAGS,
|
|
105
107
|
iastContext
|
|
106
108
|
})
|
|
@@ -8,7 +8,7 @@ const apiSecuritySampler = require('../api_security_sampler')
|
|
|
8
8
|
|
|
9
9
|
let rc
|
|
10
10
|
|
|
11
|
-
function enable (config) {
|
|
11
|
+
function enable (config, appsec) {
|
|
12
12
|
rc = new RemoteConfigManager(config)
|
|
13
13
|
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_CUSTOM_TAGS, true)
|
|
14
14
|
rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_HTTP_HEADER_TAGS, true)
|
|
@@ -31,7 +31,7 @@ function enable (config) {
|
|
|
31
31
|
if (!rcConfig) return
|
|
32
32
|
|
|
33
33
|
if (activation === Activation.ONECLICK) {
|
|
34
|
-
enableOrDisableAppsec(action, rcConfig, config)
|
|
34
|
+
enableOrDisableAppsec(action, rcConfig, config, appsec)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
apiSecuritySampler.setRequestSampling(rcConfig.api_security?.request_sample_rate)
|
|
@@ -41,7 +41,7 @@ function enable (config) {
|
|
|
41
41
|
return rc
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function enableOrDisableAppsec (action, rcConfig, config) {
|
|
44
|
+
function enableOrDisableAppsec (action, rcConfig, config, appsec) {
|
|
45
45
|
if (typeof rcConfig.asm?.enabled === 'boolean') {
|
|
46
46
|
let shouldEnable
|
|
47
47
|
|
|
@@ -52,9 +52,9 @@ function enableOrDisableAppsec (action, rcConfig, config) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (shouldEnable) {
|
|
55
|
-
|
|
55
|
+
appsec.enable(config)
|
|
56
56
|
} else {
|
|
57
|
-
|
|
57
|
+
appsec.disable()
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -15,6 +15,21 @@ const NoopDogStatsDClient = require('./noop/dogstatsd')
|
|
|
15
15
|
const spanleak = require('./spanleak')
|
|
16
16
|
const { SSITelemetry } = require('./profiling/ssi-telemetry')
|
|
17
17
|
|
|
18
|
+
class LazyModule {
|
|
19
|
+
constructor (provider) {
|
|
20
|
+
this.provider = provider
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
enable (...args) {
|
|
24
|
+
this.module = this.provider()
|
|
25
|
+
this.module.enable(...args)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
disable () {
|
|
29
|
+
this.module?.disable()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
class Tracer extends NoopProxy {
|
|
19
34
|
constructor () {
|
|
20
35
|
super()
|
|
@@ -24,6 +39,12 @@ class Tracer extends NoopProxy {
|
|
|
24
39
|
this._pluginManager = new PluginManager(this)
|
|
25
40
|
this.dogstatsd = new NoopDogStatsDClient()
|
|
26
41
|
this._tracingInitialized = false
|
|
42
|
+
|
|
43
|
+
// these requires must work with esm bundler
|
|
44
|
+
this._modules = {
|
|
45
|
+
appsec: new LazyModule(() => require('./appsec')),
|
|
46
|
+
iast: new LazyModule(() => require('./appsec/iast'))
|
|
47
|
+
}
|
|
27
48
|
}
|
|
28
49
|
|
|
29
50
|
init (options) {
|
|
@@ -58,7 +79,7 @@ class Tracer extends NoopProxy {
|
|
|
58
79
|
}
|
|
59
80
|
|
|
60
81
|
if (config.remoteConfig.enabled && !config.isCiVisibility) {
|
|
61
|
-
const rc = remoteConfig.enable(config)
|
|
82
|
+
const rc = remoteConfig.enable(config, this._modules.appsec)
|
|
62
83
|
|
|
63
84
|
rc.on('APM_TRACING', (action, conf) => {
|
|
64
85
|
if (action === 'unapply') {
|
|
@@ -113,9 +134,8 @@ class Tracer extends NoopProxy {
|
|
|
113
134
|
|
|
114
135
|
_enableOrDisableTracing (config) {
|
|
115
136
|
if (config.tracing !== false) {
|
|
116
|
-
// dirty require for now so zero appsec code is executed unless explicitly enabled
|
|
117
137
|
if (config.appsec.enabled) {
|
|
118
|
-
|
|
138
|
+
this._modules.appsec.enable(config)
|
|
119
139
|
}
|
|
120
140
|
if (!this._tracingInitialized) {
|
|
121
141
|
this._tracer = new DatadogTracer(config)
|
|
@@ -123,11 +143,11 @@ class Tracer extends NoopProxy {
|
|
|
123
143
|
this._tracingInitialized = true
|
|
124
144
|
}
|
|
125
145
|
if (config.iast.enabled) {
|
|
126
|
-
|
|
146
|
+
this._modules.iast.enable(config, this._tracer)
|
|
127
147
|
}
|
|
128
148
|
} else if (this._tracingInitialized) {
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
this._modules.appsec.disable()
|
|
150
|
+
this._modules.iast.disable()
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
if (this._tracingInitialized) {
|