dd-trace 6.17.0 → 6.19.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 -0
- package/ci/vitest-no-worker-init-setup.mjs +22 -12
- package/index.d.ts +55 -11
- package/package.json +6 -6
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
- package/packages/datadog-instrumentations/src/cucumber.js +50 -9
- package/packages/datadog-instrumentations/src/cypress-config.js +27 -15
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +6 -0
- package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -6
- package/packages/datadog-instrumentations/src/helpers/register.js +20 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +41 -4
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +15 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +51 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +13 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +28 -3
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +6 -0
- package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
- package/packages/datadog-instrumentations/src/jest.js +30 -13
- package/packages/datadog-instrumentations/src/mocha/main.js +7 -5
- package/packages/datadog-instrumentations/src/mocha/utils.js +8 -8
- package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -4
- package/packages/datadog-instrumentations/src/playwright.js +73 -6
- package/packages/datadog-instrumentations/src/undici.js +7 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +95 -21
- package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
- package/packages/datadog-instrumentations/src/webdriverio.js +28 -12
- package/packages/datadog-instrumentations/src/ws.js +41 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
- package/packages/datadog-plugin-cucumber/src/index.js +22 -8
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -14
- package/packages/datadog-plugin-cypress/src/index.js +3 -1
- package/packages/datadog-plugin-fetch/src/index.js +3 -0
- package/packages/datadog-plugin-jest/src/index.js +3 -4
- package/packages/datadog-plugin-mocha/src/index.js +3 -4
- package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
- package/packages/datadog-plugin-playwright/src/index.js +11 -6
- package/packages/datadog-plugin-undici/src/index.js +5 -0
- package/packages/datadog-plugin-vitest/src/index.js +3 -4
- package/packages/datadog-plugin-ws/src/producer.js +1 -1
- package/packages/datadog-shimmer/src/shimmer.js +14 -10
- package/packages/datadog-turbopack/index.js +4 -1
- package/packages/datadog-turbopack/src/loader.js +6 -2
- package/packages/dd-trace/src/agent/info.js +5 -4
- package/packages/dd-trace/src/aiguard/client.js +38 -19
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
- package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +19 -19
- package/packages/dd-trace/src/config/index.js +7 -6
- package/packages/dd-trace/src/config/supported-configurations.json +16 -5
- package/packages/dd-trace/src/constants.js +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +5 -3
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
- package/packages/dd-trace/src/debugger/index.js +1 -1
- package/packages/dd-trace/src/evp_proxy/constants.js +2 -0
- package/packages/dd-trace/src/evp_proxy/direct.js +17 -13
- package/packages/dd-trace/src/evp_proxy/discovery.js +14 -10
- package/packages/dd-trace/src/evp_proxy/path.js +21 -7
- package/packages/dd-trace/src/exporters/common/url.js +33 -2
- package/packages/dd-trace/src/llmobs/constants/tags.js +31 -8
- package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
- package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +13 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
- package/packages/dd-trace/src/llmobs/prompts/manager.js +10 -2
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +79 -12
- package/packages/dd-trace/src/llmobs/span_processor.js +10 -65
- package/packages/dd-trace/src/llmobs/tagger.js +11 -40
- package/packages/dd-trace/src/openfeature/configuration_source.js +4 -1
- package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -3
- package/packages/dd-trace/src/openfeature/index.js +6 -2
- package/packages/dd-trace/src/openfeature/writers/base.js +90 -17
- package/packages/dd-trace/src/openfeature/writers/exposures.js +4 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +104 -33
- package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
- package/packages/dd-trace/src/opentracing/tracer.js +3 -3
- package/packages/dd-trace/src/plugin_manager.js +27 -8
- package/packages/dd-trace/src/plugins/ci_plugin.js +1 -1
- package/packages/dd-trace/src/plugins/util/test.js +13 -4
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/proxy.js +2 -2
- package/packages/dd-trace/src/ritm.js +12 -9
- package/packages/dd-trace/src/span_processor.js +6 -1
- package/packages/dd-trace/src/telemetry/send-data.js +2 -1
- package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
- package/packages/dd-trace/src/tracer.js +5 -4
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/packages/datadog-instrumentations/src/azure-cosmos.js +0 -7
- package/packages/datadog-instrumentations/src/bullmq.js +0 -11
- package/packages/datadog-instrumentations/src/langchain.js +0 -7
- package/packages/datadog-instrumentations/src/langgraph.js +0 -7
- package/packages/datadog-instrumentations/src/mercurius.js +0 -11
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ARTIFICIAL_GEN_AI_TAGS,
|
|
5
|
+
CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
6
|
+
CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
7
|
+
DEFAULT_MODEL,
|
|
8
|
+
GEN_AI_APPLICATION_NAME,
|
|
9
|
+
GEN_AI_CONVERSATION_ID,
|
|
10
|
+
GEN_AI_OPERATION_NAME,
|
|
11
|
+
GEN_AI_PROVIDER_NAME,
|
|
12
|
+
GEN_AI_REQUEST_MODEL,
|
|
13
|
+
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
14
|
+
GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
15
|
+
GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY,
|
|
16
|
+
GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY,
|
|
17
|
+
GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
18
|
+
GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY,
|
|
19
|
+
INPUT_TOKENS_METRIC_KEY,
|
|
20
|
+
METRIC_KEY_ALIASES,
|
|
21
|
+
OUTPUT_TOKENS_METRIC_KEY,
|
|
22
|
+
REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
23
|
+
TOTAL_TOKENS_METRIC_KEY,
|
|
24
|
+
} = require('./constants/tags')
|
|
25
|
+
|
|
26
|
+
/** @type {Set<string | undefined>} */
|
|
27
|
+
const MODEL_BACKED_SPAN_KINDS = new Set(['llm', 'embedding'])
|
|
28
|
+
|
|
29
|
+
// null prototype: a metric named after an `Object.prototype` member must not resolve to an
|
|
30
|
+
// inherited property
|
|
31
|
+
const GEN_AI_USAGE_METRIC_KEYS = Object.assign(Object.create(null), {
|
|
32
|
+
[INPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY,
|
|
33
|
+
[OUTPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY,
|
|
34
|
+
[TOTAL_TOKENS_METRIC_KEY]: GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY,
|
|
35
|
+
[CACHE_READ_INPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
36
|
+
[CACHE_WRITE_INPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
37
|
+
[REASONING_OUTPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} GenAiApmTags
|
|
42
|
+
* @property {string} [spanKind] LLMObs span kind
|
|
43
|
+
* @property {string} [modelName]
|
|
44
|
+
* @property {string} [modelProvider]
|
|
45
|
+
* @property {string} [mlApp]
|
|
46
|
+
* @property {string} [sessionId]
|
|
47
|
+
* @property {Record<string, unknown>} [metrics] LLMObs metrics, in either spelling
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Writes the scalar `gen_ai.*` attributes onto the APM span, so model, provider, application,
|
|
52
|
+
* conversation and token usage are searchable in APM. Message bodies stay off the APM span.
|
|
53
|
+
*
|
|
54
|
+
* @param {import('../opentracing/span')} span
|
|
55
|
+
* @param {GenAiApmTags} tags
|
|
56
|
+
*/
|
|
57
|
+
function setGenAiApmTags (span, tags) {
|
|
58
|
+
// mirrors the LLMObs span event: a model-backed span always reports a model and provider
|
|
59
|
+
updateGenAiApmTags(span, MODEL_BACKED_SPAN_KINDS.has(tags.spanKind)
|
|
60
|
+
? { ...tags, modelName: tags.modelName || DEFAULT_MODEL, modelProvider: tags.modelProvider || DEFAULT_MODEL }
|
|
61
|
+
: tags)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Writes only the `gen_ai.*` attributes present in `tags`, for values an integration resolves
|
|
66
|
+
* after the span started. Absent fields keep whatever the span already carries.
|
|
67
|
+
*
|
|
68
|
+
* @param {import('../opentracing/span')} span
|
|
69
|
+
* @param {GenAiApmTags} tags
|
|
70
|
+
*/
|
|
71
|
+
function updateGenAiApmTags (span, { spanKind, modelName, modelProvider, mlApp, sessionId, metrics }) {
|
|
72
|
+
const spanContext = span.context()
|
|
73
|
+
|
|
74
|
+
// written ahead of the attributes it covers: a throw partway through would otherwise leave
|
|
75
|
+
// `gen_ai.*` tags behind with nothing marking them as tracer-emitted
|
|
76
|
+
if (spanKind || modelName || modelProvider || mlApp || sessionId) {
|
|
77
|
+
markArtificialGenAiTags(spanContext)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (spanKind) spanContext.setTag(GEN_AI_OPERATION_NAME, spanKind)
|
|
81
|
+
if (modelName) spanContext.setTag(GEN_AI_REQUEST_MODEL, modelName)
|
|
82
|
+
if (modelProvider) spanContext.setTag(GEN_AI_PROVIDER_NAME, modelProvider.toLowerCase())
|
|
83
|
+
if (mlApp) spanContext.setTag(GEN_AI_APPLICATION_NAME, mlApp)
|
|
84
|
+
if (sessionId) spanContext.setTag(GEN_AI_CONVERSATION_ID, sessionId)
|
|
85
|
+
if (metrics) setGenAiApmUsageMetrics(span, spanKind, metrics)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {import('../opentracing/span_context')} spanContext
|
|
90
|
+
*/
|
|
91
|
+
function markArtificialGenAiTags (spanContext) {
|
|
92
|
+
spanContext.setTag(ARTIFICIAL_GEN_AI_TAGS, 'true')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Writes the `gen_ai.usage.*` metrics onto the APM span. Accepts both the LLMObs metric keys and
|
|
97
|
+
* the camelCase spellings integrations extract before the tagger normalizes them.
|
|
98
|
+
*
|
|
99
|
+
* @param {import('../opentracing/span')} span
|
|
100
|
+
* @param {string | undefined} spanKind LLMObs span kind
|
|
101
|
+
* @param {Record<string, unknown>} metrics
|
|
102
|
+
*/
|
|
103
|
+
function setGenAiApmUsageMetrics (span, spanKind, metrics) {
|
|
104
|
+
// Other kinds carry unrelated metrics that would be misleading under a `gen_ai.usage.*` key.
|
|
105
|
+
if (!MODEL_BACKED_SPAN_KINDS.has(spanKind)) return
|
|
106
|
+
|
|
107
|
+
const spanContext = span.context()
|
|
108
|
+
|
|
109
|
+
// ahead of the metrics, for the same reason `updateGenAiApmTags` marks before its scalars
|
|
110
|
+
markArtificialGenAiTags(spanContext)
|
|
111
|
+
|
|
112
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
113
|
+
if (typeof value !== 'number') continue
|
|
114
|
+
|
|
115
|
+
const genAiKey = GEN_AI_USAGE_METRIC_KEYS[METRIC_KEY_ALIASES[key] ?? key]
|
|
116
|
+
if (!genAiKey) continue
|
|
117
|
+
|
|
118
|
+
spanContext.setTag(genAiKey, value)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
MODEL_BACKED_SPAN_KINDS,
|
|
124
|
+
setGenAiApmTags,
|
|
125
|
+
setGenAiApmUsageMetrics,
|
|
126
|
+
updateGenAiApmTags,
|
|
127
|
+
}
|
|
@@ -89,6 +89,24 @@ class DdTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
89
89
|
return { kind, name: getLlmObsSpanName(operation, ctx.attributes['ai.telemetry.functionId']) }
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* @override
|
|
94
|
+
*/
|
|
95
|
+
getGenAiApmEndTags (ctx, spanKind) {
|
|
96
|
+
if (spanKind !== 'llm' && spanKind !== 'embedding') return
|
|
97
|
+
|
|
98
|
+
const tags = /** @type {Record<string, string>} */ (getSpanTags(ctx))
|
|
99
|
+
const embeddingUsage = tags['ai.usage.tokens']
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
modelName: tags['ai.model.id'],
|
|
103
|
+
modelProvider: getModelProvider(tags),
|
|
104
|
+
metrics: spanKind === 'embedding'
|
|
105
|
+
? { inputTokens: embeddingUsage, totalTokens: embeddingUsage }
|
|
106
|
+
: getUsage(tags),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
/**
|
|
93
111
|
* @override
|
|
94
112
|
*/
|
|
@@ -140,6 +140,20 @@ function formatLanguageModelOutputMessages (content) {
|
|
|
140
140
|
return outputMessages
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* @param {object} [usage] AI SDK usage, from the result or the stream's `finish` chunk
|
|
145
|
+
* @returns {Record<string, number | undefined>}
|
|
146
|
+
*/
|
|
147
|
+
function extractUsageMetrics (usage) {
|
|
148
|
+
return {
|
|
149
|
+
inputTokens: usage?.inputTokens?.total,
|
|
150
|
+
cacheWriteTokens: usage?.inputTokens?.cacheWrite ?? 0,
|
|
151
|
+
cacheReadTokens: usage?.inputTokens?.cacheRead ?? 0,
|
|
152
|
+
outputTokens: usage?.outputTokens?.total,
|
|
153
|
+
reasoningOutputTokens: usage?.outputTokens?.reasoning ?? 0,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
144
158
|
static id = 'ai_llmobs_vercel_telemetry'
|
|
145
159
|
static integration = 'ai'
|
|
@@ -152,6 +166,13 @@ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
152
166
|
super(...arguments)
|
|
153
167
|
|
|
154
168
|
this.addSub('dd-trace:vercel-ai:chunk', ({ ctx, chunk, done }) => {
|
|
169
|
+
if (!this._llmobsEnabledFor(ctx)) {
|
|
170
|
+
// only the token usage is needed, for the `gen_ai.usage.*` metrics; the message bodies and
|
|
171
|
+
// `ctx.result` are left to the LLMObs path
|
|
172
|
+
if (chunk?.type === 'finish') ctx.streamedUsage = chunk.usage
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
ctx.chunks ??= []
|
|
156
177
|
const chunks = ctx.chunks
|
|
157
178
|
if (chunk) chunks.push(chunk)
|
|
@@ -202,6 +223,17 @@ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
202
223
|
super.asyncEnd(ctx)
|
|
203
224
|
}
|
|
204
225
|
|
|
226
|
+
/**
|
|
227
|
+
* @override
|
|
228
|
+
*/
|
|
229
|
+
getGenAiApmEndTags (ctx, spanKind) {
|
|
230
|
+
const usage = ctx.result?.usage ?? ctx.streamedUsage
|
|
231
|
+
if (!usage) return {}
|
|
232
|
+
|
|
233
|
+
// `embed` reports a single token count, the generation operations a structured breakdown
|
|
234
|
+
return { metrics: spanKind === 'embedding' ? { inputTokens: usage.tokens } : extractUsageMetrics(usage) }
|
|
235
|
+
}
|
|
236
|
+
|
|
205
237
|
/**
|
|
206
238
|
* @override
|
|
207
239
|
*/
|
|
@@ -367,14 +399,7 @@ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
367
399
|
if (!result) return
|
|
368
400
|
|
|
369
401
|
// metrics
|
|
370
|
-
|
|
371
|
-
this._tagger.tagMetrics(span, {
|
|
372
|
-
inputTokens: usage?.inputTokens?.total,
|
|
373
|
-
cacheWriteTokens: usage?.inputTokens?.cacheWrite ?? 0,
|
|
374
|
-
cacheReadTokens: usage?.inputTokens?.cacheRead ?? 0,
|
|
375
|
-
outputTokens: usage?.outputTokens?.total,
|
|
376
|
-
reasoningOutputTokens: usage?.outputTokens?.reasoning ?? 0,
|
|
377
|
-
})
|
|
402
|
+
this._tagger.tagMetrics(span, extractUsageMetrics(result.usage))
|
|
378
403
|
}
|
|
379
404
|
|
|
380
405
|
setToolTags (span, ctx) {
|
|
@@ -22,6 +22,13 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
22
22
|
super(...arguments)
|
|
23
23
|
|
|
24
24
|
this.addSub('apm:anthropic:request:chunk', ({ ctx, chunk, done }) => {
|
|
25
|
+
if (!this._llmobsEnabledFor(ctx)) {
|
|
26
|
+
// only the token usage is needed, for the `gen_ai.usage.*` metrics; the message bodies
|
|
27
|
+
// and the aggregated response are left to the LLMObs path
|
|
28
|
+
if (chunk) ctx.streamedUsage = mergeChunkUsage(ctx.streamedUsage, chunk)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
ctx.chunks ??= []
|
|
26
33
|
const chunks = ctx.chunks
|
|
27
34
|
if (chunk) chunks.push(chunk)
|
|
@@ -36,9 +43,8 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
36
43
|
const { message } = chunk
|
|
37
44
|
if (!message) continue
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (usage) response.usage = usage
|
|
46
|
+
if (message.role) response.role = message.role
|
|
47
|
+
response.usage = mergeChunkUsage(response.usage, chunk)
|
|
42
48
|
break
|
|
43
49
|
}
|
|
44
50
|
case 'content_block_start': {
|
|
@@ -86,22 +92,10 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
86
92
|
break
|
|
87
93
|
}
|
|
88
94
|
case 'message_delta': {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
const finishReason = delta?.stop_reason
|
|
95
|
+
const finishReason = chunk.delta?.stop_reason
|
|
92
96
|
if (finishReason) response.finish_reason = finishReason
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
if (usage) {
|
|
96
|
-
const responseUsage = (response.usage ??= { input_tokens: 0, output_tokens: 0 })
|
|
97
|
-
responseUsage.output_tokens = usage.output_tokens
|
|
98
|
-
|
|
99
|
-
const cacheCreationTokens = usage.cache_creation_input_tokens
|
|
100
|
-
const cacheReadTokens = usage.cache_read_input_tokens
|
|
101
|
-
if (cacheCreationTokens) responseUsage.cache_creation_input_tokens = cacheCreationTokens
|
|
102
|
-
if (cacheReadTokens) responseUsage.cache_read_input_tokens = cacheReadTokens
|
|
103
|
-
}
|
|
104
|
-
|
|
98
|
+
response.usage = mergeChunkUsage(response.usage, chunk)
|
|
105
99
|
break
|
|
106
100
|
}
|
|
107
101
|
case 'error': {
|
|
@@ -140,6 +134,13 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
140
134
|
return UNKNOWN_MODEL_PROVIDER
|
|
141
135
|
}
|
|
142
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @override
|
|
139
|
+
*/
|
|
140
|
+
getGenAiApmEndTags (ctx) {
|
|
141
|
+
return { metrics: extractUsage(ctx.result ?? { usage: ctx.streamedUsage }) }
|
|
142
|
+
}
|
|
143
|
+
|
|
143
144
|
setLLMObsTags (ctx) {
|
|
144
145
|
const span = ctx.currentStore?.span
|
|
145
146
|
if (!span) return
|
|
@@ -213,38 +214,73 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
#tagAnthropicUsage (span, result) {
|
|
216
|
-
|
|
217
|
+
const metrics = extractUsage(result)
|
|
218
|
+
if (metrics) this._tagger.tagMetrics(span, metrics)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
217
221
|
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Merges the token usage a streamed chunk carries into the usage accumulated so far.
|
|
224
|
+
*
|
|
225
|
+
* @param {Record<string, number> | undefined} usage
|
|
226
|
+
* @param {object} chunk
|
|
227
|
+
* @returns {Record<string, number> | undefined}
|
|
228
|
+
*/
|
|
229
|
+
function mergeChunkUsage (usage, chunk) {
|
|
230
|
+
if (chunk.type === 'message_start') {
|
|
231
|
+
// copied: the `message_delta` counts are merged into this object, and the chunk is handed back
|
|
232
|
+
// to the application
|
|
233
|
+
const startUsage = chunk.message?.usage
|
|
234
|
+
return startUsage ? { ...startUsage } : usage
|
|
235
|
+
}
|
|
220
236
|
|
|
221
|
-
|
|
222
|
-
const outputTokens = usage.output_tokens
|
|
223
|
-
const cacheWriteTokens = usage.cache_creation_input_tokens
|
|
224
|
-
const cacheReadTokens = usage.cache_read_input_tokens
|
|
237
|
+
if (chunk.type !== 'message_delta' || !chunk.usage) return usage
|
|
225
238
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
const mergedUsage = usage ?? { input_tokens: 0, output_tokens: 0 }
|
|
240
|
+
mergedUsage.output_tokens = chunk.usage.output_tokens
|
|
241
|
+
|
|
242
|
+
const cacheCreationTokens = chunk.usage.cache_creation_input_tokens
|
|
243
|
+
const cacheReadTokens = chunk.usage.cache_read_input_tokens
|
|
244
|
+
if (cacheCreationTokens) mergedUsage.cache_creation_input_tokens = cacheCreationTokens
|
|
245
|
+
if (cacheReadTokens) mergedUsage.cache_read_input_tokens = cacheReadTokens
|
|
229
246
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (totalTokens) metrics.totalTokens = totalTokens
|
|
247
|
+
return mergedUsage
|
|
248
|
+
}
|
|
233
249
|
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
/**
|
|
251
|
+
* @param {object} [result]
|
|
252
|
+
* @returns {Record<string, number> | undefined}
|
|
253
|
+
*/
|
|
254
|
+
function extractUsage (result) {
|
|
255
|
+
const usage = result?.usage
|
|
256
|
+
if (!usage) return
|
|
257
|
+
|
|
258
|
+
const inputTokens = usage.input_tokens
|
|
259
|
+
const outputTokens = usage.output_tokens
|
|
260
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens
|
|
261
|
+
const cacheReadTokens = usage.cache_read_input_tokens
|
|
262
|
+
|
|
263
|
+
const metrics = {
|
|
264
|
+
inputTokens: (inputTokens ?? 0) + (cacheWriteTokens ?? 0) + (cacheReadTokens ?? 0),
|
|
265
|
+
}
|
|
236
266
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
metrics.cacheWrite1hTokens = cacheCreation.ephemeral_1h_input_tokens ?? 0
|
|
241
|
-
} else if (cacheWriteTokens != null) {
|
|
242
|
-
metrics.cacheWrite5mTokens = cacheWriteTokens
|
|
243
|
-
metrics.cacheWrite1hTokens = 0
|
|
244
|
-
}
|
|
267
|
+
if (outputTokens) metrics.outputTokens = outputTokens
|
|
268
|
+
const totalTokens = metrics.inputTokens + (outputTokens ?? 0)
|
|
269
|
+
if (totalTokens) metrics.totalTokens = totalTokens
|
|
245
270
|
|
|
246
|
-
|
|
271
|
+
if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
|
|
272
|
+
if (cacheReadTokens != null) metrics.cacheReadTokens = cacheReadTokens
|
|
273
|
+
|
|
274
|
+
const cacheCreation = usage.cache_creation
|
|
275
|
+
if (cacheCreation) {
|
|
276
|
+
metrics.cacheWrite5mTokens = cacheCreation.ephemeral_5m_input_tokens ?? 0
|
|
277
|
+
metrics.cacheWrite1hTokens = cacheCreation.ephemeral_1h_input_tokens ?? 0
|
|
278
|
+
} else if (cacheWriteTokens != null) {
|
|
279
|
+
metrics.cacheWrite5mTokens = cacheWriteTokens
|
|
280
|
+
metrics.cacheWrite1hTokens = 0
|
|
247
281
|
}
|
|
282
|
+
|
|
283
|
+
return metrics
|
|
248
284
|
}
|
|
249
285
|
|
|
250
286
|
module.exports = AnthropicLLMObsPlugin
|
|
@@ -1,32 +1,89 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const log = require('../../log')
|
|
4
|
+
const { PROPAGATED_SESSION_ID_KEY } = require('../constants/tags')
|
|
5
|
+
const { MODEL_BACKED_SPAN_KINDS, setGenAiApmTags, updateGenAiApmTags } = require('../gen-ai-tags')
|
|
4
6
|
const { storage: llmobsStorage } = require('../storage')
|
|
5
7
|
const telemetry = require('../telemetry')
|
|
6
8
|
|
|
7
9
|
const TracingPlugin = require('../../plugins/tracing')
|
|
8
10
|
const LLMObsTagger = require('../tagger')
|
|
9
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} LLMObsSpanRegisterOptions
|
|
14
|
+
* @property {string} kind LLMObs span kind
|
|
15
|
+
* @property {string} [name]
|
|
16
|
+
* @property {string} [modelName]
|
|
17
|
+
* @property {string} [modelProvider]
|
|
18
|
+
* @property {string} [mlApp]
|
|
19
|
+
* @property {string} [sessionId]
|
|
20
|
+
*/
|
|
21
|
+
|
|
10
22
|
class LLMObsPlugin extends TracingPlugin {
|
|
23
|
+
/**
|
|
24
|
+
* Whether this integration emits the `gen_ai.*` APM attributes while LLM Observability is off.
|
|
25
|
+
* An integration sets this to `false` when staying subscribed would cost more than the tags are
|
|
26
|
+
* worth, and then behaves as it did before those attributes existed: disabled outright.
|
|
27
|
+
*/
|
|
28
|
+
static emitsGenAiApmTags = true
|
|
29
|
+
|
|
11
30
|
constructor (...args) {
|
|
12
31
|
super(...args)
|
|
13
32
|
|
|
14
33
|
this._tagger = new LLMObsTagger(this._tracerConfig, true)
|
|
15
34
|
}
|
|
16
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Whether the LLMObs layer is active. When it is not, the plugin stays subscribed but only
|
|
38
|
+
* emits the `gen_ai.*` APM attributes.
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
41
|
+
get _llmobsEnabled () {
|
|
42
|
+
return this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The mode one operation runs in, latched on first read. `llmobs.enable()` and `llmobs.disable()`
|
|
47
|
+
* flip the flag while operations are in flight, and an operation that switches track halfway
|
|
48
|
+
* reports neither: the tagger rejects a span the start never registered, and the reduced path
|
|
49
|
+
* has no start tags for its end hook to update. Every hook for a given operation reads it here
|
|
50
|
+
* so they all take the branch its start took.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} ctx
|
|
53
|
+
*/
|
|
54
|
+
_llmobsEnabledFor (ctx) {
|
|
55
|
+
ctx.llmobsEnabled ??= this._llmobsEnabled
|
|
56
|
+
return ctx.llmobsEnabled
|
|
57
|
+
}
|
|
58
|
+
|
|
17
59
|
setLLMObsTags (ctx) {
|
|
18
60
|
throw new Error('setLLMObsTags must be implemented by the subclass')
|
|
19
61
|
}
|
|
20
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The `gen_ai.*` values an integration can only resolve once the operation finished, such as
|
|
65
|
+
* token usage or a session id the response carries. Only used while LLMObs is disabled; the
|
|
66
|
+
* LLMObs layer reads them off the span event instead. Fields left out keep their start value.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} ctx
|
|
69
|
+
* @param {string} spanKind LLMObs span kind resolved at span start
|
|
70
|
+
* @returns {import('../gen-ai-tags').GenAiApmTags | void}
|
|
71
|
+
*/
|
|
72
|
+
getGenAiApmEndTags (ctx, spanKind) {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} ctx
|
|
76
|
+
* @returns {LLMObsSpanRegisterOptions | undefined}
|
|
77
|
+
*/
|
|
21
78
|
getLLMObsSpanRegisterOptions (ctx) {
|
|
22
79
|
throw new Error('getLLMObsSPanRegisterOptions must be implemented by the subclass')
|
|
23
80
|
}
|
|
24
81
|
|
|
25
82
|
start (ctx) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
83
|
+
if (!this._llmobsEnabledFor(ctx)) {
|
|
84
|
+
this.#setGenAiApmTagsFromRegisterOptions(ctx)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
30
87
|
|
|
31
88
|
const parentStore = llmobsStorage.getStore()
|
|
32
89
|
const apmStore = ctx.currentStore
|
|
@@ -52,8 +109,7 @@ class LLMObsPlugin extends TracingPlugin {
|
|
|
52
109
|
}
|
|
53
110
|
|
|
54
111
|
end (ctx) {
|
|
55
|
-
|
|
56
|
-
if (!enabled) return
|
|
112
|
+
if (!this._llmobsEnabledFor(ctx)) return
|
|
57
113
|
|
|
58
114
|
// only attempt to restore the context if the current span was an LLMObs span
|
|
59
115
|
const apmStore = ctx.currentStore
|
|
@@ -65,10 +121,10 @@ class LLMObsPlugin extends TracingPlugin {
|
|
|
65
121
|
}
|
|
66
122
|
|
|
67
123
|
asyncEnd (ctx) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
124
|
+
if (!this._llmobsEnabledFor(ctx)) {
|
|
125
|
+
this.#setGenAiApmEndTags(ctx)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
72
128
|
|
|
73
129
|
const apmStore = ctx.currentStore
|
|
74
130
|
const span = apmStore?.span
|
|
@@ -83,12 +139,95 @@ class LLMObsPlugin extends TracingPlugin {
|
|
|
83
139
|
this.setLLMObsTags(ctx)
|
|
84
140
|
}
|
|
85
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Resolves the LLMObs annotations the `gen_ai.*` APM attributes need from the span register
|
|
144
|
+
* options, which every integration already builds for the LLMObs layer.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} ctx
|
|
147
|
+
*/
|
|
148
|
+
#setGenAiApmTagsFromRegisterOptions (ctx) {
|
|
149
|
+
const span = ctx.currentStore?.span
|
|
150
|
+
if (!span) return
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const registerOptions = this.getLLMObsSpanRegisterOptions(ctx)
|
|
154
|
+
if (!registerOptions?.kind) return
|
|
155
|
+
|
|
156
|
+
// `asyncEnd` needs these back: the kind to gate the usage metrics, and the model in case an
|
|
157
|
+
// integration promotes the kind to one that must always report a model
|
|
158
|
+
ctx.genAiApmStartTags = {
|
|
159
|
+
spanKind: registerOptions.kind,
|
|
160
|
+
modelName: registerOptions.modelName,
|
|
161
|
+
modelProvider: registerOptions.modelProvider,
|
|
162
|
+
mlApp: registerOptions.mlApp,
|
|
163
|
+
sessionId: registerOptions.sessionId,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this._setGenAiApmTags(span, ctx.genAiApmStartTags)
|
|
167
|
+
} catch (e) {
|
|
168
|
+
log.debug('Failed to set gen_ai APM tags for %s:', this.constructor.name, e.message)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {object} ctx
|
|
174
|
+
*/
|
|
175
|
+
#setGenAiApmEndTags (ctx) {
|
|
176
|
+
const span = ctx.currentStore?.span
|
|
177
|
+
const startTags = ctx.genAiApmStartTags
|
|
178
|
+
if (!span || !startTags) return
|
|
179
|
+
|
|
180
|
+
const { spanKind } = startTags
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const endTags = this.getGenAiApmEndTags(ctx, spanKind)
|
|
184
|
+
if (!endTags) return
|
|
185
|
+
|
|
186
|
+
// An integration may correct the kind, the way the tagger's `changeKind` does. A correction
|
|
187
|
+
// into a model-backed kind has to go back through `setGenAiApmTags`, which applies the model
|
|
188
|
+
// and provider defaults a model-backed span always reports; an update alone would leave the
|
|
189
|
+
// span claiming a kind the enabled path could never emit without a model.
|
|
190
|
+
const promotedToModelBacked = endTags.spanKind &&
|
|
191
|
+
endTags.spanKind !== spanKind &&
|
|
192
|
+
MODEL_BACKED_SPAN_KINDS.has(endTags.spanKind)
|
|
193
|
+
|
|
194
|
+
if (promotedToModelBacked) {
|
|
195
|
+
this._setGenAiApmTags(span, { ...startTags, ...endTags })
|
|
196
|
+
} else {
|
|
197
|
+
updateGenAiApmTags(span, { spanKind, ...endTags })
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
log.debug('Failed to set gen_ai APM end tags for %s:', this.constructor.name, e.message)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Writes the `gen_ai.*` APM attributes.
|
|
206
|
+
*
|
|
207
|
+
* No `gen_ai.application.name`: ml_app is an LLM Observability concept, and with LLMObs off the
|
|
208
|
+
* only value left to report is the service name the span already carries. dd-trace-py's reduced
|
|
209
|
+
* path leaves it out for the same reason.
|
|
210
|
+
*
|
|
211
|
+
* @param {import('../../opentracing/span')} span
|
|
212
|
+
* @param {import('../gen-ai-tags').GenAiApmTags} tags
|
|
213
|
+
*/
|
|
214
|
+
_setGenAiApmTags (span, tags) {
|
|
215
|
+
// the in-process session default is written by the tagger, which never runs on this path, so
|
|
216
|
+
// an inherited session can only have come from an upstream service
|
|
217
|
+
const propagatedSessionId = span.context()._trace.tags[PROPAGATED_SESSION_ID_KEY]
|
|
218
|
+
|
|
219
|
+
setGenAiApmTags(span, { ...tags, sessionId: tags.sessionId || propagatedSessionId })
|
|
220
|
+
}
|
|
221
|
+
|
|
86
222
|
configure (config) {
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
223
|
+
// an integration opt-out via `tracer.use(<name>, { llmobs: false })` disables the LLMObs layer
|
|
224
|
+
// entirely. When only LLMObs itself is disabled we stay subscribed: the handlers then emit the
|
|
225
|
+
// `gen_ai.*` APM attributes and skip the LLMObs payload, unless the integration opted out of
|
|
226
|
+
// those too.
|
|
227
|
+
const disabled = config?.llmobs === false ||
|
|
228
|
+
(!this._llmobsEnabled && !this.constructor.emitsGenAiApmTags)
|
|
229
|
+
|
|
230
|
+
if (disabled) {
|
|
92
231
|
config = typeof config === 'boolean' ? false : { ...config, enabled: false } // override to false
|
|
93
232
|
}
|
|
94
233
|
super.configure(config)
|