dd-trace 5.30.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 (74) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/README.md +9 -7
  3. package/package.json +7 -6
  4. package/packages/datadog-core/src/storage.js +11 -2
  5. package/packages/datadog-instrumentations/src/aerospike.js +1 -1
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
  7. package/packages/datadog-instrumentations/src/cucumber.js +14 -5
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  9. package/packages/datadog-instrumentations/src/jest.js +70 -36
  10. package/packages/datadog-instrumentations/src/mocha/utils.js +23 -7
  11. package/packages/datadog-instrumentations/src/node-serialize.js +22 -0
  12. package/packages/datadog-instrumentations/src/openai.js +2 -0
  13. package/packages/datadog-instrumentations/src/vitest.js +107 -59
  14. package/packages/datadog-instrumentations/src/vm.js +49 -0
  15. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime.js +295 -0
  16. package/packages/datadog-plugin-aws-sdk/src/services/index.js +1 -0
  17. package/packages/datadog-plugin-cucumber/src/index.js +30 -32
  18. package/packages/datadog-plugin-jest/src/index.js +34 -37
  19. package/packages/datadog-plugin-langchain/src/index.js +12 -80
  20. package/packages/datadog-plugin-langchain/src/tracing.js +89 -0
  21. package/packages/datadog-plugin-mocha/src/index.js +18 -36
  22. package/packages/datadog-plugin-vitest/src/index.js +20 -34
  23. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  24. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +2 -0
  25. package/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js +16 -0
  26. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +9 -8
  27. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  28. package/packages/dd-trace/src/appsec/remote_config/manager.js +11 -1
  29. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +37 -0
  30. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +65 -28
  31. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +57 -17
  32. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +18 -3
  33. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +20 -3
  34. package/packages/dd-trace/src/config.js +39 -3
  35. package/packages/dd-trace/src/crashtracking/crashtracker.js +9 -0
  36. package/packages/dd-trace/src/crashtracking/noop.js +3 -0
  37. package/packages/dd-trace/src/datastreams/fnv.js +1 -1
  38. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -2
  39. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/defaults.js +1 -0
  41. package/packages/dd-trace/src/debugger/devtools_client/index.js +32 -14
  42. package/packages/dd-trace/src/debugger/devtools_client/json-buffer.js +36 -0
  43. package/packages/dd-trace/src/debugger/devtools_client/send.js +29 -10
  44. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +35 -1
  45. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +112 -0
  46. package/packages/dd-trace/src/debugger/devtools_client/status.js +20 -11
  47. package/packages/dd-trace/src/debugger/index.js +2 -13
  48. package/packages/dd-trace/src/llmobs/plugins/base.js +40 -11
  49. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +24 -0
  50. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +111 -0
  51. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +42 -0
  52. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +102 -0
  53. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js +32 -0
  54. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +131 -0
  55. package/packages/dd-trace/src/llmobs/plugins/openai.js +1 -1
  56. package/packages/dd-trace/src/llmobs/sdk.js +90 -26
  57. package/packages/dd-trace/src/llmobs/tagger.js +11 -3
  58. package/packages/dd-trace/src/llmobs/util.js +7 -1
  59. package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +3 -3
  60. package/packages/dd-trace/src/log/index.js +8 -9
  61. package/packages/dd-trace/src/noop/proxy.js +2 -2
  62. package/packages/dd-trace/src/noop/span.js +1 -1
  63. package/packages/dd-trace/src/opentelemetry/context_manager.js +43 -3
  64. package/packages/dd-trace/src/opentracing/span.js +11 -1
  65. package/packages/dd-trace/src/opentracing/span_context.js +12 -0
  66. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -27
  67. package/packages/dd-trace/src/plugins/util/test.js +42 -12
  68. package/packages/dd-trace/src/priority_sampler.js +7 -2
  69. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +21 -0
  70. package/packages/dd-trace/src/profiling/profiler.js +11 -8
  71. package/packages/dd-trace/src/profiling/profilers/events.js +17 -1
  72. package/packages/dd-trace/src/proxy.js +6 -3
  73. package/packages/dd-trace/src/scope.js +1 -1
  74. package/packages/dd-trace/src/telemetry/index.js +2 -0
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
+ MAX_SNAPSHOTS_PER_SECOND_GLOBALLY: 25,
4
5
  MAX_SNAPSHOTS_PER_SECOND_PER_PROBE: 1,
5
6
  MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE: 5_000
6
7
  }
