dd-trace 5.62.0 → 5.63.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 (33) hide show
  1. package/README.md +0 -5
  2. package/package.json +2 -2
  3. package/packages/datadog-instrumentations/src/ai.js +140 -0
  4. package/packages/datadog-instrumentations/src/couchbase.js +102 -65
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  6. package/packages/datadog-instrumentations/src/helpers/register.js +2 -22
  7. package/packages/datadog-instrumentations/src/hono.js +11 -8
  8. package/packages/datadog-instrumentations/src/knex.js +15 -17
  9. package/packages/datadog-instrumentations/src/mongodb-core.js +4 -6
  10. package/packages/datadog-instrumentations/src/next.js +4 -8
  11. package/packages/datadog-instrumentations/src/pg.js +38 -48
  12. package/packages/datadog-plugin-aerospike/src/index.js +6 -2
  13. package/packages/datadog-plugin-ai/src/index.js +17 -0
  14. package/packages/datadog-plugin-ai/src/tracing.js +33 -0
  15. package/packages/datadog-plugin-ai/src/utils.js +28 -0
  16. package/packages/datadog-plugin-couchbase/src/index.js +37 -17
  17. package/packages/datadog-plugin-pg/src/index.js +5 -2
  18. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +14 -7
  19. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  20. package/packages/dd-trace/src/appsec/recommended.json +271 -2
  21. package/packages/dd-trace/src/guardrails/telemetry.js +18 -2
  22. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +351 -0
  23. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +179 -0
  24. package/packages/dd-trace/src/llmobs/writers/base.js +3 -2
  25. package/packages/dd-trace/src/opentracing/span_context.js +4 -0
  26. package/packages/dd-trace/src/plugin_manager.js +8 -4
  27. package/packages/dd-trace/src/plugins/index.js +1 -0
  28. package/packages/dd-trace/src/plugins/util/ip_extractor.js +44 -3
  29. package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +24 -23
  30. package/packages/dd-trace/src/profiling/profilers/events.js +3 -2
  31. package/packages/dd-trace/src/profiling/profilers/wall.js +2 -2
  32. package/packages/dd-trace/src/supported-configurations.json +2 -0
  33. package/packages/dd-trace/src/tracer_metadata.js +1 -1
