dd-trace 5.100.0 → 5.101.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 +14 -0
- package/package.json +5 -5
- package/packages/datadog-instrumentations/src/cypress.js +5 -3
- package/packages/datadog-instrumentations/src/http/client.js +20 -3
- package/packages/datadog-instrumentations/src/jest.js +62 -32
- package/packages/datadog-instrumentations/src/mocha/common.js +4 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +25 -4
- package/packages/datadog-instrumentations/src/mocha/worker.js +5 -2
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +11 -6
- package/packages/datadog-plugin-bullmq/src/consumer.js +2 -2
- package/packages/datadog-plugin-bullmq/src/producer.js +14 -20
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +17 -0
- package/packages/datadog-plugin-cypress/src/plugin.js +5 -14
- package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -9
- package/packages/datadog-plugin-kafkajs/src/producer.js +2 -8
- package/packages/dd-trace/src/appsec/reporter.js +4 -1
- package/packages/dd-trace/src/ci-visibility/lage.js +2 -1
- package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -33
- package/packages/dd-trace/src/config/config-types.d.ts +0 -2
- package/packages/dd-trace/src/config/index.js +1 -55
- package/packages/dd-trace/src/datastreams/checkpointer.js +4 -10
- package/packages/dd-trace/src/datastreams/encoding.js +39 -28
- package/packages/dd-trace/src/datastreams/pathway.js +29 -26
- package/packages/dd-trace/src/datastreams/processor.js +17 -15
- package/packages/dd-trace/src/datastreams/size.js +6 -2
- package/packages/dd-trace/src/debugger/config.js +5 -2
- package/packages/dd-trace/src/debugger/devtools_client/index.js +2 -5
- package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
- package/packages/dd-trace/src/dogstatsd.js +10 -7
- package/packages/dd-trace/src/encode/0.4.js +2 -2
- package/packages/dd-trace/src/encode/0.5.js +2 -2
- package/packages/dd-trace/src/encode/agentless-json.js +2 -2
- package/packages/dd-trace/src/encode/tags-processors.js +2 -27
- package/packages/dd-trace/src/exporters/common/request.js +22 -11
- package/packages/dd-trace/src/exporters/common/retry.js +104 -0
- package/packages/dd-trace/src/git_metadata.js +66 -0
- package/packages/dd-trace/src/git_metadata_tagger.js +13 -5
- package/packages/dd-trace/src/id.js +15 -26
- package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +27 -16
- package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +3 -0
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +30 -13
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +20 -50
- package/packages/dd-trace/src/llmobs/sdk.js +5 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +28 -2
- package/packages/dd-trace/src/llmobs/tagger.js +42 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +29 -0
- package/packages/dd-trace/src/llmobs/util.js +80 -5
- package/packages/dd-trace/src/opentelemetry/active-span-proxy.js +42 -0
- package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +106 -0
- package/packages/dd-trace/src/opentelemetry/context_manager.js +11 -2
- package/packages/dd-trace/src/opentelemetry/span-helpers.js +188 -50
- package/packages/dd-trace/src/opentelemetry/span.js +42 -80
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +65 -27
- package/packages/dd-trace/src/opentracing/propagation/tracestate.js +58 -22
- package/packages/dd-trace/src/opentracing/span.js +56 -48
- package/packages/dd-trace/src/opentracing/span_context.js +1 -0
- package/packages/dd-trace/src/priority_sampler.js +6 -4
- package/packages/dd-trace/src/profiling/config.js +5 -4
- package/packages/dd-trace/src/remote_config/index.js +5 -3
- package/packages/dd-trace/src/span_format.js +52 -5
- package/packages/dd-trace/src/span_processor.js +0 -4
- package/packages/dd-trace/src/spanleak.js +0 -1
- package/packages/dd-trace/src/util.js +17 -0
|
@@ -237,15 +237,23 @@ function formatContentObject (content) {
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
// Two filter passes over `parts` collapse to a single walk. Most parts are
|
|
241
|
+
// text-only so neither bucket is allocated unless a matching part appears.
|
|
242
|
+
let functionCalls
|
|
243
|
+
let functionResponses
|
|
244
|
+
for (const part of parts) {
|
|
245
|
+
if (part.functionCall) {
|
|
246
|
+
(functionCalls ??= []).push(part)
|
|
247
|
+
} else if (part.functionResponse) {
|
|
248
|
+
(functionResponses ??= []).push(part)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (functionCalls) {
|
|
243
253
|
return formatFunctionCallMessage(parts, functionCalls, role)
|
|
244
254
|
}
|
|
245
255
|
|
|
246
|
-
|
|
247
|
-
const functionResponses = parts.filter(part => part.functionResponse)
|
|
248
|
-
if (functionResponses.length > 0) {
|
|
256
|
+
if (functionResponses) {
|
|
249
257
|
return formatFunctionResponseMessage(functionResponses, role)
|
|
250
258
|
}
|
|
251
259
|
|
|
@@ -326,15 +334,26 @@ function formatNonStreamingCandidate (candidate) {
|
|
|
326
334
|
|
|
327
335
|
const { parts } = content
|
|
328
336
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
337
|
+
// One walk replaces three (`filter` + two `find`); priority order is
|
|
338
|
+
// functionCall > executableCode > codeExecutionResult, same as before.
|
|
339
|
+
let functionCalls
|
|
340
|
+
let executableCode
|
|
341
|
+
let codeExecutionResult
|
|
342
|
+
for (const part of parts) {
|
|
343
|
+
if (part.functionCall) {
|
|
344
|
+
(functionCalls ??= []).push(part)
|
|
345
|
+
} else if (!executableCode && part.executableCode) {
|
|
346
|
+
executableCode = part
|
|
347
|
+
} else if (!codeExecutionResult && part.codeExecutionResult) {
|
|
348
|
+
codeExecutionResult = part
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (functionCalls) {
|
|
332
353
|
messages.push(formatFunctionCallMessage(parts, functionCalls, ROLES.ASSISTANT))
|
|
333
354
|
return messages
|
|
334
355
|
}
|
|
335
356
|
|
|
336
|
-
// Check for executable code
|
|
337
|
-
const executableCode = parts.find(part => part.executableCode)
|
|
338
357
|
if (executableCode) {
|
|
339
358
|
messages.push({
|
|
340
359
|
role: ROLES.ASSISTANT,
|
|
@@ -346,8 +365,6 @@ function formatNonStreamingCandidate (candidate) {
|
|
|
346
365
|
return messages
|
|
347
366
|
}
|
|
348
367
|
|
|
349
|
-
// Check for code execution result
|
|
350
|
-
const codeExecutionResult = parts.find(part => part.codeExecutionResult)
|
|
351
368
|
if (codeExecutionResult) {
|
|
352
369
|
messages.push({
|
|
353
370
|
role: ROLES.ASSISTANT,
|
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
INSTRUMENTATION_METHOD_AUTO,
|
|
8
8
|
UNKNOWN_MODEL_PROVIDER,
|
|
9
9
|
} = require('../../constants/tags')
|
|
10
|
+
const { safeJsonParse } = require('../../util')
|
|
10
11
|
const {
|
|
11
12
|
extractChatTemplateFromInstructions,
|
|
12
13
|
normalizePromptVariables,
|
|
@@ -183,13 +184,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
183
184
|
_tagChatCompletion (span, inputs, response, error) {
|
|
184
185
|
const { messages, model, ...parameters } = inputs
|
|
185
186
|
|
|
186
|
-
const metadata =
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
const metadata = {}
|
|
188
|
+
for (const key of Object.keys(parameters)) {
|
|
189
|
+
if (key !== 'tools' && key !== 'functions') {
|
|
190
|
+
metadata[key] = parameters[key]
|
|
189
191
|
}
|
|
190
|
-
|
|
191
|
-
return obj
|
|
192
|
-
}, {})
|
|
192
|
+
}
|
|
193
193
|
|
|
194
194
|
this._tagger.tagMetadata(span, metadata)
|
|
195
195
|
|
|
@@ -213,14 +213,14 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
213
213
|
if (message.function_call) {
|
|
214
214
|
const functionCallInfo = {
|
|
215
215
|
name: message.function_call.name,
|
|
216
|
-
arguments:
|
|
216
|
+
arguments: safeJsonParse(message.function_call.arguments),
|
|
217
217
|
}
|
|
218
218
|
outputMessages.push({ content, role, toolCalls: [functionCallInfo] })
|
|
219
219
|
} else if (message.tool_calls) {
|
|
220
220
|
const toolCallsInfo = []
|
|
221
221
|
for (const toolCall of message.tool_calls) {
|
|
222
222
|
const toolCallInfo = {
|
|
223
|
-
arguments:
|
|
223
|
+
arguments: safeJsonParse(toolCall.function.arguments),
|
|
224
224
|
name: toolCall.function.name,
|
|
225
225
|
toolId: toolCall.id,
|
|
226
226
|
type: toolCall.type,
|
|
@@ -277,22 +277,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
277
277
|
inputMessages.push({ role, content })
|
|
278
278
|
}
|
|
279
279
|
} else if (item.type === 'function_call') {
|
|
280
|
-
// Function call: convert to message with tool_calls
|
|
281
|
-
// Parse arguments if it's a JSON string
|
|
282
|
-
let parsedArgs = item.arguments
|
|
283
|
-
if (typeof parsedArgs === 'string') {
|
|
284
|
-
try {
|
|
285
|
-
parsedArgs = JSON.parse(parsedArgs)
|
|
286
|
-
} catch {
|
|
287
|
-
parsedArgs = {}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
280
|
inputMessages.push({
|
|
291
281
|
role: 'assistant',
|
|
292
282
|
toolCalls: [{
|
|
293
283
|
toolId: item.call_id,
|
|
294
284
|
name: item.name,
|
|
295
|
-
arguments:
|
|
285
|
+
arguments: safeJsonParse(item.arguments, {}),
|
|
296
286
|
type: item.type,
|
|
297
287
|
}],
|
|
298
288
|
})
|
|
@@ -317,12 +307,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
317
307
|
inputMessages.push({ role: 'user', content: input })
|
|
318
308
|
}
|
|
319
309
|
|
|
320
|
-
const inputMetadata =
|
|
310
|
+
const inputMetadata = {}
|
|
311
|
+
for (const key of Object.keys(parameters)) {
|
|
321
312
|
if (allowedParamKeys.has(key)) {
|
|
322
|
-
|
|
313
|
+
inputMetadata[key] = parameters[key]
|
|
323
314
|
}
|
|
324
|
-
|
|
325
|
-
}, {})
|
|
315
|
+
}
|
|
326
316
|
|
|
327
317
|
this._tagger.tagMetadata(span, inputMetadata)
|
|
328
318
|
|
|
@@ -354,21 +344,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
354
344
|
})
|
|
355
345
|
} else if (item.type === 'function_call') {
|
|
356
346
|
// Handle function_call type (responses API tool calls)
|
|
357
|
-
let args = item.arguments
|
|
358
|
-
// Parse arguments if it's a JSON string
|
|
359
|
-
if (typeof args === 'string') {
|
|
360
|
-
try {
|
|
361
|
-
args = JSON.parse(args)
|
|
362
|
-
} catch {
|
|
363
|
-
args = {}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
347
|
outputMessages.push({
|
|
367
348
|
role: 'assistant',
|
|
368
349
|
toolCalls: [{
|
|
369
350
|
toolId: item.call_id,
|
|
370
351
|
name: item.name,
|
|
371
|
-
arguments:
|
|
352
|
+
arguments: safeJsonParse(item.arguments, {}),
|
|
372
353
|
type: item.type,
|
|
373
354
|
}],
|
|
374
355
|
})
|
|
@@ -390,23 +371,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
390
371
|
|
|
391
372
|
// Extract tool calls if present in message.tool_calls
|
|
392
373
|
if (Array.isArray(item.tool_calls)) {
|
|
393
|
-
outputMsg.toolCalls = item.tool_calls.map(tc => {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} catch {
|
|
400
|
-
args = {}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
toolId: tc.id,
|
|
405
|
-
name: tc.function?.name || tc.name,
|
|
406
|
-
arguments: args,
|
|
407
|
-
type: tc.type || 'function_call',
|
|
408
|
-
}
|
|
409
|
-
})
|
|
374
|
+
outputMsg.toolCalls = item.tool_calls.map(tc => ({
|
|
375
|
+
toolId: tc.id,
|
|
376
|
+
name: tc.function?.name || tc.name,
|
|
377
|
+
arguments: safeJsonParse(tc.function?.arguments || tc.arguments, {}),
|
|
378
|
+
type: tc.type || 'function_call',
|
|
379
|
+
}))
|
|
410
380
|
}
|
|
411
381
|
|
|
412
382
|
outputMessages.push(outputMsg)
|
|
@@ -251,7 +251,7 @@ class LLMObs extends NoopLLMObs {
|
|
|
251
251
|
throw new Error('LLMObs span must have a span kind specified')
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
const { inputData, outputData, metadata, metrics, tags, prompt } = options
|
|
254
|
+
const { inputData, outputData, metadata, metrics, tags, prompt, costTags } = options
|
|
255
255
|
|
|
256
256
|
if (inputData || outputData) {
|
|
257
257
|
if (spanKind === 'llm') {
|
|
@@ -271,9 +271,13 @@ class LLMObs extends NoopLLMObs {
|
|
|
271
271
|
if (metrics) {
|
|
272
272
|
this._tagger.tagMetrics(span, metrics)
|
|
273
273
|
}
|
|
274
|
+
// Apply tags before costTags so costTags can reference tags from the same annotation.
|
|
274
275
|
if (tags) {
|
|
275
276
|
this._tagger.tagSpanTags(span, tags)
|
|
276
277
|
}
|
|
278
|
+
if (costTags != null) {
|
|
279
|
+
this._tagger.tagCostTags(span, costTags, 'annotate')
|
|
280
|
+
}
|
|
277
281
|
if (prompt) {
|
|
278
282
|
this._tagger.tagPrompt(span, prompt)
|
|
279
283
|
}
|
|
@@ -14,6 +14,8 @@ const {
|
|
|
14
14
|
MODEL_NAME,
|
|
15
15
|
MODEL_PROVIDER,
|
|
16
16
|
METADATA,
|
|
17
|
+
COST_TAGS,
|
|
18
|
+
TOOL_DEFINITIONS,
|
|
17
19
|
INPUT_MESSAGES,
|
|
18
20
|
INPUT_VALUE,
|
|
19
21
|
INTEGRATION,
|
|
@@ -130,8 +132,20 @@ class LLMObsSpanProcessor {
|
|
|
130
132
|
meta.model_provider = (mlObsTags[MODEL_PROVIDER] || 'custom').toLowerCase()
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
if (mlObsTags[METADATA]) {
|
|
134
|
-
|
|
135
|
+
if (mlObsTags[METADATA] || mlObsTags[COST_TAGS]) {
|
|
136
|
+
const metadata = {}
|
|
137
|
+
if (mlObsTags[METADATA]) this.#addObject(mlObsTags[METADATA], metadata)
|
|
138
|
+
// Only seed `metadata._dd` when there's something to put in it (currently cost_tags). Mirrors
|
|
139
|
+
// dd-trace-py and the cross-language wire format enforced by system-tests — metadata-only
|
|
140
|
+
// spans must not carry an empty `_dd: {}` block.
|
|
141
|
+
if (mlObsTags[COST_TAGS]) {
|
|
142
|
+
this.#getDdMetadata(metadata).cost_tags = mlObsTags[COST_TAGS]
|
|
143
|
+
}
|
|
144
|
+
meta.metadata = metadata
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (mlObsTags[TOOL_DEFINITIONS]) {
|
|
148
|
+
this.#addObject(mlObsTags[TOOL_DEFINITIONS], meta.tool_definitions = [])
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
|
|
@@ -259,6 +273,18 @@ class LLMObsSpanProcessor {
|
|
|
259
273
|
add(obj, carrier)
|
|
260
274
|
}
|
|
261
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Returns `metadata._dd`, normalizing it to a fresh object if missing or invalid.
|
|
278
|
+
* @param {Record<string, unknown>} metadata
|
|
279
|
+
* @returns {Record<string, unknown>}
|
|
280
|
+
*/
|
|
281
|
+
#getDdMetadata (metadata) {
|
|
282
|
+
if (!metadata._dd || typeof metadata._dd !== 'object' || Array.isArray(metadata._dd)) {
|
|
283
|
+
metadata._dd = {}
|
|
284
|
+
}
|
|
285
|
+
return metadata._dd
|
|
286
|
+
}
|
|
287
|
+
|
|
262
288
|
#getTags (span, mlApp, sessionId, error) {
|
|
263
289
|
let tags = {
|
|
264
290
|
...this.#config.parsedDdTags,
|
|
@@ -12,7 +12,9 @@ const {
|
|
|
12
12
|
INPUT_DOCUMENTS,
|
|
13
13
|
OUTPUT_VALUE,
|
|
14
14
|
METADATA,
|
|
15
|
+
COST_TAGS,
|
|
15
16
|
METRICS,
|
|
17
|
+
TOOL_DEFINITIONS,
|
|
16
18
|
PARENT_ID_KEY,
|
|
17
19
|
INPUT_MESSAGES,
|
|
18
20
|
OUTPUT_MESSAGES,
|
|
@@ -41,6 +43,7 @@ const {
|
|
|
41
43
|
INSTRUMENTATION_METHOD_ANNOTATED,
|
|
42
44
|
} = require('./constants/tags')
|
|
43
45
|
const { storage } = require('./storage')
|
|
46
|
+
const { validateCostTags } = require('./util')
|
|
44
47
|
|
|
45
48
|
// global registry of LLMObs spans
|
|
46
49
|
// maps LLMObs spans to their annotations
|
|
@@ -120,6 +123,11 @@ class LLMObsTagger {
|
|
|
120
123
|
const tags = annotationContext?.tags
|
|
121
124
|
if (tags) this.tagSpanTags(span, tags)
|
|
122
125
|
|
|
126
|
+
// apply after tags so only keys present at span start are accepted.
|
|
127
|
+
if (annotationContext?.costTags != null) {
|
|
128
|
+
this.tagCostTags(span, annotationContext.costTags, 'annotation_context')
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
// apply annotation context name
|
|
124
132
|
const annotationContextName = annotationContext?.name
|
|
125
133
|
if (annotationContextName) this._setTag(span, NAME, annotationContextName)
|
|
@@ -159,6 +167,14 @@ class LLMObsTagger {
|
|
|
159
167
|
this.#tagText(span, outputData, OUTPUT_VALUE)
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
tagToolDefinitions (span, toolDefinitions) {
|
|
171
|
+
if (Array.isArray(toolDefinitions) && toolDefinitions.length > 0) {
|
|
172
|
+
this._setTag(span, TOOL_DEFINITIONS, toolDefinitions)
|
|
173
|
+
} else {
|
|
174
|
+
this.#handleFailure('Tool definitions must be a non-empty array.', 'invalid_tool_definitions')
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
162
178
|
tagMetadata (span, metadata) {
|
|
163
179
|
const existingMetadata = registry.get(span)?.[METADATA]
|
|
164
180
|
if (existingMetadata) {
|
|
@@ -225,6 +241,32 @@ class LLMObsTagger {
|
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Validates and tags cost tag keys on an LLMObs span. Cost tag references are validated against
|
|
246
|
+
* the span's already-applied tags, which are read from the registry.
|
|
247
|
+
* @param {import('../opentracing/span')} span
|
|
248
|
+
* @param {unknown} costTags Raw user-provided cost tags; validated here.
|
|
249
|
+
* @param {'annotate' | 'annotation_context'} source
|
|
250
|
+
*/
|
|
251
|
+
tagCostTags (span, costTags, source) {
|
|
252
|
+
const spanTags = registry.get(span)?.[TAGS] || {}
|
|
253
|
+
const validatedCostTags = validateCostTags(span, costTags, source, spanTags)
|
|
254
|
+
if (!validatedCostTags.length) return
|
|
255
|
+
|
|
256
|
+
// Might consider switching to a `Set` if per-span cost tag cardinality grows large enough that
|
|
257
|
+
// this `.includes`/`.push` merge becomes a hot spot
|
|
258
|
+
const currentCostTags = registry.get(span)?.[COST_TAGS]
|
|
259
|
+
if (currentCostTags) {
|
|
260
|
+
for (const costTag of validatedCostTags) {
|
|
261
|
+
if (!currentCostTags.includes(costTag)) {
|
|
262
|
+
currentCostTags.push(costTag)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
this._setTag(span, COST_TAGS, validatedCostTags)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
228
270
|
/**
|
|
229
271
|
* Tags a prompt on an LLMObs span.
|
|
230
272
|
* @param {import('../opentracing/span')} span
|
|
@@ -6,6 +6,7 @@ const telemetryMetrics = require('../telemetry/metrics')
|
|
|
6
6
|
const {
|
|
7
7
|
SPAN_KIND,
|
|
8
8
|
MODEL_PROVIDER,
|
|
9
|
+
ML_APP,
|
|
9
10
|
PARENT_ID_KEY,
|
|
10
11
|
SESSION_ID,
|
|
11
12
|
ROOT_PARENT_ID,
|
|
@@ -123,6 +124,32 @@ function recordLLMObsAnnotate (span, err, value = 1) {
|
|
|
123
124
|
llmobsMetrics.count('annotations', tags).inc(value)
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
function recordCostTagsAnnotated (span, source, value = 1) {
|
|
128
|
+
const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
|
|
129
|
+
const tags = {
|
|
130
|
+
span_kind: mlObsTags[SPAN_KIND] || 'N/A',
|
|
131
|
+
source,
|
|
132
|
+
ml_app: mlObsTags[ML_APP] || 'N/A',
|
|
133
|
+
model_provider: mlObsTags[MODEL_PROVIDER] || 'N/A',
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
llmobsMetrics.count('cost_tags.annotated', tags).inc(value)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function recordCostTagsSubmitted (span, count, source, state, reason = 'none') {
|
|
140
|
+
const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
|
|
141
|
+
const tags = {
|
|
142
|
+
span_kind: mlObsTags[SPAN_KIND] || 'N/A',
|
|
143
|
+
source,
|
|
144
|
+
ml_app: mlObsTags[ML_APP] || 'N/A',
|
|
145
|
+
model_provider: mlObsTags[MODEL_PROVIDER] || 'N/A',
|
|
146
|
+
state,
|
|
147
|
+
reason,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
llmobsMetrics.count('cost_tags.submitted', tags).inc(count)
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
function recordUserFlush (err, value = 1) {
|
|
127
154
|
const tags = { error: Number(!!err) }
|
|
128
155
|
if (err) tags.error_type = err
|
|
@@ -167,6 +194,8 @@ module.exports = {
|
|
|
167
194
|
recordLLMObsSpanSize,
|
|
168
195
|
recordDroppedPayload,
|
|
169
196
|
recordLLMObsAnnotate,
|
|
197
|
+
recordCostTagsAnnotated,
|
|
198
|
+
recordCostTagsSubmitted,
|
|
170
199
|
recordUserFlush,
|
|
171
200
|
recordExportSpan,
|
|
172
201
|
recordSubmitEvaluation,
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const log = require('../log')
|
|
3
4
|
const { SPAN_KINDS } = require('./constants/tags')
|
|
4
5
|
|
|
6
|
+
// LLM I/O is overwhelmingly ASCII (English prompts and code). Walk once
|
|
7
|
+
// looking for the first non-ASCII char; if there is none, hand the input
|
|
8
|
+
// straight back. Otherwise pick up the slow path from the byte that needed
|
|
9
|
+
// escaping. ~5x faster on typical prompt strings than the per-char `+=`
|
|
10
|
+
// loop the function used to do unconditionally.
|
|
5
11
|
function encodeUnicode (str = '') {
|
|
6
|
-
let
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
for (let index = 0; index < str.length; index++) {
|
|
13
|
+
if (str.charCodeAt(index) > 127) {
|
|
14
|
+
let result = str.slice(0, index)
|
|
15
|
+
for (; index < str.length; index++) {
|
|
16
|
+
const code = str.charCodeAt(index)
|
|
17
|
+
result += code > 127 ? String.raw`\u${code.toString(16).padStart(4, '0')}` : str[index]
|
|
18
|
+
}
|
|
19
|
+
return result
|
|
20
|
+
}
|
|
10
21
|
}
|
|
11
|
-
return
|
|
22
|
+
return str
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
function validateKind (kind) {
|
|
@@ -22,6 +33,57 @@ function validateKind (kind) {
|
|
|
22
33
|
return kind
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Validates cost tag keys and records telemetry for the annotation source.
|
|
38
|
+
* @param {import('../opentracing/span')} span
|
|
39
|
+
* @param {unknown} costTags
|
|
40
|
+
* @param {string} source
|
|
41
|
+
* @param {Record<string, unknown>} spanTags
|
|
42
|
+
* @returns {string[]}
|
|
43
|
+
*/
|
|
44
|
+
function validateCostTags (span, costTags, source, spanTags) {
|
|
45
|
+
// Lazy-required to avoid the `index.js -> telemetry -> tagger -> util` module cycle.
|
|
46
|
+
const telemetry = require('./telemetry')
|
|
47
|
+
|
|
48
|
+
telemetry.recordCostTagsAnnotated(span, source)
|
|
49
|
+
|
|
50
|
+
if (!Array.isArray(costTags)) {
|
|
51
|
+
log.warn('costTags must be an array of strings. Ignoring value.')
|
|
52
|
+
telemetry.recordCostTagsSubmitted(span, 1, source, 'error', 'non_list')
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const validatedCostTags = new Set()
|
|
57
|
+
let nonStringEntries = 0
|
|
58
|
+
let missingSpanTags = 0
|
|
59
|
+
|
|
60
|
+
for (const costTag of costTags) {
|
|
61
|
+
if (typeof costTag !== 'string') {
|
|
62
|
+
log.warn('costTags entries must be strings. Skipping entry %s.', costTag)
|
|
63
|
+
nonStringEntries++
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
if (!Object.hasOwn(spanTags, costTag)) {
|
|
67
|
+
log.warn('costTags entry "%s" must reference a key present in span tags. Skipping entry.', costTag)
|
|
68
|
+
missingSpanTags++
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
validatedCostTags.add(costTag)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (nonStringEntries) {
|
|
75
|
+
telemetry.recordCostTagsSubmitted(span, nonStringEntries, source, 'error', 'non_string_entry')
|
|
76
|
+
}
|
|
77
|
+
if (missingSpanTags) {
|
|
78
|
+
telemetry.recordCostTagsSubmitted(span, missingSpanTags, source, 'error', 'missing_span_tag')
|
|
79
|
+
}
|
|
80
|
+
if (validatedCostTags.size) {
|
|
81
|
+
telemetry.recordCostTagsSubmitted(span, validatedCostTags.size, source, 'success')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [...validatedCostTags]
|
|
85
|
+
}
|
|
86
|
+
|
|
25
87
|
// extracts the argument names from a function string
|
|
26
88
|
function parseArgumentNames (str) {
|
|
27
89
|
const result = []
|
|
@@ -174,9 +236,22 @@ function spanHasError (span) {
|
|
|
174
236
|
return !!(tags.error || tags['error.type'])
|
|
175
237
|
}
|
|
176
238
|
|
|
239
|
+
// LLM SDKs stream tool-call argument JSON across SSE chunks; a malformed
|
|
240
|
+
// accumulation would otherwise throw straight into the chunk subscriber.
|
|
241
|
+
function safeJsonParse (value, fallback) {
|
|
242
|
+
if (typeof value !== 'string') return value
|
|
243
|
+
try {
|
|
244
|
+
return JSON.parse(value)
|
|
245
|
+
} catch {
|
|
246
|
+
return fallback === undefined ? value : fallback
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
177
250
|
module.exports = {
|
|
178
251
|
encodeUnicode,
|
|
252
|
+
validateCostTags,
|
|
179
253
|
validateKind,
|
|
180
254
|
getFunctionArguments,
|
|
255
|
+
safeJsonParse,
|
|
181
256
|
spanHasError,
|
|
182
257
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const BridgeSpanBase = require('./bridge-span-base')
|
|
4
|
+
const { setOtelResource } = require('./span-helpers')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OTel `Span`-compatible proxy around an already-active Datadog span.
|
|
8
|
+
*
|
|
9
|
+
* Makes `trace.getActiveSpan()` forward attribute/link/event/status/exception writes onto
|
|
10
|
+
* the Datadog span. `end()` is intentionally a no-op: the span's lifecycle belongs to
|
|
11
|
+
* whoever created it. Mutation methods all bail out once the underlying Datadog span has
|
|
12
|
+
* finished (gated inside the helpers), matching OTel `Span` semantics.
|
|
13
|
+
*/
|
|
14
|
+
class ActiveSpanProxy extends BridgeSpanBase {
|
|
15
|
+
/** @type {import('./span_context')} */
|
|
16
|
+
#otelSpanContext
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('../opentracing/span')} ddSpan
|
|
20
|
+
* @param {import('./span_context')} otelSpanContext
|
|
21
|
+
*/
|
|
22
|
+
constructor (ddSpan, otelSpanContext) {
|
|
23
|
+
super(ddSpan)
|
|
24
|
+
this.#otelSpanContext = otelSpanContext
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
spanContext () {
|
|
28
|
+
return this.#otelSpanContext
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} name
|
|
33
|
+
*/
|
|
34
|
+
updateName (name) {
|
|
35
|
+
setOtelResource(this._ddSpan, name)
|
|
36
|
+
return this
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
end () {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = ActiveSpanProxy
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
addOtelEvent,
|
|
5
|
+
addOtelLink,
|
|
6
|
+
addOtelLinks,
|
|
7
|
+
applyOtelStatus,
|
|
8
|
+
recordException,
|
|
9
|
+
setOtelAttribute,
|
|
10
|
+
setOtelAttributes,
|
|
11
|
+
} = require('./span-helpers')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shared base for the OTel-bridge span classes (`Span` and `ActiveSpanProxy`). Subclasses
|
|
15
|
+
* pass the underlying Datadog span to `super(ddSpan)` and provide `spanContext()`, `end()`,
|
|
16
|
+
* and `updateName()`. The writable-span gate lives in the helpers in `span-helpers.js`,
|
|
17
|
+
* so neither bridge can drift from it.
|
|
18
|
+
*
|
|
19
|
+
* `_ddSpan` is left as a `_underscore` field rather than `#private` so the bridge does not
|
|
20
|
+
* expand its published API to expose the underlying DD span. External callers that need
|
|
21
|
+
* the reference (`ContextManager` proxy-cache check, OTLP serialization, tests) reach in
|
|
22
|
+
* via `_ddSpan`, matching the existing convention for "internal, may break".
|
|
23
|
+
*/
|
|
24
|
+
class BridgeSpanBase {
|
|
25
|
+
// OTel SpanStatusCode: 0 = UNSET, 1 = OK, 2 = ERROR. Tracked for OK-is-final precedence.
|
|
26
|
+
#statusCode = 0
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('../opentracing/span')} ddSpan
|
|
30
|
+
*/
|
|
31
|
+
constructor (ddSpan) {
|
|
32
|
+
this._ddSpan = ddSpan
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get ended () {
|
|
36
|
+
return this._ddSpan._duration !== undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isRecording () {
|
|
40
|
+
return !this.ended
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} key
|
|
45
|
+
* @param {import('@opentelemetry/api').AttributeValue} value
|
|
46
|
+
*/
|
|
47
|
+
setAttribute (key, value) {
|
|
48
|
+
setOtelAttribute(this._ddSpan, key, value)
|
|
49
|
+
return this
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {import('@opentelemetry/api').Attributes} attributes
|
|
54
|
+
*/
|
|
55
|
+
setAttributes (attributes) {
|
|
56
|
+
setOtelAttributes(this._ddSpan, attributes)
|
|
57
|
+
return this
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} name
|
|
62
|
+
* @param {import('@opentelemetry/api').Attributes | import('@opentelemetry/api').TimeInput} [attributesOrStartTime]
|
|
63
|
+
* @param {import('@opentelemetry/api').TimeInput} [startTime]
|
|
64
|
+
*/
|
|
65
|
+
addEvent (name, attributesOrStartTime, startTime) {
|
|
66
|
+
addOtelEvent(this._ddSpan, name, attributesOrStartTime, startTime)
|
|
67
|
+
return this
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Accepts the OTel `Link` shape and the deprecated `(SpanContext, Attributes)` form.
|
|
72
|
+
*
|
|
73
|
+
* @param {import('@opentelemetry/api').Link | import('@opentelemetry/api').SpanContext} link
|
|
74
|
+
* @param {import('@opentelemetry/api').Attributes} [attrs]
|
|
75
|
+
*/
|
|
76
|
+
addLink (link, attrs) {
|
|
77
|
+
addOtelLink(this._ddSpan, link, attrs)
|
|
78
|
+
return this
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {import('@opentelemetry/api').Link[]} links
|
|
83
|
+
*/
|
|
84
|
+
addLinks (links) {
|
|
85
|
+
addOtelLinks(this._ddSpan, links)
|
|
86
|
+
return this
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {import('@opentelemetry/api').Exception} exception
|
|
91
|
+
* @param {import('@opentelemetry/api').TimeInput} [timeInput]
|
|
92
|
+
*/
|
|
93
|
+
recordException (exception, timeInput) {
|
|
94
|
+
recordException(this._ddSpan, exception, timeInput)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {import('@opentelemetry/api').SpanStatus} status
|
|
99
|
+
*/
|
|
100
|
+
setStatus (status) {
|
|
101
|
+
this.#statusCode = applyOtelStatus(this._ddSpan, this.#statusCode, status)
|
|
102
|
+
return this
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = BridgeSpanBase
|