@@ -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,11 +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, (err) => {
133
- if (err) log.error('Debugger error', err)
134
- else ackEmitting(probe)
135
- })
151
+ send(probe.template, logger, dd, snapshot)
136
152
  }
137
153
  })
138
154
 
@@ -141,6 +157,8 @@ function highestOrUndefined (num, max) {
141
157
  }
142
158
 
143
159
  async function getDD (callFrameId) {
160
+ // TODO: Consider if an `objectGroup` should be used, so it can be explicitly released using
161
+ // `Runtime.releaseObjectGroup`
144
162
  const { result } = await session.post('Debugger.evaluateOnCallFrame', {
145
163
  callFrameId,
146
164
  expression,
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ class JSONBuffer {
4
+ constructor ({ size, timeout, onFlush }) {
5
+ this._maxSize = size
6
+ this._timeout = timeout
7
+ this._onFlush = onFlush
8
+ this._reset()
9
+ }
10
+
11
+ _reset () {
12
+ clearTimeout(this._timer)
13
+ this._timer = null
14
+ this._partialJson = null
15
+ }
16
+
17
+ _flush () {
18
+ const json = `${this._partialJson}]`
19
+ this._reset()
20
+ this._onFlush(json)
21
+ }
22
+
23
+ write (str, size = Buffer.byteLength(str)) {
24
+ if (this._timer === null) {
25
+ this._partialJson = `[${str}`
26
+ this._timer = setTimeout(() => this._flush(), this._timeout)
27
+ } else if (Buffer.byteLength(this._partialJson) + size + 2 > this._maxSize) {
28
+ this._flush()
29
+ this.write(str, size)
30
+ } else {
31
+ this._partialJson += `,${str}`
32
+ }
33
+ }
34
+ }
35
+
36
+ module.exports = JSONBuffer
@@ -4,32 +4,34 @@ const { hostname: getHostname } = require('os')
4
4
  const { stringify } = require('querystring')
5
5
 
6
6
  const config = require('./config')
7
+ const JSONBuffer = require('./json-buffer')
7
8
  const request = require('../../exporters/common/request')
8
9
  const { GIT_COMMIT_SHA, GIT_REPOSITORY_URL } = require('../../plugins/util/tags')
10
+ const log = require('../../log')
11
+ const { version } = require('../../../../../package.json')
9
12
 
10
13
  module.exports = send
11
14
 
12
- const MAX_PAYLOAD_SIZE = 1024 * 1024 // 1MB
15
+ const MAX_LOG_PAYLOAD_SIZE = 1024 * 1024 // 1MB
13
16
 
14
17
  const ddsource = 'dd_debugger'
15
18
  const hostname = getHostname()
16
19
  const service = config.service
17
20
 
18
21
  const ddtags = [
22
+ ['env', process.env.DD_ENV],
23
+ ['version', process.env.DD_VERSION],
24
+ ['debugger_version', version],
25
+ ['host_name', hostname],
19
26
  [GIT_COMMIT_SHA, config.commitSHA],
20
27
  [GIT_REPOSITORY_URL, config.repositoryUrl]
21
28
  ].map((pair) => pair.join(':')).join(',')
22
29
 
23
30
  const path = `/debugger/v1/input?${stringify({ ddtags })}`
24
31
 
25
- function send (message, logger, dd, snapshot, cb) {
26
- const opts = {
27
- method: 'POST',
28
- url: config.url,
29
- path,
30
- headers: { 'Content-Type': 'application/json; charset=utf-8' }
31
- }
32
+ const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush })
32
33
 
34
+ function send (message, logger, dd, snapshot) {
33
35
  const payload = {
34
36
  ddsource,
35
37
  hostname,
@@ -41,8 +43,9 @@ function send (message, logger, dd, snapshot, cb) {
41
43
  }
42
44
 
43
45
  let json = JSON.stringify(payload)
46
+ let size = Buffer.byteLength(json)
44
47
 
45
- if (Buffer.byteLength(json) > MAX_PAYLOAD_SIZE) {
48
+ if (size > MAX_LOG_PAYLOAD_SIZE) {
46
49
  // TODO: This is a very crude way to handle large payloads. Proper pruning will be implemented later (DEBUG-2624)
47
50
  const line = Object.values(payload['debugger.snapshot'].captures.lines)[0]
48
51
  line.locals = {
@@ -50,7 +53,23 @@ function send (message, logger, dd, snapshot, cb) {
50
53
  size: Object.keys(line.locals).length
51
54
  }
52
55
  json = JSON.stringify(payload)
56
+ size = Buffer.byteLength(json)
57
+ }
58
+
59
+ jsonBuffer.write(json, size)
60
+ }
61
+
62
+ function onFlush (payload) {
63
+ log.debug('[debugger:devtools_client] Flushing probe payload buffer')
64
+
65
+ const opts = {
66
+ method: 'POST',
67
+ url: config.url,
68
+ path,
69
+ headers: { 'Content-Type': 'application/json; charset=utf-8' }
53
70
  }
54
71
 
55
- request(json, opts, cb)
72
+ request(payload, opts, (err) => {
73
+ if (err) log.error('[debugger:devtools_client] Error sending probe payload', err)
74
+ })
56
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,7 +1,8 @@
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
+ const JSONBuffer = require('./json-buffer')
5
6
  const request = require('../../exporters/common/request')
6
7
  const FormData = require('../../exporters/common/form-data')
7
8
  const log = require('../../log')
@@ -17,13 +18,9 @@ const ddsource = 'dd_debugger'
17
18
  const service = config.service
18
19
  const runtimeId = config.runtimeId
19
20
 
20
- const cache = new LRUCache({
21
- ttl: 1000 * 60 * 60, // 1 hour
22
- // Unfortunate requirement when using LRUCache:
23
- // It will emit a warning unless `ttlAutopurge`, `max`, or `maxSize` is set when using `ttl`.
24
- // TODO: Consider alternative as this is NOT performant :(
25
- ttlAutopurge: true
26
- })
21
+ const cache = new TTLSet(60 * 60 * 1000) // 1 hour
22
+
23
+ const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush })
27
24
 
28
25
  const STATUSES = {
29
26
  RECEIVED: 'RECEIVED',
@@ -34,6 +31,8 @@ const STATUSES = {
34
31
  }
35
32
 
36
33
  function ackReceived ({ id: probeId, version }) {
34
+ log.debug('[debugger:devtools_client] Queueing RECEIVED status for probe %s (version: %d)', probeId, version)
35
+
37
36
  onlyUniqueUpdates(
38
37
  STATUSES.RECEIVED, probeId, version,
39
38
  () => send(statusPayload(probeId, version, STATUSES.RECEIVED))
@@ -41,6 +40,8 @@ function ackReceived ({ id: probeId, version }) {
41
40
  }
42
41
 
43
42
  function ackInstalled ({ id: probeId, version }) {
43
+ log.debug('[debugger:devtools_client] Queueing INSTALLED status for probe %s (version: %d)', probeId, version)
44
+
44
45
  onlyUniqueUpdates(
45
46
  STATUSES.INSTALLED, probeId, version,
46
47
  () => send(statusPayload(probeId, version, STATUSES.INSTALLED))
@@ -48,6 +49,8 @@ function ackInstalled ({ id: probeId, version }) {
48
49
  }
49
50
 
50
51
  function ackEmitting ({ id: probeId, version }) {
52
+ log.debug('[debugger:devtools_client] Queueing EMITTING status for probe %s (version: %d)', probeId, version)
53
+
51
54
  onlyUniqueUpdates(
52
55
  STATUSES.EMITTING, probeId, version,
53
56
  () => send(statusPayload(probeId, version, STATUSES.EMITTING))
@@ -71,11 +74,17 @@ function ackError (err, { id: probeId, version }) {
71
74
  }
72
75
 
73
76
  function send (payload) {
77
+ jsonBuffer.write(JSON.stringify(payload))
78
+ }
79
+
80
+ function onFlush (payload) {
81
+ log.debug('[debugger:devtools_client] Flushing diagnostics payload buffer')
82
+
74
83
  const form = new FormData()
75
84
 
76
85
  form.append(
77
86
  'event',
78
- JSON.stringify(payload),
87
+ payload,
79
88
  { filename: 'event.json', contentType: 'application/json; charset=utf-8' }
80
89
  )
81
90
 
@@ -87,7 +96,7 @@ function send (payload) {
87
96
  }
88
97
 
89
98
  request(form, options, (err) => {
90
- if (err) log.error('[debugger:devtools_client] Error sending debugger payload', err)
99
+ if (err) log.error('[debugger:devtools_client] Error sending diagnostics payload', err)
91
100
  })
92
101
  }
93
102
 
@@ -105,5 +114,5 @@ function onlyUniqueUpdates (type, id, version, fn) {
105
114
  const key = `${type}-${id}-${version}`
106
115
  if (cache.has(key)) return
107
116
  fn()
108
- cache.set(key)
117
+ cache.add(key)
109
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