dd-trace 5.42.0 → 5.44.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 (60) hide show
  1. package/package.json +5 -5
  2. package/packages/datadog-instrumentations/src/cucumber.js +61 -23
  3. package/packages/datadog-instrumentations/src/dd-trace-api.js +7 -0
  4. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  5. package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
  6. package/packages/datadog-instrumentations/src/jest.js +134 -48
  7. package/packages/datadog-instrumentations/src/mocha/main.js +20 -4
  8. package/packages/datadog-instrumentations/src/mocha/utils.js +89 -30
  9. package/packages/datadog-instrumentations/src/mocha/worker.js +3 -1
  10. package/packages/datadog-instrumentations/src/playwright.js +97 -17
  11. package/packages/datadog-instrumentations/src/router.js +1 -0
  12. package/packages/datadog-instrumentations/src/tedious.js +13 -10
  13. package/packages/datadog-instrumentations/src/vitest.js +96 -21
  14. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  15. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +69 -20
  16. package/packages/datadog-plugin-cypress/src/support.js +39 -10
  17. package/packages/datadog-plugin-google-cloud-vertexai/src/index.js +8 -186
  18. package/packages/datadog-plugin-google-cloud-vertexai/src/tracing.js +186 -0
  19. package/packages/datadog-plugin-google-cloud-vertexai/src/utils.js +19 -0
  20. package/packages/datadog-plugin-jest/src/index.js +38 -5
  21. package/packages/datadog-plugin-mocha/src/index.js +28 -5
  22. package/packages/datadog-plugin-playwright/src/index.js +22 -2
  23. package/packages/datadog-plugin-tedious/src/index.js +14 -9
  24. package/packages/datadog-plugin-vitest/src/index.js +46 -14
  25. package/packages/dd-trace/src/appsec/blocking.js +2 -0
  26. package/packages/dd-trace/src/appsec/graphql.js +3 -1
  27. package/packages/dd-trace/src/appsec/reporter.js +13 -8
  28. package/packages/dd-trace/src/appsec/telemetry/common.js +6 -2
  29. package/packages/dd-trace/src/appsec/telemetry/index.js +20 -4
  30. package/packages/dd-trace/src/appsec/telemetry/waf.js +74 -8
  31. package/packages/dd-trace/src/appsec/waf/waf_manager.js +16 -7
  32. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -2
  33. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +3 -1
  34. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +4 -2
  35. package/packages/dd-trace/src/config.js +4 -0
  36. package/packages/dd-trace/src/dogstatsd.js +3 -3
  37. package/packages/dd-trace/src/encode/0.4.js +108 -12
  38. package/packages/dd-trace/src/encode/0.5.js +7 -1
  39. package/packages/dd-trace/src/exporters/agent/index.js +2 -1
  40. package/packages/dd-trace/src/exporters/agent/writer.js +3 -2
  41. package/packages/dd-trace/src/format.js +34 -32
  42. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  43. package/packages/dd-trace/src/llmobs/noop.js +3 -3
  44. package/packages/dd-trace/src/llmobs/plugins/base.js +1 -1
  45. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +2 -1
  46. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +196 -0
  47. package/packages/dd-trace/src/llmobs/sdk.js +5 -3
  48. package/packages/dd-trace/src/llmobs/span_processor.js +6 -0
  49. package/packages/dd-trace/src/llmobs/tagger.js +8 -2
  50. package/packages/dd-trace/src/llmobs/telemetry.js +82 -1
  51. package/packages/dd-trace/src/llmobs/writers/spans/base.js +10 -1
  52. package/packages/dd-trace/src/log/index.js +1 -13
  53. package/packages/dd-trace/src/log/utils.js +16 -0
  54. package/packages/dd-trace/src/plugin_manager.js +0 -3
  55. package/packages/dd-trace/src/plugins/ci_plugin.js +16 -26
  56. package/packages/dd-trace/src/plugins/database.js +4 -4
  57. package/packages/dd-trace/src/plugins/plugin.js +2 -0
  58. package/packages/dd-trace/src/plugins/util/test.js +62 -1
  59. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +20 -20
  60. package/packages/dd-trace/src/telemetry/send-data.js +5 -1
