dd-trace 5.24.0 → 5.26.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 +3 -0
- package/index.d.ts +345 -8
- package/init.js +60 -47
- package/package.json +16 -7
- package/packages/datadog-code-origin/index.js +4 -4
- package/packages/datadog-core/index.js +1 -3
- package/packages/datadog-core/src/storage.js +21 -0
- package/packages/datadog-core/src/utils/src/parse-tags.js +33 -0
- package/packages/datadog-esbuild/index.js +4 -2
- package/packages/datadog-instrumentations/src/amqplib.js +65 -5
- package/packages/datadog-instrumentations/src/child_process.js +135 -27
- 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/helpers/register.js +9 -0
- package/packages/datadog-instrumentations/src/jest.js +6 -2
- package/packages/datadog-instrumentations/src/kafkajs.js +123 -63
- package/packages/datadog-instrumentations/src/mocha/utils.js +2 -2
- package/packages/datadog-instrumentations/src/multer.js +37 -0
- package/packages/datadog-instrumentations/src/openai.js +2 -2
- package/packages/datadog-instrumentations/src/pug.js +23 -0
- package/packages/datadog-instrumentations/src/router.js +2 -3
- package/packages/datadog-instrumentations/src/url.js +84 -0
- package/packages/datadog-instrumentations/src/utils/src/extract-package-and-module-path.js +7 -4
- package/packages/datadog-plugin-amqplib/src/consumer.js +6 -5
- package/packages/datadog-plugin-aws-sdk/src/base.js +5 -0
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +1 -0
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +10 -7
- package/packages/datadog-plugin-aws-sdk/src/services/s3.js +35 -0
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +11 -9
- 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-fastify/src/code_origin.js +2 -2
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +10 -2
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +8 -0
- package/packages/datadog-plugin-grpc/src/client.js +3 -0
- package/packages/datadog-plugin-grpc/src/server.js +5 -1
- package/packages/datadog-plugin-http/src/client.js +42 -1
- package/packages/datadog-plugin-http2/src/client.js +26 -1
- package/packages/datadog-plugin-jest/src/index.js +2 -1
- package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +6 -3
- package/packages/datadog-plugin-kafkajs/src/consumer.js +10 -5
- package/packages/datadog-plugin-kafkajs/src/producer.js +10 -4
- package/packages/datadog-plugin-mocha/src/index.js +5 -2
- package/packages/datadog-plugin-moleculer/src/server.js +2 -2
- package/packages/datadog-plugin-openai/src/index.js +9 -1015
- package/packages/datadog-plugin-openai/src/tracing.js +1023 -0
- package/packages/datadog-plugin-rhea/src/consumer.js +2 -1
- package/packages/datadog-plugin-vitest/src/index.js +2 -1
- package/packages/dd-trace/src/appsec/addresses.js +2 -0
- package/packages/dd-trace/src/appsec/api_security_sampler.js +50 -27
- package/packages/dd-trace/src/appsec/channels.js +3 -1
- 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/taint-tracking/plugin.js +55 -7
- 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/iast/vulnerability-reporter.js +4 -2
- package/packages/dd-trace/src/appsec/index.js +9 -6
- package/packages/dd-trace/src/appsec/rasp/command_injection.js +49 -0
- package/packages/dd-trace/src/appsec/rasp/index.js +3 -0
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +4 -3
- package/packages/dd-trace/src/appsec/rasp/utils.js +3 -2
- package/packages/dd-trace/src/appsec/recommended.json +354 -158
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +2 -7
- package/packages/dd-trace/src/appsec/reporter.js +6 -4
- package/packages/dd-trace/src/appsec/sdk/track_event.js +5 -3
- package/packages/dd-trace/src/appsec/waf/waf_manager.js +4 -0
- package/packages/dd-trace/src/azure_metadata.js +120 -0
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +97 -0
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +90 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +19 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +53 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +8 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +43 -0
- package/packages/dd-trace/src/config.js +88 -10
- package/packages/dd-trace/src/constants.js +8 -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/datastreams/pathway.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +9 -13
- package/packages/dd-trace/src/debugger/devtools_client/send.js +15 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +57 -23
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +12 -2
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +31 -20
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/symbols.js +6 -0
- package/packages/dd-trace/src/debugger/devtools_client/state.js +11 -2
- package/packages/dd-trace/src/debugger/index.js +10 -3
- package/packages/dd-trace/src/llmobs/constants/tags.js +34 -0
- package/packages/dd-trace/src/llmobs/constants/text.js +6 -0
- package/packages/dd-trace/src/llmobs/constants/writers.js +13 -0
- package/packages/dd-trace/src/llmobs/index.js +103 -0
- package/packages/dd-trace/src/llmobs/noop.js +82 -0
- package/packages/dd-trace/src/llmobs/plugins/base.js +65 -0
- package/packages/dd-trace/src/llmobs/plugins/openai.js +205 -0
- package/packages/dd-trace/src/llmobs/sdk.js +377 -0
- package/packages/dd-trace/src/llmobs/span_processor.js +195 -0
- package/packages/dd-trace/src/llmobs/storage.js +7 -0
- package/packages/dd-trace/src/llmobs/tagger.js +322 -0
- package/packages/dd-trace/src/llmobs/util.js +176 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +111 -0
- package/packages/dd-trace/src/llmobs/writers/evaluations.js +29 -0
- package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +23 -0
- package/packages/dd-trace/src/llmobs/writers/spans/agentless.js +17 -0
- package/packages/dd-trace/src/llmobs/writers/spans/base.js +52 -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/proxy.js +3 -0
- package/packages/dd-trace/src/noop/span.js +4 -0
- package/packages/dd-trace/src/opentelemetry/span.js +16 -1
- package/packages/dd-trace/src/opentelemetry/tracer.js +1 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +106 -32
- package/packages/dd-trace/src/opentracing/span.js +26 -0
- package/packages/dd-trace/src/opentracing/span_context.js +1 -0
- package/packages/dd-trace/src/opentracing/tracer.js +8 -1
- package/packages/dd-trace/src/payload-tagging/config/aws.json +71 -3
- package/packages/dd-trace/src/plugins/outbound.js +9 -0
- package/packages/dd-trace/src/plugins/tracing.js +3 -3
- 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/priority_sampler.js +16 -0
- package/packages/dd-trace/src/profiling/config.js +3 -1
- package/packages/dd-trace/src/profiling/exporters/agent.js +7 -5
- package/packages/dd-trace/src/profiling/profilers/wall.js +2 -1
- package/packages/dd-trace/src/proxy.js +13 -1
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/telemetry/index.js +11 -1
- 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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../../log')
|
|
4
|
+
const { storage } = require('../storage')
|
|
5
|
+
|
|
6
|
+
const TracingPlugin = require('../../plugins/tracing')
|
|
7
|
+
const LLMObsTagger = require('../tagger')
|
|
8
|
+
|
|
9
|
+
// we make this a `Plugin` so we don't have to worry about `finish` being called
|
|
10
|
+
class LLMObsPlugin extends TracingPlugin {
|
|
11
|
+
constructor (...args) {
|
|
12
|
+
super(...args)
|
|
13
|
+
|
|
14
|
+
this._tagger = new LLMObsTagger(this._tracerConfig, true)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getName () {}
|
|
18
|
+
|
|
19
|
+
setLLMObsTags (ctx) {
|
|
20
|
+
throw new Error('setLLMObsTags must be implemented by the subclass')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getLLMObsSPanRegisterOptions (ctx) {
|
|
24
|
+
throw new Error('getLLMObsSPanRegisterOptions must be implemented by the subclass')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
start (ctx) {
|
|
28
|
+
const oldStore = storage.getStore()
|
|
29
|
+
const parent = oldStore?.span
|
|
30
|
+
const span = ctx.currentStore?.span
|
|
31
|
+
|
|
32
|
+
const registerOptions = this.getLLMObsSPanRegisterOptions(ctx)
|
|
33
|
+
|
|
34
|
+
this._tagger.registerLLMObsSpan(span, { parent, ...registerOptions })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
asyncEnd (ctx) {
|
|
38
|
+
// even though llmobs span events won't be enqueued if llmobs is disabled
|
|
39
|
+
// we should avoid doing any computations here (these listeners aren't disabled)
|
|
40
|
+
const enabled = this._tracerConfig.llmobs.enabled
|
|
41
|
+
if (!enabled) return
|
|
42
|
+
|
|
43
|
+
const span = ctx.currentStore?.span
|
|
44
|
+
if (!span) {
|
|
45
|
+
log.debug(
|
|
46
|
+
`Tried to start an LLMObs span for ${this.constructor.name} without an active APM span.
|
|
47
|
+
Not starting LLMObs span.`
|
|
48
|
+
)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.setLLMObsTags(ctx)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
configure (config) {
|
|
56
|
+
// we do not want to enable any LLMObs plugins if it is disabled on the tracer
|
|
57
|
+
const llmobsEnabled = this._tracerConfig.llmobs.enabled
|
|
58
|
+
if (llmobsEnabled === false) {
|
|
59
|
+
config = typeof config === 'boolean' ? false : { ...config, enabled: false } // override to false
|
|
60
|
+
}
|
|
61
|
+
super.configure(config)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = LLMObsPlugin
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const LLMObsPlugin = require('./base')
|
|
4
|
+
|
|
5
|
+
class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
6
|
+
static get prefix () {
|
|
7
|
+
return 'tracing:apm:openai:request'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getLLMObsSPanRegisterOptions (ctx) {
|
|
11
|
+
const resource = ctx.methodName
|
|
12
|
+
const methodName = gateResource(normalizeOpenAIResourceName(resource))
|
|
13
|
+
if (!methodName) return // we will not trace all openai methods for llmobs
|
|
14
|
+
|
|
15
|
+
const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument
|
|
16
|
+
const operation = getOperation(methodName)
|
|
17
|
+
const kind = operation === 'embedding' ? 'embedding' : 'llm'
|
|
18
|
+
const name = `openai.${methodName}`
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
modelProvider: 'openai',
|
|
22
|
+
modelName: inputs.model,
|
|
23
|
+
kind,
|
|
24
|
+
name
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setLLMObsTags (ctx) {
|
|
29
|
+
const span = ctx.currentStore?.span
|
|
30
|
+
const resource = ctx.methodName
|
|
31
|
+
const methodName = gateResource(normalizeOpenAIResourceName(resource))
|
|
32
|
+
if (!methodName) return // we will not trace all openai methods for llmobs
|
|
33
|
+
|
|
34
|
+
const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument
|
|
35
|
+
const response = ctx.result?.data // no result if error
|
|
36
|
+
const error = !!span.context()._tags.error
|
|
37
|
+
|
|
38
|
+
const operation = getOperation(methodName)
|
|
39
|
+
|
|
40
|
+
if (operation === 'completion') {
|
|
41
|
+
this._tagCompletion(span, inputs, response, error)
|
|
42
|
+
} else if (operation === 'chat') {
|
|
43
|
+
this._tagChatCompletion(span, inputs, response, error)
|
|
44
|
+
} else if (operation === 'embedding') {
|
|
45
|
+
this._tagEmbedding(span, inputs, response, error)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!error) {
|
|
49
|
+
const metrics = this._extractMetrics(response)
|
|
50
|
+
this._tagger.tagMetrics(span, metrics)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_extractMetrics (response) {
|
|
55
|
+
const metrics = {}
|
|
56
|
+
const tokenUsage = response.usage
|
|
57
|
+
|
|
58
|
+
if (tokenUsage) {
|
|
59
|
+
const inputTokens = tokenUsage.prompt_tokens
|
|
60
|
+
if (inputTokens) metrics.inputTokens = inputTokens
|
|
61
|
+
|
|
62
|
+
const outputTokens = tokenUsage.completion_tokens
|
|
63
|
+
if (outputTokens) metrics.outputTokens = outputTokens
|
|
64
|
+
|
|
65
|
+
const totalTokens = tokenUsage.total_toksn || (inputTokens + outputTokens)
|
|
66
|
+
if (totalTokens) metrics.totalTokens = totalTokens
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return metrics
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_tagEmbedding (span, inputs, response, error) {
|
|
73
|
+
const { model, ...parameters } = inputs
|
|
74
|
+
|
|
75
|
+
const metadata = {
|
|
76
|
+
encoding_format: parameters.encoding_format || 'float'
|
|
77
|
+
}
|
|
78
|
+
if (inputs.dimensions) metadata.dimensions = inputs.dimensions
|
|
79
|
+
this._tagger.tagMetadata(span, metadata)
|
|
80
|
+
|
|
81
|
+
let embeddingInputs = inputs.input
|
|
82
|
+
if (!Array.isArray(embeddingInputs)) embeddingInputs = [embeddingInputs]
|
|
83
|
+
const embeddingInput = embeddingInputs.map(input => ({ text: input }))
|
|
84
|
+
|
|
85
|
+
if (error) {
|
|
86
|
+
this._tagger.tagEmbeddingIO(span, embeddingInput, undefined)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const float = Array.isArray(response.data[0].embedding)
|
|
91
|
+
let embeddingOutput
|
|
92
|
+
if (float) {
|
|
93
|
+
const embeddingDim = response.data[0].embedding.length
|
|
94
|
+
embeddingOutput = `[${response.data.length} embedding(s) returned with size ${embeddingDim}]`
|
|
95
|
+
} else {
|
|
96
|
+
embeddingOutput = `[${response.data.length} embedding(s) returned]`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_tagCompletion (span, inputs, response, error) {
|
|
103
|
+
let { prompt, model, ...parameters } = inputs
|
|
104
|
+
if (!Array.isArray(prompt)) prompt = [prompt]
|
|
105
|
+
|
|
106
|
+
const completionInput = prompt.map(p => ({ content: p }))
|
|
107
|
+
|
|
108
|
+
const completionOutput = error ? [{ content: '' }] : response.choices.map(choice => ({ content: choice.text }))
|
|
109
|
+
|
|
110
|
+
this._tagger.tagLLMIO(span, completionInput, completionOutput)
|
|
111
|
+
this._tagger.tagMetadata(span, parameters)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_tagChatCompletion (span, inputs, response, error) {
|
|
115
|
+
const { messages, model, ...parameters } = inputs
|
|
116
|
+
|
|
117
|
+
if (error) {
|
|
118
|
+
this._tagger.tagLLMIO(span, messages, [{ content: '' }])
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const outputMessages = []
|
|
123
|
+
const { choices } = response
|
|
124
|
+
for (const choice of choices) {
|
|
125
|
+
const message = choice.message || choice.delta
|
|
126
|
+
const content = message.content || ''
|
|
127
|
+
const role = message.role
|
|
128
|
+
|
|
129
|
+
if (message.function_call) {
|
|
130
|
+
const functionCallInfo = {
|
|
131
|
+
name: message.function_call.name,
|
|
132
|
+
arguments: JSON.parse(message.function_call.arguments)
|
|
133
|
+
}
|
|
134
|
+
outputMessages.push({ content, role, toolCalls: [functionCallInfo] })
|
|
135
|
+
} else if (message.tool_calls) {
|
|
136
|
+
const toolCallsInfo = []
|
|
137
|
+
for (const toolCall of message.tool_calls) {
|
|
138
|
+
const toolCallInfo = {
|
|
139
|
+
arguments: JSON.parse(toolCall.function.arguments),
|
|
140
|
+
name: toolCall.function.name,
|
|
141
|
+
toolId: toolCall.id,
|
|
142
|
+
type: toolCall.type
|
|
143
|
+
}
|
|
144
|
+
toolCallsInfo.push(toolCallInfo)
|
|
145
|
+
}
|
|
146
|
+
outputMessages.push({ content, role, toolCalls: toolCallsInfo })
|
|
147
|
+
} else {
|
|
148
|
+
outputMessages.push({ content, role })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this._tagger.tagLLMIO(span, messages, outputMessages)
|
|
153
|
+
|
|
154
|
+
const metadata = Object.entries(parameters).reduce((obj, [key, value]) => {
|
|
155
|
+
if (!['tools', 'functions'].includes(key)) {
|
|
156
|
+
obj[key] = value
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return obj
|
|
160
|
+
}, {})
|
|
161
|
+
|
|
162
|
+
this._tagger.tagMetadata(span, metadata)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// TODO: this will be moved to the APM integration
|
|
167
|
+
function normalizeOpenAIResourceName (resource) {
|
|
168
|
+
switch (resource) {
|
|
169
|
+
// completions
|
|
170
|
+
case 'completions.create':
|
|
171
|
+
return 'createCompletion'
|
|
172
|
+
|
|
173
|
+
// chat completions
|
|
174
|
+
case 'chat.completions.create':
|
|
175
|
+
return 'createChatCompletion'
|
|
176
|
+
|
|
177
|
+
// embeddings
|
|
178
|
+
case 'embeddings.create':
|
|
179
|
+
return 'createEmbedding'
|
|
180
|
+
default:
|
|
181
|
+
return resource
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function gateResource (resource) {
|
|
186
|
+
return ['createCompletion', 'createChatCompletion', 'createEmbedding'].includes(resource)
|
|
187
|
+
? resource
|
|
188
|
+
: undefined
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getOperation (resource) {
|
|
192
|
+
switch (resource) {
|
|
193
|
+
case 'createCompletion':
|
|
194
|
+
return 'completion'
|
|
195
|
+
case 'createChatCompletion':
|
|
196
|
+
return 'chat'
|
|
197
|
+
case 'createEmbedding':
|
|
198
|
+
return 'embedding'
|
|
199
|
+
default:
|
|
200
|
+
// should never happen
|
|
201
|
+
return 'unknown'
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = OpenAiLLMObsPlugin
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { SPAN_KIND, OUTPUT_VALUE } = require('./constants/tags')
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
getFunctionArguments,
|
|
7
|
+
validateKind
|
|
8
|
+
} = require('./util')
|
|
9
|
+
const { isTrue } = require('../util')
|
|
10
|
+
|
|
11
|
+
const { storage } = require('./storage')
|
|
12
|
+
|
|
13
|
+
const Span = require('../opentracing/span')
|
|
14
|
+
|
|
15
|
+
const tracerVersion = require('../../../../package.json').version
|
|
16
|
+
const logger = require('../log')
|
|
17
|
+
|
|
18
|
+
const LLMObsTagger = require('./tagger')
|
|
19
|
+
|
|
20
|
+
// communicating with writer
|
|
21
|
+
const { channel } = require('dc-polyfill')
|
|
22
|
+
const evalMetricAppendCh = channel('llmobs:eval-metric:append')
|
|
23
|
+
const flushCh = channel('llmobs:writers:flush')
|
|
24
|
+
const NoopLLMObs = require('./noop')
|
|
25
|
+
|
|
26
|
+
class LLMObs extends NoopLLMObs {
|
|
27
|
+
constructor (tracer, llmobsModule, config) {
|
|
28
|
+
super(tracer)
|
|
29
|
+
|
|
30
|
+
this._config = config
|
|
31
|
+
this._llmobsModule = llmobsModule
|
|
32
|
+
this._tagger = new LLMObsTagger(config)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get enabled () {
|
|
36
|
+
return this._config.llmobs.enabled
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
enable (options = {}) {
|
|
40
|
+
if (this.enabled) {
|
|
41
|
+
logger.debug('LLMObs is already enabled.')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.debug('Enabling LLMObs')
|
|
46
|
+
|
|
47
|
+
const { mlApp, agentlessEnabled } = options
|
|
48
|
+
|
|
49
|
+
const { DD_LLMOBS_ENABLED } = process.env
|
|
50
|
+
|
|
51
|
+
const llmobsConfig = {
|
|
52
|
+
mlApp,
|
|
53
|
+
agentlessEnabled
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const enabled = DD_LLMOBS_ENABLED == null || isTrue(DD_LLMOBS_ENABLED)
|
|
57
|
+
if (!enabled) {
|
|
58
|
+
logger.debug('LLMObs.enable() called when DD_LLMOBS_ENABLED is false. No action taken.')
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this._config.llmobs.enabled = true
|
|
63
|
+
this._config.configure({ ...this._config, llmobs: llmobsConfig })
|
|
64
|
+
|
|
65
|
+
// configure writers and channel subscribers
|
|
66
|
+
this._llmobsModule.enable(this._config)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
disable () {
|
|
70
|
+
if (!this.enabled) {
|
|
71
|
+
logger.debug('LLMObs is already disabled.')
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.debug('Disabling LLMObs')
|
|
76
|
+
|
|
77
|
+
this._config.llmobs.enabled = false
|
|
78
|
+
|
|
79
|
+
// disable writers and channel subscribers
|
|
80
|
+
this._llmobsModule.disable()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
trace (options = {}, fn) {
|
|
84
|
+
if (typeof options === 'function') {
|
|
85
|
+
fn = options
|
|
86
|
+
options = {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const kind = validateKind(options.kind) // will throw if kind is undefined or not an expected kind
|
|
90
|
+
|
|
91
|
+
// name is required for spans generated with `trace`
|
|
92
|
+
// while `kind` is required, this should never throw (as otherwise it would have thrown above)
|
|
93
|
+
const name = options.name || kind
|
|
94
|
+
if (!name) {
|
|
95
|
+
throw new Error('No span name provided for `trace`.')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const {
|
|
99
|
+
spanOptions,
|
|
100
|
+
...llmobsOptions
|
|
101
|
+
} = this._extractOptions(options)
|
|
102
|
+
|
|
103
|
+
if (fn.length > 1) {
|
|
104
|
+
return this._tracer.trace(name, spanOptions, (span, cb) =>
|
|
105
|
+
this._activate(span, { kind, options: llmobsOptions }, () => fn(span, cb))
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this._tracer.trace(name, spanOptions, span =>
|
|
110
|
+
this._activate(span, { kind, options: llmobsOptions }, () => fn(span))
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
wrap (options = {}, fn) {
|
|
115
|
+
if (typeof options === 'function') {
|
|
116
|
+
fn = options
|
|
117
|
+
options = {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const kind = validateKind(options.kind) // will throw if kind is undefined or not an expected kind
|
|
121
|
+
let name = options.name || (fn?.name ? fn.name : undefined) || kind
|
|
122
|
+
|
|
123
|
+
if (!name) {
|
|
124
|
+
logger.warn('No span name provided for `wrap`. Defaulting to "unnamed-anonymous-function".')
|
|
125
|
+
name = 'unnamed-anonymous-function'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const {
|
|
129
|
+
spanOptions,
|
|
130
|
+
...llmobsOptions
|
|
131
|
+
} = this._extractOptions(options)
|
|
132
|
+
|
|
133
|
+
const llmobs = this
|
|
134
|
+
|
|
135
|
+
function wrapped () {
|
|
136
|
+
const span = llmobs._tracer.scope().active()
|
|
137
|
+
|
|
138
|
+
const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => {
|
|
139
|
+
if (!['llm', 'embedding'].includes(kind)) {
|
|
140
|
+
llmobs.annotate(span, { inputData: getFunctionArguments(fn, arguments) })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return fn.apply(this, arguments)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
if (result && typeof result.then === 'function') {
|
|
147
|
+
return result.then(value => {
|
|
148
|
+
if (value && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
|
|
149
|
+
llmobs.annotate(span, { outputData: value })
|
|
150
|
+
}
|
|
151
|
+
return value
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (result && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
|
|
156
|
+
llmobs.annotate(span, { outputData: result })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return this._tracer.wrap(name, spanOptions, wrapped)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
annotate (span, options) {
|
|
166
|
+
if (!this.enabled) return
|
|
167
|
+
|
|
168
|
+
if (!span) {
|
|
169
|
+
span = this._active()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if ((span && !options) && !(span instanceof Span)) {
|
|
173
|
+
options = span
|
|
174
|
+
span = this._active()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!span) {
|
|
178
|
+
throw new Error('No span provided and no active LLMObs-generated span found')
|
|
179
|
+
}
|
|
180
|
+
if (!options) {
|
|
181
|
+
throw new Error('No options provided for annotation.')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!LLMObsTagger.tagMap.has(span)) {
|
|
185
|
+
throw new Error('Span must be an LLMObs-generated span')
|
|
186
|
+
}
|
|
187
|
+
if (span._duration !== undefined) {
|
|
188
|
+
throw new Error('Cannot annotate a finished span')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const spanKind = LLMObsTagger.tagMap.get(span)[SPAN_KIND]
|
|
192
|
+
if (!spanKind) {
|
|
193
|
+
throw new Error('LLMObs span must have a span kind specified')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { inputData, outputData, metadata, metrics, tags } = options
|
|
197
|
+
|
|
198
|
+
if (inputData || outputData) {
|
|
199
|
+
if (spanKind === 'llm') {
|
|
200
|
+
this._tagger.tagLLMIO(span, inputData, outputData)
|
|
201
|
+
} else if (spanKind === 'embedding') {
|
|
202
|
+
this._tagger.tagEmbeddingIO(span, inputData, outputData)
|
|
203
|
+
} else if (spanKind === 'retrieval') {
|
|
204
|
+
this._tagger.tagRetrievalIO(span, inputData, outputData)
|
|
205
|
+
} else {
|
|
206
|
+
this._tagger.tagTextIO(span, inputData, outputData)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (metadata) {
|
|
211
|
+
this._tagger.tagMetadata(span, metadata)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (metrics) {
|
|
215
|
+
this._tagger.tagMetrics(span, metrics)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (tags) {
|
|
219
|
+
this._tagger.tagSpanTags(span, tags)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
exportSpan (span) {
|
|
224
|
+
span = span || this._active()
|
|
225
|
+
|
|
226
|
+
if (!span) {
|
|
227
|
+
throw new Error('No span provided and no active LLMObs-generated span found')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!(span instanceof Span)) {
|
|
231
|
+
throw new Error('Span must be a valid Span object.')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!LLMObsTagger.tagMap.has(span)) {
|
|
235
|
+
throw new Error('Span must be an LLMObs-generated span')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
return {
|
|
240
|
+
traceId: span.context().toTraceId(true),
|
|
241
|
+
spanId: span.context().toSpanId()
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
logger.warn('Faild to export span. Span must be a valid Span object.')
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
submitEvaluation (llmobsSpanContext, options = {}) {
|
|
249
|
+
if (!this.enabled) return
|
|
250
|
+
|
|
251
|
+
if (!this._config.apiKey) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
'DD_API_KEY is required for sending evaluation metrics. Evaluation metric data will not be sent.\n' +
|
|
254
|
+
'Ensure this configuration is set before running your application.'
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { traceId, spanId } = llmobsSpanContext
|
|
259
|
+
if (!traceId || !spanId) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
'spanId and traceId must both be specified for the given evaluation metric to be submitted.'
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const mlApp = options.mlApp || this._config.llmobs.mlApp
|
|
266
|
+
if (!mlApp) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
'ML App name is required for sending evaluation metrics. Evaluation metric data will not be sent.'
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const timestampMs = options.timestampMs || Date.now()
|
|
273
|
+
if (typeof timestampMs !== 'number' || timestampMs < 0) {
|
|
274
|
+
throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const { label, value, tags } = options
|
|
278
|
+
const metricType = options.metricType?.toLowerCase()
|
|
279
|
+
if (!label) {
|
|
280
|
+
throw new Error('label must be the specified name of the evaluation metric')
|
|
281
|
+
}
|
|
282
|
+
if (!metricType || !['categorical', 'score'].includes(metricType)) {
|
|
283
|
+
throw new Error('metricType must be one of "categorical" or "score"')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (metricType === 'categorical' && typeof value !== 'string') {
|
|
287
|
+
throw new Error('value must be a string for a categorical metric.')
|
|
288
|
+
}
|
|
289
|
+
if (metricType === 'score' && typeof value !== 'number') {
|
|
290
|
+
throw new Error('value must be a number for a score metric.')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const evaluationTags = {
|
|
294
|
+
'ddtrace.version': tracerVersion,
|
|
295
|
+
ml_app: mlApp
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (tags) {
|
|
299
|
+
for (const key in tags) {
|
|
300
|
+
const tag = tags[key]
|
|
301
|
+
if (typeof tag === 'string') {
|
|
302
|
+
evaluationTags[key] = tag
|
|
303
|
+
} else if (typeof tag.toString === 'function') {
|
|
304
|
+
evaluationTags[key] = tag.toString()
|
|
305
|
+
} else if (tag == null) {
|
|
306
|
+
evaluationTags[key] = Object.prototype.toString.call(tag)
|
|
307
|
+
} else {
|
|
308
|
+
// should be a rare case
|
|
309
|
+
// every object in JS has a toString, otherwise every primitive has its own toString
|
|
310
|
+
// null and undefined are handled above
|
|
311
|
+
throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const payload = {
|
|
317
|
+
span_id: spanId,
|
|
318
|
+
trace_id: traceId,
|
|
319
|
+
label,
|
|
320
|
+
metric_type: metricType,
|
|
321
|
+
ml_app: mlApp,
|
|
322
|
+
[`${metricType}_value`]: value,
|
|
323
|
+
timestamp_ms: timestampMs,
|
|
324
|
+
tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
evalMetricAppendCh.publish(payload)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
flush () {
|
|
331
|
+
if (!this.enabled) return
|
|
332
|
+
|
|
333
|
+
flushCh.publish()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_active () {
|
|
337
|
+
const store = storage.getStore()
|
|
338
|
+
return store?.span
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
_activate (span, { kind, options } = {}, fn) {
|
|
342
|
+
const parent = this._active()
|
|
343
|
+
if (this.enabled) storage.enterWith({ span })
|
|
344
|
+
|
|
345
|
+
this._tagger.registerLLMObsSpan(span, {
|
|
346
|
+
...options,
|
|
347
|
+
parent,
|
|
348
|
+
kind
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
return fn()
|
|
353
|
+
} finally {
|
|
354
|
+
if (this.enabled) storage.enterWith({ span: parent })
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_extractOptions (options) {
|
|
359
|
+
const {
|
|
360
|
+
modelName,
|
|
361
|
+
modelProvider,
|
|
362
|
+
sessionId,
|
|
363
|
+
mlApp,
|
|
364
|
+
...spanOptions
|
|
365
|
+
} = options
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
mlApp,
|
|
369
|
+
modelName,
|
|
370
|
+
modelProvider,
|
|
371
|
+
sessionId,
|
|
372
|
+
spanOptions
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = LLMObs
|