dd-trace 5.25.0 → 5.27.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 +2 -0
  2. package/index.d.ts +17 -8
  3. package/init.js +60 -47
  4. package/package.json +5 -2
  5. package/packages/datadog-core/index.js +1 -3
  6. package/packages/datadog-core/src/storage.js +21 -0
  7. package/packages/datadog-instrumentations/src/express.js +1 -1
  8. package/packages/datadog-instrumentations/src/handlebars.js +40 -0
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +5 -0
  10. package/packages/datadog-instrumentations/src/jest.js +6 -2
  11. package/packages/datadog-instrumentations/src/langchain.js +77 -0
  12. package/packages/datadog-instrumentations/src/next.js +19 -7
  13. package/packages/datadog-instrumentations/src/pug.js +23 -0
  14. package/packages/datadog-instrumentations/src/router.js +2 -3
  15. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -0
  16. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +7 -6
  17. package/packages/datadog-plugin-aws-sdk/src/services/s3.js +34 -0
  18. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +8 -8
  19. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +59 -45
  20. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  21. package/packages/datadog-plugin-http/src/client.js +42 -1
  22. package/packages/datadog-plugin-http2/src/client.js +26 -1
  23. package/packages/datadog-plugin-langchain/src/handlers/chain.js +50 -0
  24. package/packages/datadog-plugin-langchain/src/handlers/default.js +53 -0
  25. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +63 -0
  26. package/packages/datadog-plugin-langchain/src/handlers/language_models/chat_model.js +99 -0
  27. package/packages/datadog-plugin-langchain/src/handlers/language_models/index.js +48 -0
  28. package/packages/datadog-plugin-langchain/src/handlers/language_models/llm.js +57 -0
  29. package/packages/datadog-plugin-langchain/src/index.js +89 -0
  30. package/packages/datadog-plugin-langchain/src/tokens.js +35 -0
  31. package/packages/datadog-plugin-mocha/src/index.js +1 -1
  32. package/packages/datadog-plugin-moleculer/src/server.js +0 -1
  33. package/packages/dd-trace/src/appsec/api_security_sampler.js +50 -27
  34. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  35. package/packages/dd-trace/src/appsec/iast/analyzers/header-injection-analyzer.js +33 -16
  36. package/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js +18 -0
  37. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -2
  38. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  39. package/packages/dd-trace/src/appsec/index.js +6 -6
  40. package/packages/dd-trace/src/appsec/recommended.json +353 -155
  41. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +1 -1
  42. package/packages/dd-trace/src/appsec/remote_config/index.js +0 -7
  43. package/packages/dd-trace/src/appsec/reporter.js +1 -0
  44. package/packages/dd-trace/src/appsec/sdk/utils.js +21 -2
  45. package/packages/dd-trace/src/config.js +21 -4
  46. package/packages/dd-trace/src/constants.js +6 -1
  47. package/packages/dd-trace/src/crashtracking/crashtracker.js +98 -0
  48. package/packages/dd-trace/src/crashtracking/index.js +15 -0
  49. package/packages/dd-trace/src/crashtracking/noop.js +8 -0
  50. package/packages/dd-trace/src/llmobs/sdk.js +1 -1
  51. package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
  52. package/packages/dd-trace/src/llmobs/writers/spans/base.js +3 -0
  53. package/packages/dd-trace/src/log/index.js +10 -13
  54. package/packages/dd-trace/src/log/log.js +52 -0
  55. package/packages/dd-trace/src/log/writer.js +50 -19
  56. package/packages/dd-trace/src/noop/span.js +1 -0
  57. package/packages/dd-trace/src/opentelemetry/span.js +15 -0
  58. package/packages/dd-trace/src/opentracing/propagation/text_map.js +35 -22
  59. package/packages/dd-trace/src/opentracing/span.js +14 -0
  60. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  61. package/packages/dd-trace/src/plugins/index.js +3 -0
  62. package/packages/dd-trace/src/plugins/tracing.js +2 -2
  63. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +121 -0
  64. package/packages/dd-trace/src/plugins/util/ip_extractor.js +0 -1
  65. package/packages/dd-trace/src/plugins/util/web.js +39 -11
  66. package/packages/dd-trace/src/profiling/exporters/agent.js +42 -5
  67. package/packages/dd-trace/src/profiling/profiler.js +5 -2
  68. package/packages/dd-trace/src/proxy.js +5 -0
  69. package/packages/dd-trace/src/telemetry/logs/index.js +16 -11
  70. package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -8
  71. package/packages/dd-trace/src/telemetry/metrics.js +6 -1
  72. package/packages/dd-trace/src/util.js +16 -1
  73. package/version.js +4 -2
  74. /package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/{code-injection-sensitive-analyzer.js → tainted-range-based-sensitive-analyzer.js} +0 -0