@@ -5,11 +5,22 @@ const { Chunk, MsgpackEncoder } = require('../msgpack')
5
5
  const log = require('../log')
6
6
  const { isTrue } = require('../util')
7
7
  const coalesce = require('koalas')
8
+ const { memoize } = require('../log/utils')
8
9
 
9
10
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
10
11
 
11
- function formatSpan (span) {
12
- return normalizeSpan(truncateSpan(span, false))
12
+ function formatSpan (span, config) {
13
+ span = normalizeSpan(truncateSpan(span, false))
14
+ if (span.span_events) {
15
+ // ensure span events are encoded as tags if agent doesn't support native top level span events
16
+ if (!config?.trace?.nativeSpanEvents) {
17
+ span.meta.events = JSON.stringify(span.span_events)
18
+ delete span.span_events
19
+ } else {
20
+ formatSpanEvents(span)
21
+ }
22
+ }
23
+ return span
13
24
  }
14
25
 
15
26
  class AgentEncoder {
@@ -24,6 +35,7 @@ class AgentEncoder {
24
35
  process.env.DD_TRACE_ENCODING_DEBUG,
25
36
  false
26
37
  ))
38
+ this._config = this._writer?._config
27
39
  }
28
40
 
29
41
  count () {
@@ -74,16 +86,18 @@ class AgentEncoder {
74
86
  this._encodeArrayPrefix(bytes, trace)
75
87
 
76
88
  for (let span of trace) {
77
- span = formatSpan(span)
89
+ span = formatSpan(span, this._config)
78
90
  bytes.reserve(1)
79
91
 
80
- if (span.type && span.meta_struct) {
81
- bytes.buffer[bytes.length - 1] = 0x8d
82
- } else if (span.type || span.meta_struct) {
83
- bytes.buffer[bytes.length - 1] = 0x8c
84
- } else {
85
- bytes.buffer[bytes.length - 1] = 0x8b
86
- }
92
+ // this is the original size of the fixed map for span attributes that always exist
93
+ let mapSize = 11
94
+
95
+ // increment the payload map size depending on if some optional fields exist
96
+ if (span.type) mapSize += 1
97
+ if (span.meta_struct) mapSize += 1
98
+ if (span.span_events) mapSize += 1
99
+
100
+ bytes.buffer[bytes.length - 1] = 0x80 + mapSize
87
101
 
88
102
  if (span.type) {
89
103
  this._encodeString(bytes, 'type')
@@ -112,6 +126,10 @@ class AgentEncoder {
112
126
  this._encodeMap(bytes, span.meta)
113
127
  this._encodeString(bytes, 'metrics')
114
128
  this._encodeMap(bytes, span.metrics)
129
+ if (span.span_events) {
130
+ this._encodeString(bytes, 'span_events')
131
+ this._encodeObjectAsArray(bytes, span.span_events, new Set())
132
+ }
115
133
  if (span.meta_struct) {
116
134
  this._encodeString(bytes, 'meta_struct')
117
135
  this._encodeMetaStruct(bytes, span.meta_struct)
@@ -200,6 +218,9 @@ class AgentEncoder {
200
218
  case 'number':
201
219
  this._encodeFloat(bytes, value)
202
220
  break
221
+ case 'boolean':
222
+ this._encodeBool(bytes, value)
223
+ break
203
224
  default:
204
225
  // should not happen
205
226
  }
@@ -258,7 +279,7 @@ class AgentEncoder {
258
279
  this._encodeObjectAsArray(bytes, value, circularReferencesDetector)
259
280
  } else if (value !== null && typeof value === 'object') {
260
281
  this._encodeObjectAsMap(bytes, value, circularReferencesDetector)
261
- } else if (typeof value === 'string' || typeof value === 'number') {
282
+ } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
262
283
  this._encodeValue(bytes, value)
263
284
  }
264
285
  }
@@ -268,7 +289,7 @@ class AgentEncoder {
268
289
  const validKeys = keys.filter(key => {
269
290
  const v = value[key]
270
291
  return typeof v === 'string' ||
271
- typeof v === 'number' ||
292
+ typeof v === 'number' || typeof v === 'boolean' ||
272
293
  (v !== null && typeof v === 'object' && !circularReferencesDetector.has(v))
273
294
  })
274
295
 
@@ -319,4 +340,79 @@ class AgentEncoder {
319
340
  }
320
341
  }
321
342
 
343
+ const memoizedLogDebug = memoize((key, message) => {
344
+ log.debug(message)
345
+ // return something to store in memoize cache
346
+ return true
347
+ })
348
+
349
+ function formatSpanEvents (span) {
350
+ for (const spanEvent of span.span_events) {
351
+ if (spanEvent.attributes) {
352
+ for (const [key, value] of Object.entries(spanEvent.attributes)) {
353
+ const newValue = convertSpanEventAttributeValues(key, value)
354
+ if (newValue !== undefined) {
355
+ spanEvent.attributes[key] = newValue
356
+ } else {
357
+ delete spanEvent.attributes[key] // delete from attributes if undefined
358
+ }
359
+ }
360
+ if (Object.entries(spanEvent.attributes).length === 0) {
361
+ delete spanEvent.attributes
362
+ }
363
+ }
364
+ }
365
+ }
366
+
367
+ function convertSpanEventAttributeValues (key, value, depth = 0) {
368
+ if (typeof value === 'string') {
369
+ return {
370
+ type: 0,
371
+ string_value: value
372
+ }
373
+ } else if (typeof value === 'boolean') {
374
+ return {
375
+ type: 1,
376
+ bool_value: value
377
+ }
378
+ } else if (Number.isInteger(value)) {
379
+ return {
380
+ type: 2,
381
+ int_value: value
382
+ }
383
+ } else if (typeof value === 'number') {
384
+ return {
385
+ type: 3,
386
+ double_value: value
387
+ }
388
+ } else if (Array.isArray(value)) {
389
+ if (depth === 0) {
390
+ const convertedArray = value
391
+ .map((val) => convertSpanEventAttributeValues(key, val, 1))
392
+ .filter((convertedVal) => convertedVal !== undefined)
393
+
394
+ // Only include array_value if there are valid elements
395
+ if (convertedArray.length > 0) {
396
+ return {
397
+ type: 4,
398
+ array_value: convertedArray
399
+ }
400
+ } else {
401
+ // If all elements were unsupported, return undefined
402
+ return undefined
403
+ }
404
+ } else {
405
+ memoizedLogDebug(key, 'Encountered nested array data type for span event v0.4 encoding. ' +
406
+ `Skipping encoding key: ${key}: with value: ${typeof value}.`
407
+ )
408
+ return undefined
409
+ }
410
+ } else {
411
+ memoizedLogDebug(key, 'Encountered unsupported data type for span event v0.4 encoding, key: ' +
412
+ `${key}: with value: ${typeof value}. Skipping encoding of pair.`
413
+ )
414
+ return undefined
415
+ }
416
+ }
417
+
322
418
  module.exports = { AgentEncoder }
@@ -7,7 +7,13 @@ const ARRAY_OF_TWO = 0x92
7
7
  const ARRAY_OF_TWELVE = 0x9c
8
8
 
9
9
  function formatSpan (span) {
10
- return normalizeSpan(truncateSpan(span, false))
10
+ span = normalizeSpan(truncateSpan(span, false))
11
+ // ensure span events are encoded as tags
12
+ if (span.span_events) {
13
+ span.meta.events = JSON.stringify(span.span_events)
14
+ delete span.span_events
15
+ }
16
+ return span
11
17
  }
12
18
 
13
19
  class AgentEncoder extends BaseEncoder {
@@ -24,7 +24,8 @@ class AgentExporter {
24
24
  prioritySampler,
25
25
  lookup,
26
26
  protocolVersion,
27
- headers
27
+ headers,
28
+ config
28
29
  })
29
30
 
30
31
  this._timer = undefined
@@ -10,15 +10,16 @@ const BaseWriter = require('../common/writer')
10
10
  const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent'
11
11
 
12
12
  class Writer extends BaseWriter {
13
- constructor ({ prioritySampler, lookup, protocolVersion, headers }) {
13
+ constructor ({ prioritySampler, lookup, protocolVersion, headers, config = {} }) {
14
14
  super(...arguments)
15
15
  const AgentEncoder = getEncoder(protocolVersion)
16
16
 
17
17
  this._prioritySampler = prioritySampler
18
18
  this._lookup = lookup
19
19
  this._protocolVersion = protocolVersion
20
- this._encoder = new AgentEncoder(this)
21
20
  this._headers = headers
21
+ this._config = config
22
+ this._encoder = new AgentEncoder(this)
22
23
  }
23
24
 
24
25
  _sendPayload (data, count, done) {
@@ -68,7 +68,7 @@ function setSingleSpanIngestionTags (span, options) {
68
68
  addTag({}, span.metrics, SPAN_SAMPLING_MAX_PER_SECOND, options.maxPerSecond)
69
69
  }
70
70
 
71
- function extractSpanLinks (trace, span) {
71
+ function extractSpanLinks (formattedSpan, span) {
72
72
  const links = []
73
73
  if (span._links) {
74
74
  for (const link of span._links) {
@@ -87,10 +87,10 @@ function extractSpanLinks (trace, span) {
87
87
  links.push(formattedLink)
88
88
  }
89
89
  }
90
- if (links.length > 0) { trace.meta['_dd.span_links'] = JSON.stringify(links) }
90
+ if (links.length > 0) { formattedSpan.meta['_dd.span_links'] = JSON.stringify(links) }
91
91
  }
92
92
 
93
- function extractSpanEvents (trace, span) {
93
+ function extractSpanEvents (formattedSpan, span) {
94
94
  const events = []
95
95
  if (span._events) {
96
96
  for (const event of span._events) {
@@ -103,10 +103,12 @@ function extractSpanEvents (trace, span) {
103
103
  events.push(formattedEvent)
104
104
  }
105
105
  }
106
- if (events.length > 0) { trace.meta.events = JSON.stringify(events) }
106
+ if (events.length > 0) {
107
+ formattedSpan.span_events = events
108
+ }
107
109
  }
108
110
 
109
- function extractTags (trace, span) {
111
+ function extractTags (formattedSpan, span) {
110
112
  const context = span.context()
111
113
  const origin = context._trace.origin
112
114
  const tags = context._tags
@@ -114,7 +116,7 @@ function extractTags (trace, span) {
114
116
  const priority = context._sampling.priority
115
117
 
116
118
  if (tags['span.kind'] && tags['span.kind'] !== 'internal') {
117
- addTag({}, trace.metrics, MEASURED, 1)
119
+ addTag({}, formattedSpan.metrics, MEASURED, 1)
118
120
  }
119
121
 
120
122
  const tracerService = span.tracer()._service.toLowerCase()
@@ -129,22 +131,22 @@ function extractTags (trace, span) {
129
131
  case 'service.name':
130
132
  case 'span.type':
131
133
  case 'resource.name':
132
- addTag(trace, {}, map[tag], tags[tag])
134
+ addTag(formattedSpan, {}, map[tag], tags[tag])
133
135
  break
134
136
  // HACK: remove when Datadog supports numeric status code
135
137
  case 'http.status_code':
136
- addTag(trace.meta, {}, tag, tags[tag] && String(tags[tag]))
138
+ addTag(formattedSpan.meta, {}, tag, tags[tag] && String(tags[tag]))
137
139
  break
138
140
  case 'analytics.event':
139
- addTag({}, trace.metrics, ANALYTICS, tags[tag] === undefined || tags[tag] ? 1 : 0)
141
+ addTag({}, formattedSpan.metrics, ANALYTICS, tags[tag] === undefined || tags[tag] ? 1 : 0)
140
142
  break
141
143
  case HOSTNAME_KEY:
142
144
  case MEASURED:
143
- addTag({}, trace.metrics, tag, tags[tag] === undefined || tags[tag] ? 1 : 0)
145
+ addTag({}, formattedSpan.metrics, tag, tags[tag] === undefined || tags[tag] ? 1 : 0)
144
146
  break
145
147
  case 'error':
146
148
  if (context._name !== 'fs.operation') {
147
- extractError(trace, tags[tag])
149
+ extractError(formattedSpan, tags[tag])
148
150
  }
149
151
  break
150
152
  case ERROR_TYPE:
@@ -152,60 +154,60 @@ function extractTags (trace, span) {
152
154
  case ERROR_STACK:
153
155
  // HACK: remove when implemented in the backend
154
156
  if (context._name !== 'fs.operation') {
155
- // HACK: to ensure otel.recordException does not influence trace.error
157
+ // HACK: to ensure otel.recordException does not influence formattedSpan.error
156
158
  if (tags.setTraceError) {
157
- trace.error = 1
159
+ formattedSpan.error = 1
158
160
  }
159
161
  } else {
160
162
  break
161
163
  }
162
164
  default: // eslint-disable-line no-fallthrough
163
- addTag(trace.meta, trace.metrics, tag, tags[tag])
165
+ addTag(formattedSpan.meta, formattedSpan.metrics, tag, tags[tag])
164
166
  }
165
167
  }
166
- setSingleSpanIngestionTags(trace, context._spanSampling)
168
+ setSingleSpanIngestionTags(formattedSpan, context._spanSampling)
167
169
 
168
- addTag(trace.meta, trace.metrics, 'language', 'javascript')
169
- addTag(trace.meta, trace.metrics, PROCESS_ID, process.pid)
170
- addTag(trace.meta, trace.metrics, SAMPLING_PRIORITY_KEY, priority)
171
- addTag(trace.meta, trace.metrics, ORIGIN_KEY, origin)
172
- addTag(trace.meta, trace.metrics, HOSTNAME_KEY, hostname)
170
+ addTag(formattedSpan.meta, formattedSpan.metrics, 'language', 'javascript')
171
+ addTag(formattedSpan.meta, formattedSpan.metrics, PROCESS_ID, process.pid)
172
+ addTag(formattedSpan.meta, formattedSpan.metrics, SAMPLING_PRIORITY_KEY, priority)
173
+ addTag(formattedSpan.meta, formattedSpan.metrics, ORIGIN_KEY, origin)
174
+ addTag(formattedSpan.meta, formattedSpan.metrics, HOSTNAME_KEY, hostname)
173
175
  }
174
176
 
175
- function extractRootTags (trace, span) {
177
+ function extractRootTags (formattedSpan, span) {
176
178
  const context = span.context()
177
179
  const isLocalRoot = span === context._trace.started[0]
178
180
  const parentId = context._parentId
179
181
 
180
182
  if (!isLocalRoot || (parentId && parentId.toString(10) !== '0')) return
181
183
 
182
- addTag({}, trace.metrics, SAMPLING_RULE_DECISION, context._trace[SAMPLING_RULE_DECISION])
183
- addTag({}, trace.metrics, SAMPLING_LIMIT_DECISION, context._trace[SAMPLING_LIMIT_DECISION])
184
- addTag({}, trace.metrics, SAMPLING_AGENT_DECISION, context._trace[SAMPLING_AGENT_DECISION])
185
- addTag({}, trace.metrics, TOP_LEVEL_KEY, 1)
184
+ addTag({}, formattedSpan.metrics, SAMPLING_RULE_DECISION, context._trace[SAMPLING_RULE_DECISION])
185
+ addTag({}, formattedSpan.metrics, SAMPLING_LIMIT_DECISION, context._trace[SAMPLING_LIMIT_DECISION])
186
+ addTag({}, formattedSpan.metrics, SAMPLING_AGENT_DECISION, context._trace[SAMPLING_AGENT_DECISION])
187
+ addTag({}, formattedSpan.metrics, TOP_LEVEL_KEY, 1)
186
188
  }
187
189
 
188
- function extractChunkTags (trace, span) {
190
+ function extractChunkTags (formattedSpan, span) {
189
191
  const context = span.context()
190
192
  const isLocalRoot = span === context._trace.started[0]
191
193
 
192
194
  if (!isLocalRoot) return
193
195
 
194
196
  for (const key in context._trace.tags) {
195
- addTag(trace.meta, trace.metrics, key, context._trace.tags[key])
197
+ addTag(formattedSpan.meta, formattedSpan.metrics, key, context._trace.tags[key])
196
198
  }
197
199
  }
198
200
 
199
- function extractError (trace, error) {
201
+ function extractError (formattedSpan, error) {
200
202
  if (!error) return
201
203
 
202
- trace.error = 1
204
+ formattedSpan.error = 1
203
205
 
204
206
  if (isError(error)) {
205
207
  // AggregateError only has a code and no message.
206
- addTag(trace.meta, trace.metrics, ERROR_MESSAGE, error.message || error.code)
207
- addTag(trace.meta, trace.metrics, ERROR_TYPE, error.name)
208
- addTag(trace.meta, trace.metrics, ERROR_STACK, error.stack)
208
+ addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_MESSAGE, error.message || error.code)
209
+ addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_TYPE, error.name)
210
+ addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_STACK, error.stack)
209
211
  }
210
212
  }
211
213
 
@@ -4,6 +4,8 @@ module.exports = {
4
4
  SPAN_KINDS: ['llm', 'agent', 'workflow', 'task', 'tool', 'embedding', 'retrieval'],
5
5
  SPAN_KIND: '_ml_obs.meta.span.kind',
6
6
  SESSION_ID: '_ml_obs.session_id',
7
+ DECORATOR: '_ml_obs.decorator',
8
+ INTEGRATION: '_ml_obs.integration',
7
9
  METADATA: '_ml_obs.meta.metadata',
8
10
  METRICS: '_ml_obs.metrics',
9
11
  ML_APP: '_ml_obs.meta.ml_app',
@@ -43,14 +43,14 @@ class NoopLLMObs {
43
43
  const ctx = ctxOrPropertyKey
44
44
  if (ctx.kind !== 'method') return target
45
45
 
46
- return llmobs.wrap({ name: ctx.name, ...options }, target)
46
+ return llmobs.wrap({ name: ctx.name, _decorator: true, ...options }, target)
47
47
  } else {
48
48
  const propertyKey = ctxOrPropertyKey
49
49
  if (descriptor) {
50
50
  if (typeof descriptor.value !== 'function') return descriptor
51
51
 
52
52
  const original = descriptor.value
53
- descriptor.value = llmobs.wrap({ name: propertyKey, ...options }, original)
53
+ descriptor.value = llmobs.wrap({ name: propertyKey, _decorator: true, ...options }, original)
54
54
 
55
55
  return descriptor
56
56
  } else {
@@ -59,7 +59,7 @@ class NoopLLMObs {
59
59
  const original = target[propertyKey]
60
60
  Object.defineProperty(target, propertyKey, {
61
61
  ...Object.getOwnPropertyDescriptor(target, propertyKey),
62
- value: llmobs.wrap({ name: propertyKey, ...options }, original)
62
+ value: llmobs.wrap({ name: propertyKey, _decorator: true, ...options }, original)
63
63
  })
64
64
 
65
65
  return target
@@ -43,7 +43,7 @@ class LLMObsPlugin extends TracingPlugin {
43
43
  llmobsStorage.enterWith({ span })
44
44
  ctx.llmobs.parent = parent
45
45
 
46
- this._tagger.registerLLMObsSpan(span, { parent, ...registerOptions })
46
+ this._tagger.registerLLMObsSpan(span, { parent, integration: this.constructor.id, ...registerOptions })
47
47
  }
48
48
  }
49
49
 
@@ -55,7 +55,8 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
55
55
  modelName: modelName.toLowerCase(),
56
56
  modelProvider: modelProvider.toLowerCase(),
57
57
  kind: 'llm',
58
- name: 'bedrock-runtime.command'
58
+ name: 'bedrock-runtime.command',
59
+ integration: 'bedrock'
59
60
  })
60
61
 
61
62
  const requestParams = extractRequestParams(request.params, modelProvider)
@@ -0,0 +1,196 @@
1
+ 'use strict'
2
+
3
+ const LLMObsPlugin = require('./base')
4
+ const {
5
+ extractModel,
6
+ extractSystemInstructions
7
+ } = require('../../../../datadog-plugin-google-cloud-vertexai/src/utils')
8
+
9
+ class VertexAILLMObsPlugin extends LLMObsPlugin {
10
+ static get id () { return 'vertexai' } // used for llmobs telemetry
11
+ static get prefix () {
12
+ return 'tracing:apm:vertexai:request'
13
+ }
14
+
15
+ getLLMObsSpanRegisterOptions (ctx) {
16
+ const history = ctx.instance?.historyInternal || []
17
+ ctx.history = history
18
+
19
+ return {
20
+ kind: 'llm',
21
+ modelName: extractModel(ctx.instance),
22
+ modelProvider: 'google',
23
+ name: ctx.resource
24
+ }
25
+ }
26
+
27
+ setLLMObsTags (ctx) {
28
+ const span = ctx.currentStore?.span
29
+ if (!span) return
30
+
31
+ const { instance, result, request } = ctx
32
+ const history = ctx.history || []
33
+ const systemInstructions = extractSystemInstructions(instance)
34
+
35
+ const metadata = getMetadata(instance)
36
+ const inputMessages = extractInputMessages(request, history, systemInstructions)
37
+ const outputMessages = extractOutputMessages(result)
38
+ const metrics = extractMetrics(result)
39
+
40
+ this._tagger.tagLLMIO(span, inputMessages, outputMessages)
41
+ this._tagger.tagMetadata(span, metadata)
42
+ this._tagger.tagMetrics(span, metrics)
43
+ }
44
+ }
45
+
46
+ function getMetadata (instance) {
47
+ const metadata = {}
48
+
49
+ const modelConfig = instance.generationConfig
50
+ if (!modelConfig) return metadata
51
+
52
+ for (const [parameter, parameterKey] of [
53
+ ['temperature', 'temperature'],
54
+ ['maxOutputTokens', 'max_output_tokens'],
55
+ ['candidateCount', 'candidate_count'],
56
+ ['topP', 'top_p'],
57
+ ['topK', 'top_k']
58
+ ]) {
59
+ if (modelConfig[parameter]) {
60
+ metadata[parameterKey] = modelConfig[parameter]
61
+ }
62
+ }
63
+
64
+ return metadata
65
+ }
66
+
67
+ function extractInputMessages (request, history, systemInstructions) {
68
+ const contents = typeof request === 'string' || Array.isArray(request) ? request : request.contents
69
+ const messages = []
70
+
71
+ if (systemInstructions) {
72
+ for (const instruction of systemInstructions) {
73
+ messages.push({ content: instruction || '', role: 'system' })
74
+ }
75
+ }
76
+
77
+ for (const content of history) {
78
+ messages.push(...extractMessagesFromContent(content))
79
+ }
80
+
81
+ if (typeof contents === 'string') {
82
+ messages.push({ content: contents })
83
+ return messages
84
+ }
85
+
86
+ if (isPart(contents)) {
87
+ messages.push(extractMessageFromPart(contents))
88
+ return messages
89
+ }
90
+
91
+ if (!Array.isArray(contents)) {
92
+ messages.push({
93
+ content: '[Non-array content object: ' +
94
+ `${(typeof contents.toString === 'function' ? contents.toString() : String(contents))}]`
95
+ })
96
+ return messages
97
+ }
98
+
99
+ for (const content of contents) {
100
+ if (typeof content === 'string') {
101
+ messages.push({ content })
102
+ continue
103
+ }
104
+
105
+ if (isPart(content)) {
106
+ messages.push(extractMessageFromPart(content))
107
+ continue
108
+ }
109
+
110
+ messages.push(...extractMessagesFromContent(content))
111
+ }
112
+
113
+ return messages
114
+ }
115
+
116
+ function extractOutputMessages (result) {
117
+ if (!result) return [{ content: '' }]
118
+ const { response } = result
119
+
120
+ if (!response) return [{ content: '' }]
121
+
122
+ const outputMessages = []
123
+ const candidates = response.candidates || []
124
+ for (const candidate of candidates) {
125
+ const content = candidate.content || ''
126
+ outputMessages.push(...extractMessagesFromContent(content))
127
+ }
128
+
129
+ return outputMessages
130
+ }
131
+
132
+ function extractMessagesFromContent (content) {
133
+ const messages = []
134
+
135
+ const role = content.role || ''
136
+ const parts = content.parts || []
137
+ if (parts == null || parts.length === 0 || !Array.isArray(parts)) {
138
+ const message = {
139
+ content:
140
+ `[Non-text content object: ${(typeof content.toString === 'function' ? content.toString() : String(content))}]`
141
+ }
142
+ if (role) message.role = role
143
+ messages.push(message)
144
+ return messages
145
+ }
146
+
147
+ for (const part of parts) {
148
+ const message = extractMessageFromPart(part, role)
149
+ messages.push(message)
150
+ }
151
+
152
+ return messages
153
+ }
154
+
155
+ function extractMessageFromPart (part, role) {
156
+ const text = part.text || ''
157
+ const functionCall = part.functionCall
158
+ const functionResponse = part.functionResponse
159
+
160
+ const message = { content: text }
161
+ if (role) message.role = role
162
+ if (functionCall) {
163
+ message.toolCalls = [{
164
+ name: functionCall.name,
165
+ arguments: functionCall.args
166
+ }]
167
+ }
168
+ if (functionResponse) {
169
+ message.content = `[tool result: ${functionResponse.response}]`
170
+ }
171
+
172
+ return message
173
+ }
174
+
175
+ function extractMetrics (result) {
176
+ if (!result) return {}
177
+ const { response } = result
178
+
179
+ if (!response) return {}
180
+
181
+ const tokenCounts = response.usageMetadata
182
+ const metrics = {}
183
+ if (tokenCounts) {
184
+ metrics.inputTokens = tokenCounts.promptTokenCount
185
+ metrics.outputTokens = tokenCounts.candidatesTokenCount
186
+ metrics.totalTokens = tokenCounts.totalTokenCount
187
+ }
188
+
189
+ return metrics
190
+ }
191
+
192
+ function isPart (part) {
193
+ return part.text || part.functionCall || part.functionResponse
194
+ }
195
+
196
+ module.exports = VertexAILLMObsPlugin
@@ -105,12 +105,12 @@ class LLMObs extends NoopLLMObs {
105
105
 
106
106
  if (fn.length > 1) {
107
107
  return this._tracer.trace(name, spanOptions, (span, cb) =>
108
- this._activate(span, { kind, options: llmobsOptions }, () => fn(span, cb))
108
+ this._activate(span, { kind, ...llmobsOptions }, () => fn(span, cb))
109
109
  )
110
110
  }
111
111
 
112
112
  return this._tracer.trace(name, spanOptions, span =>
113
- this._activate(span, { kind, options: llmobsOptions }, () => fn(span))
113
+ this._activate(span, { kind, ...llmobsOptions }, () => fn(span))
114
114
  )
115
115
  }
116
116
 
@@ -166,7 +166,7 @@ class LLMObs extends NoopLLMObs {
166
166
  }
167
167
 
168
168
  try {
169
- const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => fn.apply(this, fnArgs))
169
+ const result = llmobs._activate(span, { kind, ...llmobsOptions }, () => fn.apply(this, fnArgs))
170
170
 
171
171
  if (result && typeof result.then === 'function') {
172
172
  return result.then(
@@ -430,6 +430,7 @@ class LLMObs extends NoopLLMObs {
430
430
  modelProvider,
431
431
  sessionId,
432
432
  mlApp,
433
+ _decorator,
433
434
  ...spanOptions
434
435
  } = options
435
436
 
@@ -438,6 +439,7 @@ class LLMObs extends NoopLLMObs {
438
439
  modelName,
439
440
  modelProvider,
440
441
  sessionId,
442
+ _decorator,
441
443
  spanOptions
442
444
  }
443
445
  }