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.
Files changed (110) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/vitest-no-worker-init-setup.mjs +22 -12
  3. package/index.d.ts +55 -11
  4. package/package.json +6 -6
  5. package/packages/datadog-instrumentations/src/anthropic.js +111 -9
  6. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
  7. package/packages/datadog-instrumentations/src/cucumber.js +50 -9
  8. package/packages/datadog-instrumentations/src/cypress-config.js +27 -15
  9. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +6 -0
  10. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +2 -1
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -6
  12. package/packages/datadog-instrumentations/src/helpers/register.js +20 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +41 -4
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +15 -13
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +51 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +13 -0
  17. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +28 -3
  18. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +6 -0
  19. package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
  20. package/packages/datadog-instrumentations/src/jest.js +30 -13
  21. package/packages/datadog-instrumentations/src/mocha/main.js +7 -5
  22. package/packages/datadog-instrumentations/src/mocha/utils.js +8 -8
  23. package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -4
  24. package/packages/datadog-instrumentations/src/playwright.js +73 -6
  25. package/packages/datadog-instrumentations/src/undici.js +7 -3
  26. package/packages/datadog-instrumentations/src/vitest-main.js +95 -21
  27. package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
  28. package/packages/datadog-instrumentations/src/webdriverio.js +28 -12
  29. package/packages/datadog-instrumentations/src/ws.js +41 -1
  30. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
  31. package/packages/datadog-plugin-cucumber/src/index.js +22 -8
  32. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -14
  33. package/packages/datadog-plugin-cypress/src/index.js +3 -1
  34. package/packages/datadog-plugin-fetch/src/index.js +3 -0
  35. package/packages/datadog-plugin-jest/src/index.js +3 -4
  36. package/packages/datadog-plugin-mocha/src/index.js +3 -4
  37. package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
  38. package/packages/datadog-plugin-playwright/src/index.js +11 -6
  39. package/packages/datadog-plugin-undici/src/index.js +5 -0
  40. package/packages/datadog-plugin-vitest/src/index.js +3 -4
  41. package/packages/datadog-plugin-ws/src/producer.js +1 -1
  42. package/packages/datadog-shimmer/src/shimmer.js +14 -10
  43. package/packages/datadog-turbopack/index.js +4 -1
  44. package/packages/datadog-turbopack/src/loader.js +6 -2
  45. package/packages/dd-trace/src/agent/info.js +5 -4
  46. package/packages/dd-trace/src/aiguard/client.js +38 -19
  47. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
  48. package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
  49. package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
  50. package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
  51. package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
  52. package/packages/dd-trace/src/config/generated-config-types.d.ts +19 -19
  53. package/packages/dd-trace/src/config/index.js +7 -6
  54. package/packages/dd-trace/src/config/supported-configurations.json +16 -5
  55. package/packages/dd-trace/src/constants.js +2 -0
  56. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
  57. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
  58. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +5 -3
  59. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  60. package/packages/dd-trace/src/debugger/index.js +1 -1
  61. package/packages/dd-trace/src/evp_proxy/constants.js +2 -0
  62. package/packages/dd-trace/src/evp_proxy/direct.js +17 -13
  63. package/packages/dd-trace/src/evp_proxy/discovery.js +14 -10
  64. package/packages/dd-trace/src/evp_proxy/path.js +21 -7
  65. package/packages/dd-trace/src/exporters/common/url.js +33 -2
  66. package/packages/dd-trace/src/llmobs/constants/tags.js +31 -8
  67. package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
  68. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
  69. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
  70. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
  71. package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
  72. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
  73. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
  74. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
  75. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
  76. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
  77. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
  78. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
  79. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +13 -0
  80. package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
  81. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
  82. package/packages/dd-trace/src/llmobs/prompts/manager.js +10 -2
  83. package/packages/dd-trace/src/llmobs/prompts/prompt.js +79 -12
  84. package/packages/dd-trace/src/llmobs/span_processor.js +10 -65
  85. package/packages/dd-trace/src/llmobs/tagger.js +11 -40
  86. package/packages/dd-trace/src/openfeature/configuration_source.js +4 -1
  87. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -3
  88. package/packages/dd-trace/src/openfeature/index.js +6 -2
  89. package/packages/dd-trace/src/openfeature/writers/base.js +90 -17
  90. package/packages/dd-trace/src/openfeature/writers/exposures.js +4 -0
  91. package/packages/dd-trace/src/openfeature/writers/util.js +104 -33
  92. package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
  93. package/packages/dd-trace/src/opentracing/tracer.js +3 -3
  94. package/packages/dd-trace/src/plugin_manager.js +27 -8
  95. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -1
  96. package/packages/dd-trace/src/plugins/util/test.js +13 -4
  97. package/packages/dd-trace/src/profiler.js +51 -5
  98. package/packages/dd-trace/src/profiling/profiler.js +24 -20
  99. package/packages/dd-trace/src/proxy.js +2 -2
  100. package/packages/dd-trace/src/ritm.js +12 -9
  101. package/packages/dd-trace/src/span_processor.js +6 -1
  102. package/packages/dd-trace/src/telemetry/send-data.js +2 -1
  103. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  104. package/packages/dd-trace/src/tracer.js +5 -4
  105. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  106. package/packages/datadog-instrumentations/src/azure-cosmos.js +0 -7
  107. package/packages/datadog-instrumentations/src/bullmq.js +0 -11
  108. package/packages/datadog-instrumentations/src/langchain.js +0 -7
  109. package/packages/datadog-instrumentations/src/langgraph.js +0 -7
  110. 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
- const { usage } = result
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
- const { role, usage } = message
40
- if (role) response.role = role
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 { delta } = chunk
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
- const { usage } = chunk
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
- if (!result) return
217
+ const metrics = extractUsage(result)
218
+ if (metrics) this._tagger.tagMetrics(span, metrics)
219
+ }
220
+ }
217
221
 
218
- const { usage } = result
219
- if (!usage) return
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
- const inputTokens = usage.input_tokens
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
- const metrics = {
227
- inputTokens: (inputTokens ?? 0) + (cacheWriteTokens ?? 0) + (cacheReadTokens ?? 0),
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
- if (outputTokens) metrics.outputTokens = outputTokens
231
- const totalTokens = metrics.inputTokens + (outputTokens ?? 0)
232
- if (totalTokens) metrics.totalTokens = totalTokens
247
+ return mergedUsage
248
+ }
233
249
 
234
- if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
235
- if (cacheReadTokens != null) metrics.cacheReadTokens = cacheReadTokens
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
- const cacheCreation = usage.cache_creation
238
- if (cacheCreation) {
239
- metrics.cacheWrite5mTokens = cacheCreation.ephemeral_5m_input_tokens ?? 0
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
- this._tagger.tagMetrics(span, metrics)
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
- // even though llmobs span events won't be enqueued if llmobs is disabled
27
- // we should avoid doing any computations here (these listeners aren't disabled)
28
- const enabled = this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
29
- if (!enabled) return
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
- const enabled = this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
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
- // even though llmobs span events won't be enqueued if llmobs is disabled
69
- // we should avoid doing any computations here (these listeners aren't disabled)
70
- const enabled = this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
71
- if (!enabled) return
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
- // we do not want to enable any LLMObs plugins if it is disabled on the tracer, or if the
88
- // integration opted out via `tracer.use(<name>, { llmobs: false })`. Opting out only disables
89
- // the LLMObs layer: the integration keeps emitting APM spans and propagating trace context.
90
- const llmobsEnabled = this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
91
- if (llmobsEnabled === false || config?.llmobs === false) {
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)