@@ -1,6 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const BaseAwsSdkPlugin = require('../base')
4
+ const log = require('../../../dd-trace/src/log')
5
+ const { generatePointerHash } = require('../../../dd-trace/src/util')
6
+ const { S3_PTR_KIND, SPAN_POINTER_DIRECTION } = require('../../../dd-trace/src/constants')
4
7
 
5
8
  class S3 extends BaseAwsSdkPlugin {
6
9
  static get id () { return 's3' }
@@ -18,6 +21,37 @@ class S3 extends BaseAwsSdkPlugin {
18
21
  bucketname: params.Bucket
19
22
  })
20
23
  }
24
+
25
+ addSpanPointers (span, response) {
26
+ const request = response?.request
27
+ const operationName = request?.operation
28
+ if (!['putObject', 'copyObject', 'completeMultipartUpload'].includes(operationName)) {
29
+ // We don't create span links for other S3 operations.
30
+ return
31
+ }
32
+
33
+ // AWS v2: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html
34
+ // AWS v3: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/
35
+ const bucketName = request?.params?.Bucket
36
+ const objectKey = request?.params?.Key
37
+ let eTag =
38
+ response?.ETag || // v3 PutObject & CompleteMultipartUpload
39
+ response?.CopyObjectResult?.ETag || // v3 CopyObject
40
+ response?.data?.ETag || // v2 PutObject & CompleteMultipartUpload
41
+ response?.data?.CopyObjectResult?.ETag // v2 CopyObject
42
+
43
+ if (!bucketName || !objectKey || !eTag) {
44
+ log.debug('Unable to calculate span pointer hash because of missing parameters.')
45
+ return
46
+ }
47
+
48
+ // https://github.com/DataDog/dd-span-pointer-rules/blob/main/AWS/S3/Object/README.md
49
+ if (eTag.startsWith('"') && eTag.endsWith('"')) {
50
+ eTag = eTag.slice(1, -1)
51
+ }
52
+ const pointerHash = generatePointerHash([bucketName, objectKey, eTag])
53
+ span.addSpanPointer(S3_PTR_KIND, SPAN_POINTER_DIRECTION.DOWNSTREAM, pointerHash)
54
+ }
21
55
  }
22
56
 
23
57
  module.exports = S3
@@ -42,7 +42,7 @@ class Sqs extends BaseAwsSdkPlugin {
42
42
  // extract DSM context after as we might not have a parent-child but may have a DSM context
43
43
 
44
44
  this.responseExtractDSMContext(
45
- request.operation, request.params, response, span || null, { parsedMessageAttributes }
45
+ request.operation, request.params, response, span || null, { parsedAttributes: parsedMessageAttributes }
46
46
  )
47
47
  })
48
48
 
@@ -195,16 +195,16 @@ class Sqs extends BaseAwsSdkPlugin {
195
195
  parsedAttributes = this.parseDatadogAttributes(message.MessageAttributes._datadog)
196
196
  }
197
197
  }
