dd-trace 5.31.0 → 5.32.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 (62) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/README.md +9 -7
  3. package/package.json +5 -4
  4. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
  5. package/packages/datadog-instrumentations/src/cucumber.js +14 -5
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  7. package/packages/datadog-instrumentations/src/jest.js +70 -36
  8. package/packages/datadog-instrumentations/src/mocha/utils.js +23 -7
  9. package/packages/datadog-instrumentations/src/node-serialize.js +22 -0
  10. package/packages/datadog-instrumentations/src/openai.js +2 -0
  11. package/packages/datadog-instrumentations/src/vitest.js +107 -59
  12. package/packages/datadog-instrumentations/src/vm.js +49 -0
  13. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime.js +295 -0
  14. package/packages/datadog-plugin-aws-sdk/src/services/index.js +1 -0
  15. package/packages/datadog-plugin-cucumber/src/index.js +30 -32
  16. package/packages/datadog-plugin-jest/src/index.js +34 -37
  17. package/packages/datadog-plugin-langchain/src/index.js +12 -80
  18. package/packages/datadog-plugin-langchain/src/tracing.js +89 -0
  19. package/packages/datadog-plugin-mocha/src/index.js +18 -36
  20. package/packages/datadog-plugin-vitest/src/index.js +20 -34
  21. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  22. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +2 -0
  23. package/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js +16 -0
  24. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +9 -8
  25. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  26. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +37 -0
  27. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +65 -28
  28. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +57 -17
  29. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +18 -3
  30. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +20 -3
  31. package/packages/dd-trace/src/config.js +39 -3
  32. package/packages/dd-trace/src/crashtracking/crashtracker.js +9 -0
  33. package/packages/dd-trace/src/crashtracking/noop.js +3 -0
  34. package/packages/dd-trace/src/datastreams/fnv.js +1 -1
  35. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -2
  36. package/packages/dd-trace/src/debugger/devtools_client/config.js +1 -0
  37. package/packages/dd-trace/src/debugger/devtools_client/defaults.js +1 -0
  38. package/packages/dd-trace/src/debugger/devtools_client/index.js +30 -13
  39. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -8
  40. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +35 -1
  41. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +112 -0
  42. package/packages/dd-trace/src/debugger/devtools_client/status.js +12 -10
  43. package/packages/dd-trace/src/debugger/index.js +2 -13
  44. package/packages/dd-trace/src/llmobs/plugins/base.js +40 -11
  45. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +24 -0
  46. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +111 -0
  47. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +42 -0
  48. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +102 -0
  49. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js +32 -0
  50. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +131 -0
  51. package/packages/dd-trace/src/llmobs/plugins/openai.js +1 -1
  52. package/packages/dd-trace/src/llmobs/tagger.js +11 -3
  53. package/packages/dd-trace/src/llmobs/util.js +7 -1
  54. package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +3 -3
  55. package/packages/dd-trace/src/opentelemetry/context_manager.js +43 -3
  56. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -27
  57. package/packages/dd-trace/src/plugins/util/test.js +42 -12
  58. package/packages/dd-trace/src/priority_sampler.js +4 -1
  59. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +21 -0
  60. package/packages/dd-trace/src/profiling/profiler.js +11 -8
  61. package/packages/dd-trace/src/profiling/profilers/events.js +17 -1
  62. package/packages/dd-trace/src/proxy.js +6 -3
@@ -8,6 +8,7 @@ const send = require('./send')
8
8
  const { getStackFromCallFrames } = require('./state')
9
9
  const { ackEmitting, ackError } = require('./status')
10
10
  const { parentThreadId } = require('./config')
11
+ const { MAX_SNAPSHOTS_PER_SECOND_GLOBALLY } = require('./defaults')
11
12
  const log = require('../../log')
12
13
  const { version } = require('../../../../../package.json')
13
14
 