@@ -0,0 +1,179 @@
1
+ 'use strict'
2
+
3
+ const MODEL_METADATA_KEYS = new Set([
4
+ 'frequency_penalty',
5
+ 'max_tokens',
6
+ 'presence_penalty',
7
+ 'temperature',
8
+ 'top_p',
9
+ 'top_k',
10
+ 'stop_sequences'
11
+ ])
12
+
13
+ /**
14
+ * Get the span tags from the context (either the attributes or the span tags).
15
+ *
16
+ * @param {Record<string, any>} ctx
17
+ * @returns {Record<string, any>}
18
+ */
19
+ function getSpanTags (ctx) {
20
+ const span = ctx.currentStore?.span
21
+ const carrier = ctx.attributes ?? span?.context()._tags ?? {}
22
+ return carrier
23
+ }
24
+
25
+ /**
26
+ * Get the operation name from the span name
27
+ *
28
+ * @example
29
+ * span._name = 'ai.generateText'
30
+ * getOperation(span) // 'generateText'
31
+ *
32
+ * @example
33
+ * span._name = 'ai.generateText.doGenerate'
34
+ * getOperation(span) // 'doGenerate'
35
+ *
36
+ * @param {import('../../../opentracing/span')} span
37
+ * @returns {string}
38
+ */
39
+ function getOperation (span) {
40
+ const name = span._name
41
+ if (!name) return
42
+
43
+ return name.split('.').pop()
44
+ }
45
+
46
+ /**
47
+ * Get the LLM token usage from the span tags
48
+ * @param {Record<string, string>} tags
49
+ * @returns {{inputTokens: number, outputTokens: number, totalTokens: number}}
50
+ */
51
+ function getUsage (tags) {
52
+ const usage = {}
53
+ const inputTokens = tags['ai.usage.promptTokens']
54
+ const outputTokens = tags['ai.usage.completionTokens']
55
+
56
+ if (inputTokens != null) usage.inputTokens = inputTokens
57
+ if (outputTokens != null) usage.outputTokens = outputTokens
58
+
59
+ const totalTokens = inputTokens + outputTokens
60
+ if (!Number.isNaN(totalTokens)) usage.totalTokens = totalTokens
61
+
62
+ return usage
63
+ }
64
+
65
+ /**
66
+ * Safely JSON parses a string value with a default fallback
67
+ * @param {string} str
68
+ * @param {any} defaultValue
69
+ * @returns {Record<string, any> | string | Array<any>}
70
+ */
71
+ function getJsonStringValue (str, defaultValue) {
72
+ let maybeValue = defaultValue
73
+ try {
74
+ maybeValue = JSON.parse(str)
75
+ } catch {
76
+ // do nothing
77
+ }
78
+
79
+ return maybeValue
80
+ }
81
+
82
+ /**
83
+ * Get the model metadata from the span tags (top_p, top_k, temperature, etc.)
84
+ * @param {import('../../../opentracing/span')} span
85
+ * @returns {Record<string, string> | null}
86
+ */
87
+ function getModelMetadata (tags) {
88
+ const modelMetadata = {}
89
+ for (const metadata of MODEL_METADATA_KEYS) {
90
+ const metadataTagKey = `gen_ai.request.${metadata}`
91
+ const metadataValue = tags[metadataTagKey]
92
+ if (metadataValue) {
93
+ modelMetadata[metadata] = metadataValue
94
+ }
95
+ }
96
+
97
+ return Object.keys(modelMetadata).length ? modelMetadata : null
98
+ }
99
+
100
+ /**
101
+ * Get the generation metadata from the span tags (maxSteps, maxRetries, etc.)
102
+ * @param {Record<string, string>} tags
103
+ * @returns {Record<string, string> | null}
104
+ */
105
+ function getGenerationMetadata (tags) {
106
+ const metadata = {}
107
+
108
+ for (const tag of Object.keys(tags)) {
109
+ if (!tag.startsWith('ai.settings')) continue
110
+
111
+ const settingKey = tag.split('.').pop()
112
+ const transformedKey = settingKey.replaceAll(/[A-Z]/g, letter => '_' + letter.toLowerCase())
113
+ if (MODEL_METADATA_KEYS.has(transformedKey)) continue
114
+
115
+ const settingValue = tags[tag]
116
+ metadata[settingKey] = settingValue
117
+ }
118
+
119
+ return Object.keys(metadata).length ? metadata : null
120
+ }
121
+
122
+ /**
123
+ * Get the tool name from the span tags.
124
+ * If the tool name is a parsable number, or is not found, null is returned.
125
+ * Older versions of the ai sdk would tag the tool name as its index in the tools array.
126
+ *
127
+ * @param {Record<string, string>} tags
128
+ * @returns {string | null}
129
+ */
130
+ function getToolNameFromTags (tags) {
131
+ const toolName = tags['ai.toolCall.name']
132
+ if (!toolName) return null
133
+
134
+ const parsedToolName = Number.parseInt(toolName)
135
+ if (!Number.isNaN(parsedToolName)) return null
136
+
137
+ return toolName
138
+ }
139
+
140
+ /**
141
+ * Get the content of a tool call result.
142
+ * Version 5 of the ai sdk sets this tag as `content.output`, with a `
143
+ * @param {Record<string, any>} content
144
+ * @returns {string}
145
+ */
146
+ function getToolCallResultContent (content) {
147
+ const { output, result } = content
148
+ if (output) {
149
+ if (output.type === 'text') {
150
+ return output.value
151
+ } else if (output.type === 'json') {
152
+ return JSON.stringify(output.value)
153
+ }
154
+ return '[Unparsable Tool Result]'
155
+ } else if (result) {
156
+ if (typeof result === 'string') {
157
+ return result
158
+ }
159
+
160
+ try {
161
+ return JSON.stringify(result)
162
+ } catch {
163
+ return '[Unparsable Tool Result]'
164
+ }
165
+ } else {
166
+ return '[Unsupported Tool Result]'
167
+ }
168
+ }
169
+
170
+ module.exports = {
171
+ getSpanTags,
172
+ getOperation,
173
+ getUsage,
174
+ getJsonStringValue,
175
+ getModelMetadata,
176
+ getGenerationMetadata,
177
+ getToolNameFromTags,
178
+ getToolCallResultContent
179
+ }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const request = require('../../exporters/common/request')
4
+ const { getEnvironmentVariable } = require('../../config-helper')
4
5
  const { URL, format } = require('node:url')
5
6
  const path = require('node:path')
6
7
 
@@ -17,8 +18,8 @@ const { parseResponseAndLog } = require('./util')
17
18
 
