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.
- package/LICENSE-3rdparty.csv +2 -0
- package/index.d.ts +17 -8
- package/init.js +60 -47
- package/package.json +5 -2
- package/packages/datadog-core/index.js +1 -3
- package/packages/datadog-core/src/storage.js +21 -0
- package/packages/datadog-instrumentations/src/express.js +1 -1
- package/packages/datadog-instrumentations/src/handlebars.js +40 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +5 -0
- package/packages/datadog-instrumentations/src/jest.js +6 -2
- package/packages/datadog-instrumentations/src/langchain.js +77 -0
- package/packages/datadog-instrumentations/src/next.js +19 -7
- package/packages/datadog-instrumentations/src/pug.js +23 -0
- package/packages/datadog-instrumentations/src/router.js +2 -3
- package/packages/datadog-plugin-aws-sdk/src/base.js +5 -0
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +7 -6
- package/packages/datadog-plugin-aws-sdk/src/services/s3.js +34 -0
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +8 -8
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +59 -45
- package/packages/datadog-plugin-cypress/src/support.js +1 -0
- package/packages/datadog-plugin-http/src/client.js +42 -1
- package/packages/datadog-plugin-http2/src/client.js +26 -1
- package/packages/datadog-plugin-langchain/src/handlers/chain.js +50 -0
- package/packages/datadog-plugin-langchain/src/handlers/default.js +53 -0
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +63 -0
- package/packages/datadog-plugin-langchain/src/handlers/language_models/chat_model.js +99 -0
- package/packages/datadog-plugin-langchain/src/handlers/language_models/index.js +48 -0
- package/packages/datadog-plugin-langchain/src/handlers/language_models/llm.js +57 -0
- package/packages/datadog-plugin-langchain/src/index.js +89 -0
- package/packages/datadog-plugin-langchain/src/tokens.js +35 -0
- package/packages/datadog-plugin-mocha/src/index.js +1 -1
- package/packages/datadog-plugin-moleculer/src/server.js +0 -1
- package/packages/dd-trace/src/appsec/api_security_sampler.js +50 -27
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/header-injection-analyzer.js +33 -16
- package/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js +18 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
- package/packages/dd-trace/src/appsec/index.js +6 -6
- package/packages/dd-trace/src/appsec/recommended.json +353 -155
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +1 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +0 -7
- package/packages/dd-trace/src/appsec/reporter.js +1 -0
- package/packages/dd-trace/src/appsec/sdk/utils.js +21 -2
- package/packages/dd-trace/src/config.js +21 -4
- package/packages/dd-trace/src/constants.js +6 -1
- package/packages/dd-trace/src/crashtracking/crashtracker.js +98 -0
- package/packages/dd-trace/src/crashtracking/index.js +15 -0
- package/packages/dd-trace/src/crashtracking/noop.js +8 -0
- package/packages/dd-trace/src/llmobs/sdk.js +1 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
- package/packages/dd-trace/src/llmobs/writers/spans/base.js +3 -0
- package/packages/dd-trace/src/log/index.js +10 -13
- package/packages/dd-trace/src/log/log.js +52 -0
- package/packages/dd-trace/src/log/writer.js +50 -19
- package/packages/dd-trace/src/noop/span.js +1 -0
- package/packages/dd-trace/src/opentelemetry/span.js +15 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +35 -22
- package/packages/dd-trace/src/opentracing/span.js +14 -0
- package/packages/dd-trace/src/opentracing/span_context.js +1 -0
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/plugins/tracing.js +2 -2
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +121 -0
- package/packages/dd-trace/src/plugins/util/ip_extractor.js +0 -1
- package/packages/dd-trace/src/plugins/util/web.js +39 -11
- package/packages/dd-trace/src/profiling/exporters/agent.js +42 -5
- package/packages/dd-trace/src/profiling/profiler.js +5 -2
- package/packages/dd-trace/src/proxy.js +5 -0
- package/packages/dd-trace/src/telemetry/logs/index.js +16 -11
- package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -8
- package/packages/dd-trace/src/telemetry/metrics.js +6 -1
- package/packages/dd-trace/src/util.js +16 -1
- package/version.js +4 -2
- /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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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.
|
|
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
|
-
|
|
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
|