@@ -24,11 +25,14 @@ const expression = `
24
25
  const threadId = parentThreadId === 0 ? `pid:${process.pid}` : `pid:${process.pid};tid:${parentThreadId}`
25
26
  const threadName = parentThreadId === 0 ? 'MainThread' : `WorkerThread:${parentThreadId}`
26
27
 
28
+ const oneSecondNs = 1_000_000_000n
29
+ let globalSnapshotSamplingRateWindowStart = 0n
30
+ let snapshotsSampledWithinTheLastSecond = 0
31
+
27
32
  // WARNING: The code above the line `await session.post('Debugger.resume')` is highly optimized. Please edit with care!
28
33
  session.on('Debugger.paused', async ({ params }) => {
29
34
  const start = process.hrtime.bigint()
30
35
 
31
- let captureSnapshotForProbe = null
32
36
  let maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength
33
37
 
34
38
  // V8 doesn't allow seting more than one breakpoint at a specific location, however, it's possible to set two
@@ -38,25 +42,39 @@ session.on('Debugger.paused', async ({ params }) => {
38
42
  let sampled = false
39
43
  const length = params.hitBreakpoints.length
40
44
  let probes = new Array(length)
45
+ // TODO: Consider reusing this array between pauses and only recreating it if it needs to grow
46
+ const snapshotProbeIndex = new Uint8Array(length) // TODO: Is a limit of 256 probes ever going to be a problem?
47
+ let numberOfProbesWithSnapshots = 0
41
48
  for (let i = 0; i < length; i++) {
42
49
  const id = params.hitBreakpoints[i]
43
50
  const probe = breakpoints.get(id)
44
51
 
45
- if (start - probe.lastCaptureNs < probe.sampling.nsBetweenSampling) {
52
+ if (start - probe.lastCaptureNs < probe.nsBetweenSampling) {
46
53
  continue
47
54
  }
48
55
 
49
- sampled = true
50
- probe.lastCaptureNs = start
51
-
52
56
  if (probe.captureSnapshot === true) {
53
- captureSnapshotForProbe = probe
57
+ // This algorithm to calculate number of sampled snapshots within the last second is not perfect, as it's not a
58
+ // sliding window. But it's quick and easy :)
59
+ if (i === 0 && start - globalSnapshotSamplingRateWindowStart > oneSecondNs) {
60
+ snapshotsSampledWithinTheLastSecond = 1
61
+ globalSnapshotSamplingRateWindowStart = start
62
+ } else if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
63
+ continue
64
+ } else {
65
+ snapshotsSampledWithinTheLastSecond++
66
+ }
67
+
68
+ snapshotProbeIndex[numberOfProbesWithSnapshots++] = i
54
69
  maxReferenceDepth = highestOrUndefined(probe.capture.maxReferenceDepth, maxReferenceDepth)
55
70
  maxCollectionSize = highestOrUndefined(probe.capture.maxCollectionSize, maxCollectionSize)
56
71
  maxFieldCount = highestOrUndefined(probe.capture.maxFieldCount, maxFieldCount)
57
72
  maxLength = highestOrUndefined(probe.capture.maxLength, maxLength)
58
73
  }
59
74
 
75
+ sampled = true
76
+ probe.lastCaptureNs = start
77
+
60
78
  probes[i] = probe
61
79
  }
62
80
 
@@ -68,7 +86,7 @@ session.on('Debugger.paused', async ({ params }) => {
68
86
  const dd = await getDD(params.callFrames[0].callFrameId)
69
87
 
70
88
  let processLocalState
71
- if (captureSnapshotForProbe !== null) {
89
+ if (numberOfProbesWithSnapshots !== 0) {
72
90
  try {
73
91
  // TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
74
92
  processLocalState = await getLocalStateForCallFrame(
@@ -76,9 +94,9 @@ session.on('Debugger.paused', async ({ params }) => {
76
94
  { maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength }
77
95
  )
78
96
  } catch (err) {
79
- // TODO: This error is not tied to a specific probe, but to all probes with `captureSnapshot: true`.
80
- // However, in 99,99% of cases, there will be just a single probe, so I guess this simplification is ok?
81
- ackError(err, captureSnapshotForProbe) // TODO: Ok to continue after sending ackError?
97
+ for (let i = 0; i < numberOfProbesWithSnapshots; i++) {
98
+ ackError(err, probes[snapshotProbeIndex[i]]) // TODO: Ok to continue after sending ackError?
99
+ }
82
100
  }
83
101
  }
84
102
 
@@ -128,10 +146,9 @@ session.on('Debugger.paused', async ({ params }) => {
128
146
  }
129
147
  }
130
148
 
149
+ ackEmitting(probe)
131
150
  // TODO: Process template (DEBUG-2628)
132
- send(probe.template, logger, dd, snapshot, () => {
133
- ackEmitting(probe)
134
- })
151
+ send(probe.template, logger, dd, snapshot)
135
152
  }
136
153
  })
137
154
 
@@ -29,10 +29,9 @@ const ddtags = [
29
29
 
30
30
  const path = `/debugger/v1/input?${stringify({ ddtags })}`
31
31
 
32
- let callbacks = []
33
32
  const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush })
34
33
 
35
- function send (message, logger, dd, snapshot, cb) {
34
+ function send (message, logger, dd, snapshot) {
36
35
  const payload = {
37
36
  ddsource,
38
37
  hostname,
@@ -58,10 +57,11 @@ function send (message, logger, dd, snapshot, cb) {
58
57
  }
59
58
 
60
59
  jsonBuffer.write(json, size)
61
- callbacks.push(cb)
62
60
  }
63
61
 
64
62
  function onFlush (payload) {
63
+ log.debug('[debugger:devtools_client] Flushing probe payload buffer')
64
+
65
65
  const opts = {
66
66
  method: 'POST',
67
67
  url: config.url,
@@ -69,11 +69,7 @@ function onFlush (payload) {
69
69
  headers: { 'Content-Type': 'application/json; charset=utf-8' }
70
70
  }
71
71
 
72
- const _callbacks = callbacks
73
- callbacks = []
74
-
75
72
  request(payload, opts, (err) => {
76
- if (err) log.error('Could not send debugger payload', err)
77
- else _callbacks.forEach(cb => cb())
73
+ if (err) log.error('[debugger:devtools_client] Error sending probe payload', err)
78
74
  })
79
75
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { collectionSizeSym, fieldCountSym } = require('./symbols')
4
+ const { normalizeName, REDACTED_IDENTIFIERS } = require('./redaction')
4
5
 
5
6
  module.exports = {
6
7
  processRawState: processProperties
@@ -24,7 +25,14 @@ function processProperties (props, maxLength) {
24
25
  return result
25
26
  }
26
27
 
28
+ // TODO: Improve performance of redaction algorithm.
29
+ // This algorithm is probably slower than if we embedded the redaction logic inside the functions below.
30
+ // That way we didn't have to traverse objects that will just be redacted anyway.
27
31
  function getPropertyValue (prop, maxLength) {
32
+ return redact(prop, getPropertyValueRaw(prop, maxLength))
33
+ }
34
+
35
+ function getPropertyValueRaw (prop, maxLength) {
28
36
  // Special case for getters and setters which does not have a value property
29
37
  if ('get' in prop) {
30
38
  const hasGet = prop.get.type !== 'undefined'
@@ -185,8 +193,11 @@ function toMap (type, pairs, maxLength) {
185
193
  // `pair.value` is a special wrapper-object with subtype `internal#entry`. This can be skipped and we can go