18
19
  class BaseLLMObsWriter {
19
20
  constructor ({ interval, timeout, eventType, config, endpoint, intake }) {
20
- this._interval = interval || 1000 // 1s
21
- this._timeout = timeout || 5000 // 5s
21
+ this._interval = interval ?? getEnvironmentVariable('_DD_LLMOBS_FLUSH_INTERVAL') ?? 1000 // 1s
22
+ this._timeout = timeout ?? getEnvironmentVariable('_DD_LLMOBS_TIMEOUT') ?? 5000 // 5s
22
23
  this._eventType = eventType
23
24
 
24
25
  this._buffer = []
@@ -59,6 +59,10 @@ class DatadogSpanContext {
59
59
  return this._spanId.toString(10)
60
60
  }
61
61
 
62
+ toBigIntSpanId () {
63
+ return this._spanId.toBigInt()
64
+ }
65
+
62
66
  toTraceparent () {
63
67
  const flags = this._sampling.priority >= AUTO_KEEP ? '01' : '00'
64
68
  const traceId = this.toTraceId(true)
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
- const { isFalse, normalizePluginEnvName } = require('./util')
4
+ const { isFalse, isTrue, normalizePluginEnvName } = require('./util')
5
5
  const plugins = require('./plugins')
6
6
  const log = require('./log')
7
7
  const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper')
@@ -32,8 +32,7 @@ loadChannel.subscribe(({ name }) => {
32
32
  function maybeEnable (Plugin) {
33
33
  if (!Plugin || typeof Plugin !== 'function') return
34
34
  if (!pluginClasses[Plugin.id]) {
35
- const envName = `DD_TRACE_${Plugin.id.toUpperCase()}_ENABLED`
36
- const enabled = getEnvironmentVariable(normalizePluginEnvName(envName))
35
+ const enabled = getEnvEnabled(Plugin)
37
36
 
38
37
  // TODO: remove the need to load the plugin class in order to disable the plugin
39
38
  if (isFalse(enabled) || disabledPlugins.has(Plugin.id)) {
@@ -46,6 +45,11 @@ function maybeEnable (Plugin) {
46
45
  }
47
46
  }
48
47
 
48
+ function getEnvEnabled (Plugin) {
49
+ const envName = `DD_TRACE_${Plugin.id.toUpperCase()}_ENABLED`
50
+ return getEnvironmentVariable(normalizePluginEnvName(envName))
51
+ }
52
+
49
53
  // TODO this must always be a singleton.
50
54
  module.exports = class PluginManager {
51
55
  constructor (tracer) {
@@ -74,7 +78,7 @@ module.exports = class PluginManager {
74
78
  this._pluginsByName[name] = new Plugin(this._tracer, this._tracerConfig)
75
79
  }
76
80
  const pluginConfig = this._configsByName[name] || {
77
- enabled: this._tracerConfig.plugins !== false
81
+ enabled: this._tracerConfig.plugins !== false && (!Plugin.experimental || isTrue(getEnvEnabled(Plugin)))
78
82
  }
79
83
 
80
84
  // extracts predetermined configuration from tracer and combines it with plugin-specific config
@@ -26,6 +26,7 @@ module.exports = {
26
26
  get '@smithy/smithy-client' () { return require('../../../datadog-plugin-aws-sdk/src') },
27
27
  get '@vitest/runner' () { return require('../../../datadog-plugin-vitest/src') },
28
28
  get aerospike () { return require('../../../datadog-plugin-aerospike/src') },
29
+ get ai () { return require('../../../datadog-plugin-ai/src') },
29
30
  get amqp10 () { return require('../../../datadog-plugin-amqp10/src') },
30
31
  get amqplib () { return require('../../../datadog-plugin-amqplib/src') },
31
32
  get avsc () { return require('../../../datadog-plugin-avsc/src') },
@@ -3,11 +3,14 @@
3
3
  const { BlockList } = require('net')
4
4
  const net = require('net')
5
5
 
6
+ const FORWARED_HEADER_NAME = 'forwarded'
7
+
6
8
  const ipHeaderList = [
7
9
  'x-forwarded-for',
8
10
  'x-real-ip',
9
11
  'true-client-ip',
10
12
  'x-client-ip',
13
+ FORWARED_HEADER_NAME,
11
14
  'forwarded-for',
12
15
  'x-cluster-client-ip',
13
16
  'fastly-client-ip',
@@ -49,7 +52,8 @@ function extractIp (config, req) {
49
52
  let firstPrivateIp
50
53
  if (headers) {
51
54
  for (const ipHeaderName of ipHeaderList) {
52
- const firstIp = findFirstIp(headers[ipHeaderName])
55
+ const header = headers[ipHeaderName]
56
+ const firstIp = ipHeaderName === FORWARED_HEADER_NAME ? findFirstIpForwardedFormat(header) : findFirstIp(header)
53
57
 
54
58
  if (firstIp.public) {
55
59
  return firstIp.public
@@ -62,6 +66,10 @@ function extractIp (config, req) {
62
66
  return firstPrivateIp || req.socket?.remoteAddress
63
67
  }
64
68
 
69
+ function isPublicIp (ip, type) {
70
+ return !privateIPMatcher.check(ip, type === 6 ? 'ipv6' : 'ipv4')
71
+ }
72
+
65
73
  function findFirstIp (str) {
66
74
  const result = {}
67
75
  if (!str) return result
@@ -76,10 +84,10 @@ function findFirstIp (str) {
76
84
  const type = net.isIP(chunk)
77
85
  if (!type) continue
78
86
 
79
- if (!privateIPMatcher.check(chunk, type === 6 ? 'ipv6' : 'ipv4')) {
87
+ if (isPublicIp(chunk, type)) {
80
88
  // it's public, return it immediately
81
89
  result.public = chunk
82
- break
90
+ return result
83
91
  }
84
92
 
85
93
  // it's private, only save the first one found
@@ -89,6 +97,39 @@ function findFirstIp (str) {
89
97
  return result
90
98
  }
91
99
 
100
+ const forwardedForRegexp = /for="?\[?(([0-9]+\.)+[0-9]+|[0-9a-f:]*:[0-9a-f]*)/i
101
+ const forwardedByRegexp = /by="?\[?(([0-9]+\.)+[0-9]+|[0-9a-f:]*:[0-9a-f]*)/i
102
+ const forwardedRegexps = [forwardedForRegexp, forwardedByRegexp]
103
+
104
+ function findFirstIpForwardedFormat (str) {
105
+ const result = {}
106
+ if (!str) return result
107
+
108
+ const splitted = str.split(',')
109
+
110
+ for (const part of splitted) {
111
+ const chunk = part.trim()
112
+
113
+ for (const regex of forwardedRegexps) {
114
+ const ip = regex.exec(chunk)?.[1]
115
+
116
+ const type = net.isIP(ip)
117
+ if (!type) continue
118
+
119
+ if (isPublicIp(ip, type)) {
120
+ // it's public, return it immediately
121
+ result.public = ip
122
+ return result
123
+ }
124
+
125
+ // it's private, only save the first one found
126
+ if (!result.private) result.private = ip
127
+ }
128
+ }
129
+
130
+ return result
131
+ }
132
+
92
133
  module.exports = {
93
134
  extractIp,
94
135
  ipHeaderList
@@ -7,54 +7,55 @@ const { performance } = require('perf_hooks')
7
7
  // start/error/finish methods to the appropriate diagnostic channels.
8
8
  // TODO: Decouple this from TracingPlugin.
9
9
  class EventPlugin extends TracingPlugin {
10
+ #eventHandler
11
+ #eventFilter
12
+ #dataSymbol
13
+ #entryType
14
+
10
15
  constructor (eventHandler, eventFilter) {
11
16
  super()
12
- this.eventHandler = eventHandler
13
- this.eventFilter = eventFilter
14
- this.contextData = new WeakMap()
15
- this.entryType = this.constructor.entryType
17
+ this.#eventHandler = eventHandler
18
+ this.#eventFilter = eventFilter
19
+ this.#entryType = this.constructor.entryType
20
+ this.#dataSymbol = Symbol(`dd-trace.profiling.event.${this.#entryType}.${this.constructor.operation}`)
16
21
  }
17
22
 
18
23
  start (ctx) {
19
- this.contextData.set(ctx, {
20
- startEvent: ctx,
21
- startTime: performance.now()
22
- })
24
+ ctx[this.#dataSymbol] = performance.now()
23
25
  }
24
26
 
25
27
  error (ctx) {
26
- const data = this.contextData.get(ctx)
27
- if (data) {
28
- data.error = true
29
- }
28
+ // We don't emit perf events for failed operations
29
+ ctx[this.#dataSymbol] = undefined
30
30
  }
31
31
 
32
32
  finish (ctx) {
33
- const data = this.contextData.get(ctx)
34
-
35
- if (!data) return
33
+ const startTime = ctx[this.#dataSymbol]
34
+ if (startTime === undefined) {
35
+ return
36
+ }
37
+ ctx[this.#dataSymbol] = undefined
36
38
 
37
- const { startEvent, startTime, error } = data
38
- if (error || this.ignoreEvent(startEvent)) {
39
- return // don't emit perf events for failed operations or ignored events
39
+ if (this.ignoreEvent(ctx)) {
40
+ return // don't emit perf events for ignored events
40
41
  }
41
42
 
42
43
  const duration = performance.now() - startTime
43
44
  const event = {
44
- entryType: this.entryType,
45
+ entryType: this.#entryType,
45
46
  startTime,
46
47
  duration
47
48
  }
48
49
 
49
- if (!this.eventFilter(event)) {
50
+ if (!this.#eventFilter(event)) {
50
51
  return
51
52
  }
52
53
 
53
54
  const context = (ctx.currentStore?.span || this.activeSpan)?.context()
54
- event._ddSpanId = context?.toSpanId()
55
- event._ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || event._ddSpanId
55
+ event._ddSpanId = context?.toBigIntSpanId()
56
+ event._ddRootSpanId = context?._trace.started[0]?.context().toBigIntSpanId() || event._ddSpanId
56
57
 
57
- this.eventHandler(this.extendEvent(event, startEvent))
58
+ this.#eventHandler(this.extendEvent(event, ctx))
58
59
  }
59
60
 
60
61
  ignoreEvent () {
@@ -273,10 +273,11 @@ class EventSerializer {
273
273
  new Label({ key: this.timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) })
274
274
  ]
275
275
  if (_ddSpanId) {
276
- label.push(labelFromStr(this.stringTable, this.spanIdKey, _ddSpanId))
276
+ label.push(
277
+ new Label({ key: this.spanIdKey, num: _ddSpanId }))
277
278
  }
278
279
  if (_ddRootSpanId) {
279
- label.push(labelFromStr(this.stringTable, this.rootSpanIdKey, _ddRootSpanId))
280
+ label.push(new Label({ key: this.rootSpanIdKey, num: _ddRootSpanId }))
280
281
  }
281
282
 
282
283
  const sampleInput = {
@@ -215,10 +215,10 @@ class NativeWallProfiler {
215
215
 
216
216
  _updateContext (context) {
217
217
  if (context.spanId !== null && typeof context.spanId === 'object') {
218
- context.spanId = context.spanId.toString(10)
218
+ context.spanId = context.spanId.toBigInt()
219
219
  }
220
220
  if (context.rootSpanId !== null && typeof context.rootSpanId === 'object') {
221
- context.rootSpanId = context.rootSpanId.toString(10)
221
+ context.rootSpanId = context.rootSpanId.toBigInt()
222
222
  }
223
223
  if (context.webTags !== undefined && context.endpoint === undefined) {
224
224
  // endpoint may not be determined yet, but keep it as fallback
@@ -129,6 +129,7 @@
129
129
  "DD_PROFILING_HEAP_ENABLED": ["A"],
130
130
  "DD_PROFILING_PROFILERS": ["A"],
131
131
  "DD_PROFILING_SOURCE_MAP": ["A"],
132
+ "DD_PROFILING_TIMELINE_ENABLED": ["A"],
132
133
  "DD_PROFILING_UPLOAD_PERIOD": ["A"],
133
134
  "DD_PROFILING_V8_PROFILER_BUG_WORKAROUND": ["A"],
134
135
  "DD_PROFILING_WALLTIME_ENABLED": ["A"],
@@ -160,6 +161,7 @@
160
161
  "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED": ["A"],
161
162
  "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED": ["A"],
162
163
  "DD_TRACE_AEROSPIKE_ENABLED": ["A"],
164
+ "DD_TRACE_AI_ENABLED": ["A"],
163
165
  "DD_TRACE_AGENT_PORT": ["A"],
164
166
  "DD_TRACE_AGENT_PROTOCOL_VERSION": ["A"],
165
167
  "DD_TRACE_AGENT_URL": ["A"],
@@ -14,7 +14,7 @@ function storeConfig (config) {
14
14
  config.tags['runtime-id'],
15
15
  tracerVersion,
16
16
  config.hostname,
17
- config.server || null,
17
+ config.service || null,
18
18
  config.env || null,
19
19
  config.version || null
20
20
  )