198
+ const payloadSize = getHeadersSize({
199
+ Body: message.Body,
200
+ MessageAttributes: message.MessageAttributes
201
+ })
202
+ const queue = params.QueueUrl.split('/').pop()
198
203
  if (parsedAttributes) {
199
- const payloadSize = getHeadersSize({
200
- Body: message.Body,
201
- MessageAttributes: message.MessageAttributes
202
- })
203
- const queue = params.QueueUrl.split('/').pop()
204
204
  this.tracer.decodeDataStreamsContext(parsedAttributes)
205
- this.tracer
206
- .setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize)
207
205
  }
206
+ this.tracer
207
+ .setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize)
208
208
  })
209
209
  }
210
210
 
@@ -654,55 +654,69 @@ class CypressPlugin {
654
654
  return this.activeTestSpan ? { traceId: this.activeTestSpan.context().toTraceId() } : {}
655
655
  },
656
656
  'dd:afterEach': ({ test, coverage }) => {
657
- const { state, error, isRUMActive, testSourceLine, testSuite, testName, isNew, isEfdRetry } = test
658
- if (this.activeTestSpan) {
659
- if (coverage && this.isCodeCoverageEnabled && this.tracer._tracer._exporter?.exportCoverage) {
660
- const coverageFiles = getCoveredFilenamesFromCoverage(coverage)
661
- const relativeCoverageFiles = coverageFiles.map(file => getTestSuitePath(file, this.rootDir))
662
- if (!relativeCoverageFiles.length) {
663
- incrementCountMetric(TELEMETRY_CODE_COVERAGE_EMPTY)
664
- }
665
- distributionMetric(TELEMETRY_CODE_COVERAGE_NUM_FILES, {}, relativeCoverageFiles.length)
666
- const { _traceId, _spanId } = this.testSuiteSpan.context()
667
- const formattedCoverage = {
668
- sessionId: _traceId,
669
- suiteId: _spanId,
670
- testId: this.activeTestSpan.context()._spanId,
671
- files: relativeCoverageFiles
672
- }
673
- this.tracer._tracer._exporter.exportCoverage(formattedCoverage)
674
- }
675
- const testStatus = CYPRESS_STATUS_TO_TEST_STATUS[state]
676
- this.activeTestSpan.setTag(TEST_STATUS, testStatus)
677
-
678
- if (error) {
679
- this.activeTestSpan.setTag('error', error)
680
- }
681
- if (isRUMActive) {
682
- this.activeTestSpan.setTag(TEST_IS_RUM_ACTIVE, 'true')
683
- }
684
- if (testSourceLine) {
685
- this.activeTestSpan.setTag(TEST_SOURCE_START, testSourceLine)
686
- }
687
- if (isNew) {
688
- this.activeTestSpan.setTag(TEST_IS_NEW, 'true')
689
- if (isEfdRetry) {
690
- this.activeTestSpan.setTag(TEST_IS_RETRY, 'true')
691
- }
657
+ if (!this.activeTestSpan) {
658
+ log.warn('There is no active test span in dd:afterEach handler')
659
+ return null
660
+ }
661
+ const {
662
+ state,
663
+ error,
664
+ isRUMActive,
665
+ testSourceLine,
666
+ testSuite,
667
+ testSuiteAbsolutePath,
668
+ testName,
669
+ isNew,
670
+ isEfdRetry
671
+ } = test
672
+ if (coverage && this.isCodeCoverageEnabled && this.tracer._tracer._exporter?.exportCoverage) {
673
+ const coverageFiles = getCoveredFilenamesFromCoverage(coverage)
674
+ const relativeCoverageFiles = [...coverageFiles, testSuiteAbsolutePath].map(
675
+ file => getTestSuitePath(file, this.repositoryRoot || this.rootDir)
676
+ )
677
+ if (!relativeCoverageFiles.length) {
678
+ incrementCountMetric(TELEMETRY_CODE_COVERAGE_EMPTY)
692
679
  }
693
- const finishedTest = {
694
- testName,
695
- testStatus,
696
- finishTime: this.activeTestSpan._getTime(), // we store the finish time here
697
- testSpan: this.activeTestSpan
680
+ distributionMetric(TELEMETRY_CODE_COVERAGE_NUM_FILES, {}, relativeCoverageFiles.length)
681
+ const { _traceId, _spanId } = this.testSuiteSpan.context()
682
+ const formattedCoverage = {
683
+ sessionId: _traceId,
684
+ suiteId: _spanId,
685
+ testId: this.activeTestSpan.context()._spanId,
686
+ files: relativeCoverageFiles
698
687
  }
699
- if (this.finishedTestsByFile[testSuite]) {
700
- this.finishedTestsByFile[testSuite].push(finishedTest)
701
- } else {
702
- this.finishedTestsByFile[testSuite] = [finishedTest]
688
+ this.tracer._tracer._exporter.exportCoverage(formattedCoverage)
689
+ }
690
+ const testStatus = CYPRESS_STATUS_TO_TEST_STATUS[state]
691
+ this.activeTestSpan.setTag(TEST_STATUS, testStatus)
692
+
693
+ if (error) {
694
+ this.activeTestSpan.setTag('error', error)
695
+ }
696
+ if (isRUMActive) {
697
+ this.activeTestSpan.setTag(TEST_IS_RUM_ACTIVE, 'true')
698
+ }
699
+ if (testSourceLine) {
700
+ this.activeTestSpan.setTag(TEST_SOURCE_START, testSourceLine)
701
+ }
702
+ if (isNew) {
703
+ this.activeTestSpan.setTag(TEST_IS_NEW, 'true')
704
+ if (isEfdRetry) {
705
+ this.activeTestSpan.setTag(TEST_IS_RETRY, 'true')
703
706
  }
704
- // test spans are finished at after:spec
705
707
  }
708
+ const finishedTest = {
709
+ testName,
710
+ testStatus,
711
+ finishTime: this.activeTestSpan._getTime(), // we store the finish time here
712
+ testSpan: this.activeTestSpan
713
+ }
714
+ if (this.finishedTestsByFile[testSuite]) {
715
+ this.finishedTestsByFile[testSuite].push(finishedTest)
716
+ } else {
717
+ this.finishedTestsByFile[testSuite] = [finishedTest]
718
+ }
719
+ // test spans are finished at after:spec
706
720
  this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
707
721
  hasCodeOwners: !!this.activeTestSpan.context()._tags[TEST_CODE_OWNERS],
708
722
  isNew,
@@ -88,6 +88,7 @@ afterEach(function () {
88
88
  const testInfo = {
89
89
  testName: currentTest.fullTitle(),
90
90
  testSuite: Cypress.mocha.getRootSuite().file,
91
+ testSuiteAbsolutePath: Cypress.spec && Cypress.spec.absolute,
91
92
  state: currentTest.state,
92
93
  error: currentTest.err,
93
94
  isNew: currentTest._ddIsNew,
@@ -58,7 +58,7 @@ class HttpClientPlugin extends ClientPlugin {
58
58
  span._spanContext._trace.record = false
59
59
  }
60
60
 
61
- if (this.config.propagationFilter(uri)) {
61
+ if (this.shouldInjectTraceHeaders(options, uri)) {
62
62
  this.tracer.inject(span, HTTP_HEADERS, options.headers)
63
63
  }
64
64
 
@@ -71,6 +71,18 @@ class HttpClientPlugin extends ClientPlugin {
71
71
  return message.currentStore
72
72
  }
73
73
 
74
+ shouldInjectTraceHeaders (options, uri) {
75
+ if (hasAmazonSignature(options) && !this.config.enablePropagationWithAmazonHeaders) {
76
+ return false
77
+ }
78
+
79
+ if (!this.config.propagationFilter(uri)) {
80
+ return false
81
+ }
82
+
83
+ return true
84
+ }
85
+
74
86
  bindAsyncStart ({ parentStore }) {
75
87
  return parentStore
76
88
  }
@@ -200,6 +212,31 @@ function getHooks (config) {
200
212
  return { request }
201
213
  }
202
214
 
215
+ function hasAmazonSignature (options) {
216
+ if (!options) {
217
+ return false
218
+ }
219
+
220
+ if (options.headers) {
221
+ const headers = Object.keys(options.headers)
222
+ .reduce((prev, next) => Object.assign(prev, {
223
+ [next.toLowerCase()]: options.headers[next]
224
+ }), {})
225
+
226
+ if (headers['x-amz-signature']) {
227
+ return true
228
+ }
229
+
230
+ if ([].concat(headers.authorization).some(startsWith('AWS4-HMAC-SHA256'))) {
231
+ return true
232
+ }
233
+ }
234
+
235
+ const search = options.search || options.path
236
+
237
+ return search && search.toLowerCase().indexOf('x-amz-signature=') !== -1
238
+ }
239
+
203
240
  function extractSessionDetails (options) {
204
241
  if (typeof options === 'string') {
205
242
  return new URL(options).host
@@ -211,4 +248,8 @@ function extractSessionDetails (options) {
211
248
  return { host, port }
212
249
  }
213
250
 
251
+ function startsWith (searchString) {
252
+ return value => String(value).startsWith(searchString)
253
+ }
254
+
214
255
  module.exports = HttpClientPlugin
@@ -62,7 +62,9 @@ class Http2ClientPlugin extends ClientPlugin {
62
62
 
63
63
  addHeaderTags(span, headers, HTTP_REQUEST_HEADERS, this.config)
64
64
 
65
- this.tracer.inject(span, HTTP_HEADERS, headers)
65
+ if (!hasAmazonSignature(headers, path)) {
66
+ this.tracer.inject(span, HTTP_HEADERS, headers)
67
+ }
66
68
 
67
69
  message.parentStore = store
68
70
  message.currentStore = { ...store, span }
@@ -132,6 +134,29 @@ function extractSessionDetails (authority, options) {
132
134
  return { protocol, port, host }
133
135
  }
134
136
 
137
+ function hasAmazonSignature (headers, path) {
138
+ if (headers) {
139
+ headers = Object.keys(headers)
140
+ .reduce((prev, next) => Object.assign(prev, {
141
+ [next.toLowerCase()]: headers[next]
142
+ }), {})
143
+
144
+ if (headers['x-amz-signature']) {
145
+ return true
146
+ }
147
+
148
+ if ([].concat(headers.authorization).some(startsWith('AWS4-HMAC-SHA256'))) {
149
+ return true
150
+ }
151
+ }
152
+
153
+ return path && path.toLowerCase().indexOf('x-amz-signature=') !== -1
154
+ }
155
+
156
+ function startsWith (searchString) {
157
+ return value => String(value).startsWith(searchString)
158
+ }
159
+
135
160
  function getStatusValidator (config) {
136
161
  if (typeof config.validateStatus === 'function') {
137
162
  return config.validateStatus
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ const LangChainHandler = require('./default')
4
+
5
+ class LangChainChainHandler extends LangChainHandler {
6
+ getSpanStartTags (ctx) {
7
+ const tags = {}
8
+
9
+ if (!this.isPromptCompletionSampled()) return tags
10
+
11
+ let inputs = ctx.args?.[0]
12
+ inputs = Array.isArray(inputs) ? inputs : [inputs]
13
+
14
+ for (const idx in inputs) {
15
+ const input = inputs[idx]
16
+ if (typeof input !== 'object') {
17
+ tags[`langchain.request.inputs.${idx}`] = this.normalize(input)
18
+ } else {
19
+ for (const [key, value] of Object.entries(input)) {
20
+ // these are mappings to the python client names, ie lc_kwargs
21
+ // only present on BaseMessage types
22
+ if (key.includes('lc_')) continue
23
+ tags[`langchain.request.inputs.${idx}.${key}`] = this.normalize(value)
24
+ }
25
+ }
26
+ }
27
+
28
+ return tags
29
+ }
30
+
31
+ getSpanEndTags (ctx) {
32
+ const tags = {}
33
+
34
+ if (!this.isPromptCompletionSampled()) return tags
35
+
36
+ let outputs = ctx.result
37
+ outputs = Array.isArray(outputs) ? outputs : [outputs]
38
+
39
+ for (const idx in outputs) {
40
+ const output = outputs[idx]
41
+ tags[`langchain.response.outputs.${idx}`] = this.normalize(
42
+ typeof output === 'string' ? output : JSON.stringify(output)
43
+ )
44
+ }
45
+
46
+ return tags
47
+ }
48
+ }
49
+
50
+ module.exports = LangChainChainHandler
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+
3
+ const Sampler = require('../../../dd-trace/src/sampler')
4
+
5
+ const RE_NEWLINE = /\n/g
6
+ const RE_TAB = /\t/g
7
+
8
+ // TODO: should probably refactor the OpenAI integration to use a shared LLMTracingPlugin base class
9
+ // This logic isn't particular to LangChain
10
+ class LangChainHandler {
11
+ constructor (config) {
12
+ this.config = config
13
+ this.sampler = new Sampler(config.spanPromptCompletionSampleRate)
14
+ }
15
+
16
+ // no-op for default handler
17
+ getSpanStartTags (ctx) {}
18
+
19
+ // no-op for default handler
20
+ getSpanEndTags (ctx) {}
21
+
22
+ // no-op for default handler
23
+ extractApiKey (instance) {}
24
+
25
+ // no-op for default handler
26
+ extractProvider (instance) {}
27
+
28
+ // no-op for default handler
29
+ extractModel (instance) {}
30
+
31
+ normalize (text) {
32
+ if (!text) return
33
+ if (typeof text !== 'string' || !text || (typeof text === 'string' && text.length === 0)) return
34
+
35
+ const max = this.config.spanCharLimit
36
+
37
+ text = text
38
+ .replace(RE_NEWLINE, '\\n')
39
+ .replace(RE_TAB, '\\t')
40
+
41
+ if (text.length > max) {
42
+ return text.substring(0, max) + '...'
43
+ }
44
+
45
+ return text
46
+ }
47
+
48
+ isPromptCompletionSampled () {
49
+ return this.sampler.isSampled()
50
+ }
51
+ }
52
+
53
+ module.exports = LangChainHandler
@@ -0,0 +1,63 @@
1
+ 'use strict'
2
+
3
+ const LangChainHandler = require('./default')
4
+
5
+ class LangChainEmbeddingHandler extends LangChainHandler {
6
+ getSpanStartTags (ctx) {
7
+ const tags = {}
8
+
9
+ const inputTexts = ctx.args?.[0]
10
+
11
+ const sampled = this.isPromptCompletionSampled()
12
+ if (typeof inputTexts === 'string') {
13
+ // embed query
14
+ if (sampled) {
15
+ tags['langchain.request.inputs.0.text'] = this.normalize(inputTexts)
16
+ }
17
+ tags['langchain.request.input_counts'] = 1
18
+ } else {
19
+ // embed documents
20
+ if (sampled) {
21
+ for (const idx in inputTexts) {
22
+ const inputText = inputTexts[idx]
23
+ tags[`langchain.request.inputs.${idx}.text`] = this.normalize(inputText)
24
+ }
25
+ }
26
+ tags['langchain.request.input_counts'] = inputTexts.length
27
+ }
28
+
29
+ return tags
30
+ }
31
+
32
+ getSpanEndTags (ctx) {
33
+ const tags = {}
34
+
35
+ const { result } = ctx
36
+ if (!Array.isArray(result)) return
37
+
38
+ tags['langchain.response.outputs.embedding_length'] = (
39
+ Array.isArray(result[0]) ? result[0] : result
40
+ ).length
41
+
42
+ return tags
43
+ }
44
+
45
+ extractApiKey (instance) {
46
+ const apiKey = instance.clientConfig?.apiKey
47
+ if (!apiKey || apiKey.length < 4) return ''
48
+ return `...${apiKey.slice(-4)}`
49
+ }
50
+
51
+ extractProvider (instance) {
52
+ return instance.constructor.name.split('Embeddings')[0].toLowerCase()
53
+ }
54
+
55
+ extractModel (instance) {
56
+ for (const attr of ['model', 'modelName', 'modelId', 'modelKey', 'repoId']) {
57
+ const modelName = instance[attr]
58
+ if (modelName) return modelName
59
+ }
60
+ }
61
+ }
62
+
63
+ module.exports = LangChainEmbeddingHandler
@@ -0,0 +1,99 @@
1
+ 'use strict'
2
+
3
+ const LangChainLanguageModelHandler = require('.')
4
+
5
+ const COMPLETIONS = 'langchain.response.completions'
6
+
7
+ class LangChainChatModelHandler extends LangChainLanguageModelHandler {
8
+ getSpanStartTags (ctx, provider) {
9
+ const tags = {}
10
+
11
+ const inputs = ctx.args?.[0]
12
+
13
+ for (const messageSetIndex in inputs) {
14
+ const messageSet = inputs[messageSetIndex]
15
+
16
+ for (const messageIndex in messageSet) {
17
+ const message = messageSet[messageIndex]
18
+ if (this.isPromptCompletionSampled()) {
19
+ tags[`langchain.request.messages.${messageSetIndex}.${messageIndex}.content`] =
20
+ this.normalize(message.content) || ''
21
+ }
22
+ tags[`langchain.request.messages.${messageSetIndex}.${messageIndex}.message_type`] = message.constructor.name
23
+ }
24
+ }
25
+
26
+ const instance = ctx.instance
27
+ const identifyingParams = (typeof instance._identifyingParams === 'function' && instance._identifyingParams()) || {}
28
+ for (const [param, val] of Object.entries(identifyingParams)) {
29
+ if (param.toLowerCase().includes('apikey') || param.toLowerCase().includes('apitoken')) continue
30
+ if (typeof val === 'object') {
31
+ for (const [key, value] of Object.entries(val)) {
32
+ tags[`langchain.request.${provider}.parameters.${param}.${key}`] = value
33
+ }
34
+ } else {
35
+ tags[`langchain.request.${provider}.parameters.${param}`] = val
36
+ }
37
+ }
38
+
39
+ return tags
40
+ }
41
+
42
+ getSpanEndTags (ctx) {
43
+ const { result } = ctx
44
+
45
+ const tags = {}
46
+
47
+ this.extractTokenMetrics(ctx.currentStore?.span, result)
48
+
49
+ for (const messageSetIdx in result.generations) {
50
+ const messageSet = result.generations[messageSetIdx]
51
+
52
+ for (const chatCompletionIdx in messageSet) {
53
+ const chatCompletion = messageSet[chatCompletionIdx]
54
+
55
+ const text = chatCompletion.text
56
+ const message = chatCompletion.message
57
+ let toolCalls = message.tool_calls
58
+
59
+ if (text && this.isPromptCompletionSampled()) {
60
+ tags[
61
+ `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.content`
62
+ ] = this.normalize(text)
63
+ }
64
+
65
+ tags[
66
+ `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.message_type`
67
+ ] = message.constructor.name
68
+
69
+ if (toolCalls) {
70
+ if (!Array.isArray(toolCalls)) {
71
+ toolCalls = [toolCalls]
72
+ }
73
+
74
+ for (const toolCallIndex in toolCalls) {
75
+ const toolCall = toolCalls[toolCallIndex]
76
+
77
+ tags[
78
+ `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.tool_calls.${toolCallIndex}.id`
79
+ ] = toolCall.id
80
+ tags[
81
+ `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.tool_calls.${toolCallIndex}.name`
82
+ ] = toolCall.name
83
+
84
+ const args = toolCall.args || {}
85
+ for (const [name, value] of Object.entries(args)) {
86
+ tags[
87
+ `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.tool_calls.${toolCallIndex}.args.${name}`
88
+ ] = this.normalize(value)
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ return tags
96
+ }
97
+ }
98
+
99
+ module.exports = LangChainChatModelHandler
@@ -0,0 +1,48 @@
1
+ 'use strict'
2
+
3
+ const { getTokensFromLlmOutput } = require('../../tokens')
4
+ const LangChainHandler = require('../default')
5
+
6
+ class LangChainLanguageModelHandler extends LangChainHandler {
7
+ extractApiKey (instance) {
8
+ const key = Object.keys(instance)
9
+ .find(key => {
10
+ const lower = key.toLowerCase()
11
+ return lower.includes('apikey') || lower.includes('apitoken')
12
+ })
13
+
14
+ let apiKey = instance[key]
15
+ if (apiKey?.secretValue && typeof apiKey.secretValue === 'function') {
16
+ apiKey = apiKey.secretValue()
17
+ }
18
+ if (!apiKey || apiKey.length < 4) return ''
19
+ return `...${apiKey.slice(-4)}`
20
+ }
21
+
22
+ extractProvider (instance) {
23
+ return typeof instance._llmType === 'function' && instance._llmType().split('-')[0]
24
+ }
25
+
26
+ extractModel (instance) {
27
+ for (const attr of ['model', 'modelName', 'modelId', 'modelKey', 'repoId']) {
28
+ const modelName = instance[attr]
29
+ if (modelName) return modelName
30
+ }
31
+ }
32
+
33
+ extractTokenMetrics (span, result) {
34
+ if (!span || !result) return
35
+
36
+ // we do not tag token metrics for non-openai providers
37
+ const provider = span.context()._tags['langchain.request.provider']
38
+ if (provider !== 'openai') return
39
+
40
+ const tokens = getTokensFromLlmOutput(result)
41
+
42
+ for (const [tokenKey, tokenCount] of Object.entries(tokens)) {
43
+ span.setTag(`langchain.tokens.${tokenKey}_tokens`, tokenCount)
44
+ }
45
+ }
46
+ }
47
+
48
+ module.exports = LangChainLanguageModelHandler
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const LangChainLanguageModelHandler = require('.')
4
+
5
+ class LangChainLLMHandler extends LangChainLanguageModelHandler {
6
+ getSpanStartTags (ctx, provider) {
7
+ const tags = {}
8
+
9
+ const prompts = ctx.args?.[0]
10
+ for (const promptIdx in prompts) {
11
+ if (!this.isPromptCompletionSampled()) continue
12
+
13
+ const prompt = prompts[promptIdx]
14
+ tags[`langchain.request.prompts.${promptIdx}.content`] = this.normalize(prompt) || ''
15
+ }
16
+
17
+ const instance = ctx.instance
18
+ const identifyingParams = (typeof instance._identifyingParams === 'function' && instance._identifyingParams()) || {}
19
+ for (const [param, val] of Object.entries(identifyingParams)) {
20
+ if (param.toLowerCase().includes('apikey') || param.toLowerCase().includes('apitoken')) continue
21
+ if (typeof val === 'object') {
22
+ for (const [key, value] of Object.entries(val)) {
23
+ tags[`langchain.request.${provider}.parameters.${param}.${key}`] = value
24
+ }
25
+ } else {
26
+ tags[`langchain.request.${provider}.parameters.${param}`] = val
27
+ }
28
+ }
29
+
30
+ return tags
31
+ }
32
+
33
+ getSpanEndTags (ctx) {
34
+ const { result } = ctx
35
+
36
+ const tags = {}
37
+
38
+ this.extractTokenMetrics(ctx.currentStore?.span, result)
39
+
40
+ for (const completionIdx in result.generations) {
41
+ const completion = result.generations[completionIdx]
42
+ if (this.isPromptCompletionSampled()) {
43
+ tags[`langchain.response.completions.${completionIdx}.text`] = this.normalize(completion[0].text) || ''
44
+ }
45
+
46
+ if (completion && completion[0].generationInfo) {
47
+ const generationInfo = completion[0].generationInfo
48
+ tags[`langchain.response.completions.${completionIdx}.finish_reason`] = generationInfo.finishReason
49
+ tags[`langchain.response.completions.${completionIdx}.logprobs`] = generationInfo.logprobs
50
+ }
51
+ }
52
+
53
+ return tags
54
+ }
55
+ }
56
+
57
+ module.exports = LangChainLLMHandler