186
194
  // directly to its children, of which there will always be exactly two, the first containing the key, and the
187
195
  // second containing the value of this entry of the Map.
196
+ const shouldRedact = shouldRedactMapValue(pair.value.properties[0])
188
197
  const key = getPropertyValue(pair.value.properties[0], maxLength)
189
- const val = getPropertyValue(pair.value.properties[1], maxLength)
198
+ const val = shouldRedact
199
+ ? notCapturedRedacted(pair.value.properties[1].value.type)
200
+ : getPropertyValue(pair.value.properties[1], maxLength)
190
201
  result.entries[i++] = [key, val]
191
202
  }
192
203
 
@@ -240,6 +251,25 @@ function arrayBufferToString (bytes, size) {
240
251
  return buf.toString()
241
252
  }
242
253
 
254
+ function redact (prop, obj) {
255
+ const name = getNormalizedNameFromProp(prop)
256
+ return REDACTED_IDENTIFIERS.has(name) ? notCapturedRedacted(obj.type) : obj
257
+ }
258
+
259
+ function shouldRedactMapValue (key) {
260
+ const isSymbol = key.value.type === 'symbol'
261
+ if (!isSymbol && key.value.type !== 'string') return false // WeakMaps uses objects as keys
262
+ const name = normalizeName(
263
+ isSymbol ? key.value.description : key.value.value,
264
+ isSymbol
265
+ )
266
+ return REDACTED_IDENTIFIERS.has(name)
267
+ }
268
+
269
+ function getNormalizedNameFromProp (prop) {
270
+ return normalizeName(prop.name, 'symbol' in prop)
271
+ }
272
+
243
273
  function setNotCaptureReasonOnCollection (result, collection) {
244
274
  if (collectionSizeSym in collection) {
245
275
  result.notCapturedReason = 'collectionSize'
@@ -250,3 +280,7 @@ function setNotCaptureReasonOnCollection (result, collection) {
250
280
  function notCapturedDepth (type) {
251
281
  return { type, notCapturedReason: 'depth' }
252
282
  }
283
+
284
+ function notCapturedRedacted (type) {
285
+ return { type, notCapturedReason: 'redactedIdent' }
286
+ }
@@ -0,0 +1,112 @@
1
+ 'use strict'
2
+
3
+ const config = require('../config')
4
+
5
+ const excludedIdentifiers = config.dynamicInstrumentation.redactionExcludedIdentifiers
6
+ .map((name) => normalizeName(name))
7
+
8
+ const REDACTED_IDENTIFIERS = new Set(
9
+ [
10
+ '2fa',
11
+ '_csrf',
12
+ '_csrf_token',
13
+ '_session',
14
+ '_xsrf',
15
+ 'access_token',
16
+ 'aiohttp_session',
17
+ 'api_key',
18
+ 'apisecret',
19
+ 'apisignature',
20
+ 'applicationkey',
21
+ 'appkey',
22
+ 'auth',
23
+ 'authtoken',
24
+ 'authorization',
25
+ 'cc_number',
26
+ 'certificatepin',
27
+ 'cipher',
28
+ 'client_secret',
29
+ 'clientid',
30
+ 'connect.sid',
31
+ 'connectionstring',
32
+ 'cookie',
33
+ 'credentials',
34
+ 'creditcard',
35
+ 'csrf',
36
+ 'csrf_token',
37
+ 'cvv',
38
+ 'databaseurl',
39
+ 'db_url',
40
+ 'encryption_key',
41
+ 'encryptionkeyid',
42
+ 'geo_location',
43
+ 'gpg_key',
44
+ 'ip_address',
45
+ 'jti',
46
+ 'jwt',
47
+ 'license_key',
48
+ 'masterkey',
49
+ 'mysql_pwd',
50
+ 'nonce',
51
+ 'oauth',
52
+ 'oauthtoken',
53
+ 'otp',
54
+ 'passhash',
55
+ 'passwd',
56
+ 'password',
57
+ 'passwordb',
58
+ 'pem_file',
59
+ 'pgp_key',
60
+ 'PHPSESSID',
61
+ 'pin',
62
+ 'pincode',
63
+ 'pkcs8',
64
+ 'private_key',
65
+ 'publickey',
66
+ 'pwd',
67
+ 'recaptcha_key',
68
+ 'refresh_token',
69
+ 'routingnumber',
70
+ 'salt',
71
+ 'secret',
72
+ 'secretKey',
73
+ 'secrettoken',
74
+ 'securitycode',
75
+ 'security_answer',
76
+ 'security_question',
77
+ 'serviceaccountcredentials',
78
+ 'session',
79
+ 'sessionid',
80
+ 'sessionkey',
81
+ 'set_cookie',
82
+ 'signature',
83
+ 'signaturekey',
84
+ 'ssh_key',
85
+ 'ssn',
86
+ 'symfony',
87
+ 'token',
88
+ 'transactionid',
89
+ 'twilio_token',
90
+ 'user_session',
91
+ 'voterid',
92
+ 'x-auth-token',
93
+ 'x_api_key',
94
+ 'x_csrftoken',
95
+ 'x_forwarded_for',
96
+ 'x_real_ip',
97
+ 'XSRF-TOKEN',
98
+ ...config.dynamicInstrumentation.redactedIdentifiers
99
+ ]
100
+ .map((name) => normalizeName(name))
101
+ .filter((name) => excludedIdentifiers.includes(name) === false)
102
+ )
103
+
104
+ function normalizeName (name, isSymbol) {
105
+ if (isSymbol) name = name.slice(7, -1) // Remove `Symbol(` and `)`
106
+ return name.toLowerCase().replace(/[-_@$.]/g, '')
107
+ }
108
+
109
+ module.exports = {
110
+ REDACTED_IDENTIFIERS,
111
+ normalizeName
112
+ }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const LRUCache = require('lru-cache')
3
+ const TTLSet = require('ttl-set')
4
4
  const config = require('./config')
5
5
  const JSONBuffer = require('./json-buffer')
6
6
  const request = require('../../exporters/common/request')
@@ -18,13 +18,7 @@ const ddsource = 'dd_debugger'
18
18
  const service = config.service
19
19
  const runtimeId = config.runtimeId
20
20
 
21
- const cache = new LRUCache({
22
- ttl: 1000 * 60 * 60, // 1 hour
23
- // Unfortunate requirement when using LRUCache:
24
- // It will emit a warning unless `ttlAutopurge`, `max`, or `maxSize` is set when using `ttl`.
25
- // TODO: Consider alternative as this is NOT performant :(
26
- ttlAutopurge: true
27
- })
21
+ const cache = new TTLSet(60 * 60 * 1000) // 1 hour
28
22
 
29
23
  const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush })
30
24
 
@@ -37,6 +31,8 @@ const STATUSES = {
37
31
  }
38
32
 
39
33
  function ackReceived ({ id: probeId, version }) {
34
+ log.debug('[debugger:devtools_client] Queueing RECEIVED status for probe %s (version: %d)', probeId, version)
35
+
40
36
  onlyUniqueUpdates(
41
37
  STATUSES.RECEIVED, probeId, version,
42
38
  () => send(statusPayload(probeId, version, STATUSES.RECEIVED))
@@ -44,6 +40,8 @@ function ackReceived ({ id: probeId, version }) {
44
40
  }
45
41
 
46
42
  function ackInstalled ({ id: probeId, version }) {
43
+ log.debug('[debugger:devtools_client] Queueing INSTALLED status for probe %s (version: %d)', probeId, version)
44
+
47
45
  onlyUniqueUpdates(
48
46
  STATUSES.INSTALLED, probeId, version,
49
47
  () => send(statusPayload(probeId, version, STATUSES.INSTALLED))
@@ -51,6 +49,8 @@ function ackInstalled ({ id: probeId, version }) {
51
49
  }
52
50
 
53
51
  function ackEmitting ({ id: probeId, version }) {
52
+ log.debug('[debugger:devtools_client] Queueing EMITTING status for probe %s (version: %d)', probeId, version)
53
+
54
54
  onlyUniqueUpdates(
55
55
  STATUSES.EMITTING, probeId, version,
56
56
  () => send(statusPayload(probeId, version, STATUSES.EMITTING))
@@ -78,6 +78,8 @@ function send (payload) {
78
78
  }
79
79
 
80
80
  function onFlush (payload) {
81
+ log.debug('[debugger:devtools_client] Flushing diagnostics payload buffer')
82
+
81
83
  const form = new FormData()
82
84
 
83
85
  form.append(
@@ -94,7 +96,7 @@ function onFlush (payload) {
94
96
  }
95
97
 
96
98
  request(form, options, (err) => {
97
- if (err) log.error('[debugger:devtools_client] Error sending probe payload', err)
99
+ if (err) log.error('[debugger:devtools_client] Error sending diagnostics payload', err)
98
100
  })
99
101
  }
100
102
 
@@ -112,5 +114,5 @@ function onlyUniqueUpdates (type, id, version, fn) {
112
114
  const key = `${type}-${id}-${version}`
113
115
  if (cache.has(key)) return
114
116
  fn()
115
- cache.set(key)
117
+ cache.add(key)
116
118
  }
@@ -48,7 +48,7 @@ function start (config, rc) {
48
48
  execArgv: [], // Avoid worker thread inheriting the `-r` command line argument
49
49
  env, // Avoid worker thread inheriting the `NODE_OPTIONS` environment variable (in case it contains `-r`)
50
50
  workerData: {
51
- config: serializableConfig(config),
51
+ config: config.serialize(),
52
52
  parentThreadId,
53
53
  rcPort: rcChannel.port1,
54
54
  configPort: configChannel.port1
@@ -88,16 +88,5 @@ function start (config, rc) {
88
88
 
89
89
  function configure (config) {
90
90
  if (configChannel === null) return
91
- configChannel.port2.postMessage(serializableConfig(config))
92
- }
93
-
94
- // TODO: Refactor the Config class so it never produces any config objects that are incompatible with MessageChannel
95
- function serializableConfig (config) {
96
- // URL objects cannot be serialized over the MessageChannel, so we need to convert them to strings first
97
- if (config.url instanceof URL) {
98
- config = { ...config }
99
- config.url = config.url.toString()
100
- }
101
-
102
- return config
91
+ configChannel.port2.postMessage(config.serialize())
103
92
  }
@@ -1,12 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const log = require('../../log')
4
- const { storage } = require('../storage')
4
+ const { storage: llmobsStorage } = require('../storage')
5
5
 
6
6
  const TracingPlugin = require('../../plugins/tracing')
7
7
  const LLMObsTagger = require('../tagger')
8
8
 
9
- // we make this a `Plugin` so we don't have to worry about `finish` being called
10
9
  class LLMObsPlugin extends TracingPlugin {
11
10
  constructor (...args) {
12
11
  super(...args)
@@ -14,24 +13,48 @@ class LLMObsPlugin extends TracingPlugin {
14
13
  this._tagger = new LLMObsTagger(this._tracerConfig, true)
15
14
  }
16
15
 
17
- getName () {}
18
-
19
16
  setLLMObsTags (ctx) {
20
17
  throw new Error('setLLMObsTags must be implemented by the subclass')
21
18
  }
22
19
 
23
- getLLMObsSPanRegisterOptions (ctx) {
20
+ getLLMObsSpanRegisterOptions (ctx) {
24
21
  throw new Error('getLLMObsSPanRegisterOptions must be implemented by the subclass')
25
22
  }
26
23
 
27
24
  start (ctx) {
28
- const oldStore = storage.getStore()
29
- const parent = oldStore?.span
30
- const span = ctx.currentStore?.span
25
+ // even though llmobs span events won't be enqueued if llmobs is disabled
26
+ // we should avoid doing any computations here (these listeners aren't disabled)
27
+ const enabled = this._tracerConfig.llmobs.enabled
28
+ if (!enabled) return
29
+
30
+ const parent = this.getLLMObsParent(ctx)
31
+ const apmStore = ctx.currentStore
32
+ const span = apmStore?.span
33
+
34
+ const registerOptions = this.getLLMObsSpanRegisterOptions(ctx)
35
+
36
+ // register options may not be set for operations we do not trace with llmobs
37
+ // ie OpenAI fine tuning jobs, file jobs, etc.
38
+ if (registerOptions) {
39
+ ctx.llmobs = {} // initialize context-based namespace
40
+ llmobsStorage.enterWith({ span })
41
+ ctx.llmobs.parent = parent
31
42
 
32
- const registerOptions = this.getLLMObsSPanRegisterOptions(ctx)
43
+ this._tagger.registerLLMObsSpan(span, { parent, ...registerOptions })
44
+ }
45
+ }
46
+
47
+ end (ctx) {
48
+ const enabled = this._tracerConfig.llmobs.enabled
49
+ if (!enabled) return
50
+
51
+ // only attempt to restore the context if the current span was an LLMObs span
52
+ const apmStore = ctx.currentStore
53
+ const span = apmStore?.span
54
+ if (!LLMObsTagger.tagMap.has(span)) return
33
55
 
34
- this._tagger.registerLLMObsSpan(span, { parent, ...registerOptions })
56
+ const parent = ctx.llmobs.parent
57
+ llmobsStorage.enterWith({ span: parent })
35
58
  }
36
59
 
37
60
  asyncEnd (ctx) {
@@ -40,7 +63,8 @@ class LLMObsPlugin extends TracingPlugin {
40
63
  const enabled = this._tracerConfig.llmobs.enabled
41
64
  if (!enabled) return
42
65
 
43
- const span = ctx.currentStore?.span
66
+ const apmStore = ctx.currentStore
67
+ const span = apmStore?.span
44
68
  if (!span) {
45
69
  log.debug(
46
70
  `Tried to start an LLMObs span for ${this.constructor.name} without an active APM span.
@@ -60,6 +84,11 @@ class LLMObsPlugin extends TracingPlugin {
60
84
  }
61
85
  super.configure(config)
62
86
  }
87
+
88
+ getLLMObsParent () {
89
+ const store = llmobsStorage.getStore()
90
+ return store?.span
91
+ }
63
92
  }
64
93
 
65
94
  module.exports = LLMObsPlugin
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+ const LangChainLLMObsHandler = require('.')
4
+ const { spanHasError } = require('../../../util')
5
+
6
+ class LangChainLLMObsChainHandler extends LangChainLLMObsHandler {
7
+ setMetaTags ({ span, inputs, results }) {
8
+ let input, output
9
+ if (inputs) {
10
+ input = this.formatIO(inputs)
11
+ }
12
+
13
+ if (!results || spanHasError(span)) {
14
+ output = ''
15
+ } else {
16
+ output = this.formatIO(results)
17
+ }
18
+
19
+ // chain spans will always be workflows
20
+ this._tagger.tagTextIO(span, input, output)
21
+ }
22
+ }
23
+
24
+ module.exports = LangChainLLMObsChainHandler
@@ -0,0 +1,111 @@
1
+ 'use strict'
2
+
3
+ const LangChainLLMObsHandler = require('.')
4
+ const LLMObsTagger = require('../../../tagger')
5
+ const { spanHasError } = require('../../../util')
6
+
7
+ const LLM = 'llm'
8
+
9
+ class LangChainLLMObsChatModelHandler extends LangChainLLMObsHandler {
10
+ setMetaTags ({ span, inputs, results, options, integrationName }) {
11
+ if (integrationName === 'openai' && options?.response_format) {
12
+ // langchain-openai will call a beta client if "response_format" is passed in on the options object
13
+ // we do not trace these calls, so this should be an llm span
14
+ this._tagger.changeKind(span, LLM)
15
+ }
16
+ const spanKind = LLMObsTagger.getSpanKind(span)
17
+ const isWorkflow = spanKind === 'workflow'
18
+
19
+ const inputMessages = []
20
+ if (!Array.isArray(inputs)) inputs = [inputs]
21
+
22
+ for (const messageSet of inputs) {
23
+ for (const message of messageSet) {
24
+ const content = message.content || ''
25
+ const role = this.getRole(message)
26
+ inputMessages.push({ content, role })
27
+ }
28
+ }
29
+
30
+ if (spanHasError(span)) {
31
+ if (isWorkflow) {
32
+ this._tagger.tagTextIO(span, inputMessages, [{ content: '' }])
33
+ } else {
34
+ this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
35
+ }
36
+ return
37
+ }
38
+
39
+ const outputMessages = []
40
+ let inputTokens = 0
41
+ let outputTokens = 0
42
+ let totalTokens = 0
43
+ let tokensSetTopLevel = false
44
+ const tokensPerRunId = {}
45
+
46
+ if (!isWorkflow) {
47
+ const tokens = this.checkTokenUsageChatOrLLMResult(results)
48
+ inputTokens = tokens.inputTokens
49
+ outputTokens = tokens.outputTokens
50
+ totalTokens = tokens.totalTokens
51
+ tokensSetTopLevel = totalTokens > 0
52
+ }
53
+
54
+ for (const messageSet of results.generations) {
55
+ for (const chatCompletion of messageSet) {
56
+ const chatCompletionMessage = chatCompletion.message
57
+ const role = this.getRole(chatCompletionMessage)
58
+ const content = chatCompletionMessage.text || ''
59
+ const toolCalls = this.extractToolCalls(chatCompletionMessage)
60
+ outputMessages.push({ content, role, toolCalls })
61
+
62
+ if (!isWorkflow && !tokensSetTopLevel) {
63
+ const { tokens, runId } = this.checkTokenUsageFromAIMessage(chatCompletionMessage)
64
+ if (!tokensPerRunId[runId]) {
65
+ tokensPerRunId[runId] = tokens
66
+ } else {
67
+ tokensPerRunId[runId].inputTokens += tokens.inputTokens
68
+ tokensPerRunId[runId].outputTokens += tokens.outputTokens
69
+ tokensPerRunId[runId].totalTokens += tokens.totalTokens
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ if (!isWorkflow && !tokensSetTopLevel) {
76
+ inputTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.inputTokens, 0)
77
+ outputTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.outputTokens, 0)
78
+ totalTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.totalTokens, 0)
79
+ }
80
+
81
+ if (isWorkflow) {
82
+ this._tagger.tagTextIO(span, inputMessages, outputMessages)
83
+ } else {
84
+ this._tagger.tagLLMIO(span, inputMessages, outputMessages)
85
+ this._tagger.tagMetrics(span, {
86
+ inputTokens,
87
+ outputTokens,
88
+ totalTokens
89
+ })
90
+ }
91
+ }
92
+
93
+ extractToolCalls (message) {
94
+ let toolCalls = message.tool_calls
95
+ if (!toolCalls) return []
96
+
97
+ const toolCallsInfo = []
98
+ if (!Array.isArray(toolCalls)) toolCalls = [toolCalls]
99
+ for (const toolCall of toolCalls) {
100
+ toolCallsInfo.push({
101
+ name: toolCall.name || '',
102
+ arguments: toolCall.args || {},
103
+ tool_id: toolCall.id || ''
104
+ })
105
+ }
106
+
107
+ return toolCallsInfo
108
+ }
109
+ }
110
+
111
+ module.exports = LangChainLLMObsChatModelHandler