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,195 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
SPAN_KIND,
|
|
5
|
+
MODEL_NAME,
|
|
6
|
+
MODEL_PROVIDER,
|
|
7
|
+
METADATA,
|
|
8
|
+
INPUT_MESSAGES,
|
|
9
|
+
INPUT_VALUE,
|
|
10
|
+
OUTPUT_MESSAGES,
|
|
11
|
+
INPUT_DOCUMENTS,
|
|
12
|
+
OUTPUT_DOCUMENTS,
|
|
13
|
+
OUTPUT_VALUE,
|
|
14
|
+
METRICS,
|
|
15
|
+
ML_APP,
|
|
16
|
+
TAGS,
|
|
17
|
+
PARENT_ID_KEY,
|
|
18
|
+
SESSION_ID,
|
|
19
|
+
NAME
|
|
20
|
+
} = require('./constants/tags')
|
|
21
|
+
const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text')
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
ERROR_MESSAGE,
|
|
25
|
+
ERROR_TYPE,
|
|
26
|
+
ERROR_STACK
|
|
27
|
+
} = require('../constants')
|
|
28
|
+
|
|
29
|
+
const LLMObsTagger = require('./tagger')
|
|
30
|
+
|
|
31
|
+
const tracerVersion = require('../../../../package.json').version
|
|
32
|
+
const logger = require('../log')
|
|
33
|
+
|
|
34
|
+
class LLMObsSpanProcessor {
|
|
35
|
+
constructor (config) {
|
|
36
|
+
this._config = config
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setWriter (writer) {
|
|
40
|
+
this._writer = writer
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// TODO: instead of relying on the tagger's weakmap registry, can we use some namespaced storage correlation?
|
|
44
|
+
process ({ span }) {
|
|
45
|
+
if (!this._config.llmobs.enabled) return
|
|
46
|
+
// if the span is not in our private tagger map, it is not an llmobs span
|
|
47
|
+
if (!LLMObsTagger.tagMap.has(span)) return
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const formattedEvent = this.format(span)
|
|
51
|
+
this._writer.append(formattedEvent)
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// this should be a rare case
|
|
54
|
+
// we protect against unserializable properties in the format function, and in
|
|
55
|
+
// safeguards in the tagger
|
|
56
|
+
logger.warn(`
|
|
57
|
+
Failed to append span to LLM Observability writer, likely due to an unserializable property.
|
|
58
|
+
Span won't be sent to LLM Observability: ${e.message}
|
|
59
|
+
`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
format (span) {
|
|
64
|
+
const spanTags = span.context()._tags
|
|
65
|
+
const mlObsTags = LLMObsTagger.tagMap.get(span)
|
|
66
|
+
|
|
67
|
+
const spanKind = mlObsTags[SPAN_KIND]
|
|
68
|
+
|
|
69
|
+
const meta = { 'span.kind': spanKind, input: {}, output: {} }
|
|
70
|
+
const input = {}
|
|
71
|
+
const output = {}
|
|
72
|
+
|
|
73
|
+
if (['llm', 'embedding'].includes(spanKind)) {
|
|
74
|
+
meta.model_name = mlObsTags[MODEL_NAME] || 'custom'
|
|
75
|
+
meta.model_provider = (mlObsTags[MODEL_PROVIDER] || 'custom').toLowerCase()
|
|
76
|
+
}
|
|
77
|
+
if (mlObsTags[METADATA]) {
|
|
78
|
+
this._addObject(mlObsTags[METADATA], meta.metadata = {})
|
|
79
|
+
}
|
|
80
|
+
if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
|
|
81
|
+
input.messages = mlObsTags[INPUT_MESSAGES]
|
|
82
|
+
}
|
|
83
|
+
if (mlObsTags[INPUT_VALUE]) {
|
|
84
|
+
input.value = mlObsTags[INPUT_VALUE]
|
|
85
|
+
}
|
|
86
|
+
if (spanKind === 'llm' && mlObsTags[OUTPUT_MESSAGES]) {
|
|
87
|
+
output.messages = mlObsTags[OUTPUT_MESSAGES]
|
|
88
|
+
}
|
|
89
|
+
if (spanKind === 'embedding' && mlObsTags[INPUT_DOCUMENTS]) {
|
|
90
|
+
input.documents = mlObsTags[INPUT_DOCUMENTS]
|
|
91
|
+
}
|
|
92
|
+
if (mlObsTags[OUTPUT_VALUE]) {
|
|
93
|
+
output.value = mlObsTags[OUTPUT_VALUE]
|
|
94
|
+
}
|
|
95
|
+
if (spanKind === 'retrieval' && mlObsTags[OUTPUT_DOCUMENTS]) {
|
|
96
|
+
output.documents = mlObsTags[OUTPUT_DOCUMENTS]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const error = spanTags.error || spanTags[ERROR_TYPE]
|
|
100
|
+
if (error) {
|
|
101
|
+
meta[ERROR_MESSAGE] = spanTags[ERROR_MESSAGE] || error.message || error.code
|
|
102
|
+
meta[ERROR_TYPE] = spanTags[ERROR_TYPE] || error.name
|
|
103
|
+
meta[ERROR_STACK] = spanTags[ERROR_STACK] || error.stack
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (input) meta.input = input
|
|
107
|
+
if (output) meta.output = output
|
|
108
|
+
|
|
109
|
+
const metrics = mlObsTags[METRICS] || {}
|
|
110
|
+
|
|
111
|
+
const mlApp = mlObsTags[ML_APP]
|
|
112
|
+
const sessionId = mlObsTags[SESSION_ID]
|
|
113
|
+
const parentId = mlObsTags[PARENT_ID_KEY]
|
|
114
|
+
|
|
115
|
+
const name = mlObsTags[NAME] || span._name
|
|
116
|
+
|
|
117
|
+
const llmObsSpanEvent = {
|
|
118
|
+
trace_id: span.context().toTraceId(true),
|
|
119
|
+
span_id: span.context().toSpanId(),
|
|
120
|
+
parent_id: parentId,
|
|
121
|
+
name,
|
|
122
|
+
tags: this._processTags(span, mlApp, sessionId, error),
|
|
123
|
+
start_ns: Math.round(span._startTime * 1e6),
|
|
124
|
+
duration: Math.round(span._duration * 1e6),
|
|
125
|
+
status: error ? 'error' : 'ok',
|
|
126
|
+
meta,
|
|
127
|
+
metrics,
|
|
128
|
+
_dd: {
|
|
129
|
+
span_id: span.context().toSpanId(),
|
|
130
|
+
trace_id: span.context().toTraceId(true)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (sessionId) llmObsSpanEvent.session_id = sessionId
|
|
135
|
+
|
|
136
|
+
return llmObsSpanEvent
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// For now, this only applies to metadata, as we let users annotate this field with any object
|
|
140
|
+
// However, we want to protect against circular references or BigInts (unserializable)
|
|
141
|
+
// This function can be reused for other fields if needed
|
|
142
|
+
// Messages, Documents, and Metrics are safeguarded in `llmobs/tagger.js`
|
|
143
|
+
_addObject (obj, carrier) {
|
|
144
|
+
const seenObjects = new WeakSet()
|
|
145
|
+
seenObjects.add(obj) // capture root object
|
|
146
|
+
|
|
147
|
+
const isCircular = value => {
|
|
148
|
+
if (typeof value !== 'object') return false
|
|
149
|
+
if (seenObjects.has(value)) return true
|
|
150
|
+
seenObjects.add(value)
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const add = (obj, carrier) => {
|
|
155
|
+
for (const key in obj) {
|
|
156
|
+
const value = obj[key]
|
|
157
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
|
|
158
|
+
if (typeof value === 'bigint' || isCircular(value)) {
|
|
159
|
+
// mark as unserializable instead of dropping
|
|
160
|
+
logger.warn(`Unserializable property found in metadata: ${key}`)
|
|
161
|
+
carrier[key] = UNSERIALIZABLE_VALUE_TEXT
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
if (typeof value === 'object') {
|
|
165
|
+
add(value, carrier[key] = {})
|
|
166
|
+
} else {
|
|
167
|
+
carrier[key] = value
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
add(obj, carrier)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_processTags (span, mlApp, sessionId, error) {
|
|
176
|
+
let tags = {
|
|
177
|
+
version: this._config.version,
|
|
178
|
+
env: this._config.env,
|
|
179
|
+
service: this._config.service,
|
|
180
|
+
source: 'integration',
|
|
181
|
+
ml_app: mlApp,
|
|
182
|
+
'ddtrace.version': tracerVersion,
|
|
183
|
+
error: Number(!!error) || 0,
|
|
184
|
+
language: 'javascript'
|
|
185
|
+
}
|
|
186
|
+
const errType = span.context()._tags[ERROR_TYPE] || error?.name
|
|
187
|
+
if (errType) tags.error_type = errType
|
|
188
|
+
if (sessionId) tags.session_id = sessionId
|
|
189
|
+
const existingTags = LLMObsTagger.tagMap.get(span)?.[TAGS] || {}
|
|
190
|
+
if (existingTags) tags = { ...tags, ...existingTags }
|
|
191
|
+
return Object.entries(tags).map(([key, value]) => `${key}:${value ?? ''}`)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = LLMObsSpanProcessor
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../log')
|
|
4
|
+
const {
|
|
5
|
+
MODEL_NAME,
|
|
6
|
+
MODEL_PROVIDER,
|
|
7
|
+
SESSION_ID,
|
|
8
|
+
ML_APP,
|
|
9
|
+
SPAN_KIND,
|
|
10
|
+
INPUT_VALUE,
|
|
11
|
+
OUTPUT_DOCUMENTS,
|
|
12
|
+
INPUT_DOCUMENTS,
|
|
13
|
+
OUTPUT_VALUE,
|
|
14
|
+
METADATA,
|
|
15
|
+
METRICS,
|
|
16
|
+
PARENT_ID_KEY,
|
|
17
|
+
INPUT_MESSAGES,
|
|
18
|
+
OUTPUT_MESSAGES,
|
|
19
|
+
TAGS,
|
|
20
|
+
NAME,
|
|
21
|
+
PROPAGATED_PARENT_ID_KEY,
|
|
22
|
+
ROOT_PARENT_ID,
|
|
23
|
+
INPUT_TOKENS_METRIC_KEY,
|
|
24
|
+
OUTPUT_TOKENS_METRIC_KEY,
|
|
25
|
+
TOTAL_TOKENS_METRIC_KEY
|
|
26
|
+
} = require('./constants/tags')
|
|
27
|
+
|
|
28
|
+
// global registry of LLMObs spans
|
|
29
|
+
// maps LLMObs spans to their annotations
|
|
30
|
+
const registry = new WeakMap()
|
|
31
|
+
|
|
32
|
+
class LLMObsTagger {
|
|
33
|
+
constructor (config, softFail = false) {
|
|
34
|
+
this._config = config
|
|
35
|
+
|
|
36
|
+
this.softFail = softFail
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static get tagMap () {
|
|
40
|
+
return registry
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
registerLLMObsSpan (span, {
|
|
44
|
+
modelName,
|
|
45
|
+
modelProvider,
|
|
46
|
+
sessionId,
|
|
47
|
+
mlApp,
|
|
48
|
+
parent,
|
|
49
|
+
kind,
|
|
50
|
+
name
|
|
51
|
+
} = {}) {
|
|
52
|
+
if (!this._config.llmobs.enabled) return
|
|
53
|
+
if (!kind) return // do not register it in the map if it doesn't have an llmobs span kind
|
|
54
|
+
|
|
55
|
+
this._register(span)
|
|
56
|
+
|
|
57
|
+
if (name) this._setTag(span, NAME, name)
|
|
58
|
+
|
|
59
|
+
this._setTag(span, SPAN_KIND, kind)
|
|
60
|
+
if (modelName) this._setTag(span, MODEL_NAME, modelName)
|
|
61
|
+
if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider)
|
|
62
|
+
|
|
63
|
+
sessionId = sessionId || parent?.context()._tags[SESSION_ID]
|
|
64
|
+
if (sessionId) this._setTag(span, SESSION_ID, sessionId)
|
|
65
|
+
|
|
66
|
+
if (!mlApp) mlApp = parent?.context()._tags[ML_APP] || this._config.llmobs.mlApp
|
|
67
|
+
this._setTag(span, ML_APP, mlApp)
|
|
68
|
+
|
|
69
|
+
const parentId =
|
|
70
|
+
parent?.context().toSpanId() ||
|
|
71
|
+
span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY] ||
|
|
72
|
+
ROOT_PARENT_ID
|
|
73
|
+
this._setTag(span, PARENT_ID_KEY, parentId)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// TODO: similarly for the following `tag` methods,
|
|
77
|
+
// how can we transition from a span weakmap to core API functionality
|
|
78
|
+
tagLLMIO (span, inputData, outputData) {
|
|
79
|
+
this._tagMessages(span, inputData, INPUT_MESSAGES)
|
|
80
|
+
this._tagMessages(span, outputData, OUTPUT_MESSAGES)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
tagEmbeddingIO (span, inputData, outputData) {
|
|
84
|
+
this._tagDocuments(span, inputData, INPUT_DOCUMENTS)
|
|
85
|
+
this._tagText(span, outputData, OUTPUT_VALUE)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
tagRetrievalIO (span, inputData, outputData) {
|
|
89
|
+
this._tagText(span, inputData, INPUT_VALUE)
|
|
90
|
+
this._tagDocuments(span, outputData, OUTPUT_DOCUMENTS)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
tagTextIO (span, inputData, outputData) {
|
|
94
|
+
this._tagText(span, inputData, INPUT_VALUE)
|
|
95
|
+
this._tagText(span, outputData, OUTPUT_VALUE)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
tagMetadata (span, metadata) {
|
|
99
|
+
this._setTag(span, METADATA, metadata)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
tagMetrics (span, metrics) {
|
|
103
|
+
const filterdMetrics = {}
|
|
104
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
105
|
+
let processedKey = key
|
|
106
|
+
|
|
107
|
+
// processing these specifically for our metrics ingestion
|
|
108
|
+
switch (key) {
|
|
109
|
+
case 'inputTokens':
|
|
110
|
+
processedKey = INPUT_TOKENS_METRIC_KEY
|
|
111
|
+
break
|
|
112
|
+
case 'outputTokens':
|
|
113
|
+
processedKey = OUTPUT_TOKENS_METRIC_KEY
|
|
114
|
+
break
|
|
115
|
+
case 'totalTokens':
|
|
116
|
+
processedKey = TOTAL_TOKENS_METRIC_KEY
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof value === 'number') {
|
|
121
|
+
filterdMetrics[processedKey] = value
|
|
122
|
+
} else {
|
|
123
|
+
this._handleFailure(`Value for metric '${key}' must be a number, instead got ${value}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this._setTag(span, METRICS, filterdMetrics)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
tagSpanTags (span, tags) {
|
|
131
|
+
// new tags will be merged with existing tags
|
|
132
|
+
const currentTags = registry.get(span)?.[TAGS]
|
|
133
|
+
if (currentTags) {
|
|
134
|
+
Object.assign(tags, currentTags)
|
|
135
|
+
}
|
|
136
|
+
this._setTag(span, TAGS, tags)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_tagText (span, data, key) {
|
|
140
|
+
if (data) {
|
|
141
|
+
if (typeof data === 'string') {
|
|
142
|
+
this._setTag(span, key, data)
|
|
143
|
+
} else {
|
|
144
|
+
try {
|
|
145
|
+
this._setTag(span, key, JSON.stringify(data))
|
|
146
|
+
} catch {
|
|
147
|
+
const type = key === INPUT_VALUE ? 'input' : 'output'
|
|
148
|
+
this._handleFailure(`Failed to parse ${type} value, must be JSON serializable.`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_tagDocuments (span, data, key) {
|
|
155
|
+
if (data) {
|
|
156
|
+
if (!Array.isArray(data)) {
|
|
157
|
+
data = [data]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const documents = data.map(document => {
|
|
161
|
+
if (typeof document === 'string') {
|
|
162
|
+
return { text: document }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (document == null || typeof document !== 'object') {
|
|
166
|
+
this._handleFailure('Documents must be a string, object, or list of objects.')
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { text, name, id, score } = document
|
|
171
|
+
let validDocument = true
|
|
172
|
+
|
|
173
|
+
if (typeof text !== 'string') {
|
|
174
|
+
this._handleFailure('Document text must be a string.')
|
|
175
|
+
validDocument = false
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const documentObj = { text }
|
|
179
|
+
|
|
180
|
+
validDocument = this._tagConditionalString(name, 'Document name', documentObj, 'name') && validDocument
|
|
181
|
+
validDocument = this._tagConditionalString(id, 'Document ID', documentObj, 'id') && validDocument
|
|
182
|
+
validDocument = this._tagConditionalNumber(score, 'Document score', documentObj, 'score') && validDocument
|
|
183
|
+
|
|
184
|
+
return validDocument ? documentObj : undefined
|
|
185
|
+
}).filter(doc => !!doc)
|
|
186
|
+
|
|
187
|
+
if (documents.length) {
|
|
188
|
+
this._setTag(span, key, documents)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_tagMessages (span, data, key) {
|
|
194
|
+
if (data) {
|
|
195
|
+
if (!Array.isArray(data)) {
|
|
196
|
+
data = [data]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const messages = data.map(message => {
|
|
200
|
+
if (typeof message === 'string') {
|
|
201
|
+
return { content: message }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (message == null || typeof message !== 'object') {
|
|
205
|
+
this._handleFailure('Messages must be a string, object, or list of objects')
|
|
206
|
+
return undefined
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let validMessage = true
|
|
210
|
+
|
|
211
|
+
const { content = '', role } = message
|
|
212
|
+
let toolCalls = message.toolCalls
|
|
213
|
+
const messageObj = { content }
|
|
214
|
+
|
|
215
|
+
if (typeof content !== 'string') {
|
|
216
|
+
this._handleFailure('Message content must be a string.')
|
|
217
|
+
validMessage = false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
validMessage = this._tagConditionalString(role, 'Message role', messageObj, 'role') && validMessage
|
|
221
|
+
|
|
222
|
+
if (toolCalls) {
|
|
223
|
+
if (!Array.isArray(toolCalls)) {
|
|
224
|
+
toolCalls = [toolCalls]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const filteredToolCalls = toolCalls.map(toolCall => {
|
|
228
|
+
if (typeof toolCall !== 'object') {
|
|
229
|
+
this._handleFailure('Tool call must be an object.')
|
|
230
|
+
return undefined
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let validTool = true
|
|
234
|
+
|
|
235
|
+
const { name, arguments: args, toolId, type } = toolCall
|
|
236
|
+
const toolCallObj = {}
|
|
237
|
+
|
|
238
|
+
validTool = this._tagConditionalString(name, 'Tool name', toolCallObj, 'name') && validTool
|
|
239
|
+
validTool = this._tagConditionalObject(args, 'Tool arguments', toolCallObj, 'arguments') && validTool
|
|
240
|
+
validTool = this._tagConditionalString(toolId, 'Tool ID', toolCallObj, 'tool_id') && validTool
|
|
241
|
+
validTool = this._tagConditionalString(type, 'Tool type', toolCallObj, 'type') && validTool
|
|
242
|
+
|
|
243
|
+
return validTool ? toolCallObj : undefined
|
|
244
|
+
}).filter(toolCall => !!toolCall)
|
|
245
|
+
|
|
246
|
+
if (filteredToolCalls.length) {
|
|
247
|
+
messageObj.tool_calls = filteredToolCalls
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return validMessage ? messageObj : undefined
|
|
252
|
+
}).filter(msg => !!msg)
|
|
253
|
+
|
|
254
|
+
if (messages.length) {
|
|
255
|
+
this._setTag(span, key, messages)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_tagConditionalString (data, type, carrier, key) {
|
|
261
|
+
if (!data) return true
|
|
262
|
+
if (typeof data !== 'string') {
|
|
263
|
+
this._handleFailure(`"${type}" must be a string.`)
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
carrier[key] = data
|
|
267
|
+
return true
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_tagConditionalNumber (data, type, carrier, key) {
|
|
271
|
+
if (!data) return true
|
|
272
|
+
if (typeof data !== 'number') {
|
|
273
|
+
this._handleFailure(`"${type}" must be a number.`)
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
carrier[key] = data
|
|
277
|
+
return true
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_tagConditionalObject (data, type, carrier, key) {
|
|
281
|
+
if (!data) return true
|
|
282
|
+
if (typeof data !== 'object') {
|
|
283
|
+
this._handleFailure(`"${type}" must be an object.`)
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
carrier[key] = data
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// any public-facing LLMObs APIs using this tagger should not soft fail
|
|
291
|
+
// auto-instrumentation should soft fail
|
|
292
|
+
_handleFailure (msg) {
|
|
293
|
+
if (this.softFail) {
|
|
294
|
+
log.warn(msg)
|
|
295
|
+
} else {
|
|
296
|
+
throw new Error(msg)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_register (span) {
|
|
301
|
+
if (!this._config.llmobs.enabled) return
|
|
302
|
+
if (registry.has(span)) {
|
|
303
|
+
this._handleFailure(`LLMObs Span "${span._name}" already registered.`)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
registry.set(span, {})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_setTag (span, key, value) {
|
|
311
|
+
if (!this._config.llmobs.enabled) return
|
|
312
|
+
if (!registry.has(span)) {
|
|
313
|
+
this._handleFailure('Span must be an LLMObs generated span.')
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const tagsCarrier = registry.get(span)
|
|
318
|
+
Object.assign(tagsCarrier, { [key]: value })
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = LLMObsTagger
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { SPAN_KINDS } = require('./constants/tags')
|
|
4
|
+
|
|
5
|
+
function encodeUnicode (str) {
|
|
6
|
+
if (!str) return str
|
|
7
|
+
return str.split('').map(char => {
|
|
8
|
+
const code = char.charCodeAt(0)
|
|
9
|
+
if (code > 127) {
|
|
10
|
+
return `\\u${code.toString(16).padStart(4, '0')}`
|
|
11
|
+
}
|
|
12
|
+
return char
|
|
13
|
+
}).join('')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validateKind (kind) {
|
|
17
|
+
if (!SPAN_KINDS.includes(kind)) {
|
|
18
|
+
throw new Error(`
|
|
19
|
+
Invalid span kind specified: "${kind}"
|
|
20
|
+
Must be one of: ${SPAN_KINDS.join(', ')}
|
|
21
|
+
`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return kind
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// extracts the argument names from a function string
|
|
28
|
+
function parseArgumentNames (str) {
|
|
29
|
+
const result = []
|
|
30
|
+
let current = ''
|
|
31
|
+
let closerCount = 0
|
|
32
|
+
let recording = true
|
|
33
|
+
let inSingleLineComment = false
|
|
34
|
+
let inMultiLineComment = false
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < str.length; i++) {
|
|
37
|
+
const char = str[i]
|
|
38
|
+
const nextChar = str[i + 1]
|
|
39
|
+
|
|
40
|
+
// Handle single-line comments
|
|
41
|
+
if (!inMultiLineComment && char === '/' && nextChar === '/') {
|
|
42
|
+
inSingleLineComment = true
|
|
43
|
+
i++ // Skip the next character
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle multi-line comments
|
|
48
|
+
if (!inSingleLineComment && char === '/' && nextChar === '*') {
|
|
49
|
+
inMultiLineComment = true
|
|
50
|
+
i++ // Skip the next character
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// End of single-line comment
|
|
55
|
+
if (inSingleLineComment && char === '\n') {
|
|
56
|
+
inSingleLineComment = false
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// End of multi-line comment
|
|
61
|
+
if (inMultiLineComment && char === '*' && nextChar === '/') {
|
|
62
|
+
inMultiLineComment = false
|
|
63
|
+
i++ // Skip the next character
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Skip characters inside comments
|
|
68
|
+
if (inSingleLineComment || inMultiLineComment) {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (['{', '[', '('].includes(char)) {
|
|
73
|
+
closerCount++
|
|
74
|
+
} else if (['}', ']', ')'].includes(char)) {
|
|
75
|
+
closerCount--
|
|
76
|
+
} else if (char === '=' && nextChar !== '>' && closerCount === 0) {
|
|
77
|
+
recording = false
|
|
78
|
+
// record the variable name early, and stop counting characters until we reach the next comma
|
|
79
|
+
result.push(current.trim())
|
|
80
|
+
current = ''
|
|
81
|
+
continue
|
|
82
|
+
} else if (char === ',' && closerCount === 0) {
|
|
83
|
+
if (recording) {
|
|
84
|
+
result.push(current.trim())
|
|
85
|
+
current = ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
recording = true
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (recording) {
|
|
93
|
+
current += char
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (current && recording) {
|
|
98
|
+
result.push(current.trim())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// finds the bounds of the arguments in a function string
|
|
105
|
+
function findArgumentsBounds (str) {
|
|
106
|
+
let start = -1
|
|
107
|
+
let end = -1
|
|
108
|
+
let closerCount = 0
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < str.length; i++) {
|
|
111
|
+
const char = str[i]
|
|
112
|
+
|
|
113
|
+
if (char === '(') {
|
|
114
|
+
if (closerCount === 0) {
|
|
115
|
+
start = i
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
closerCount++
|
|
119
|
+
} else if (char === ')') {
|
|
120
|
+
closerCount--
|
|
121
|
+
|
|
122
|
+
if (closerCount === 0) {
|
|
123
|
+
end = i
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { start, end }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const memo = new WeakMap()
|
|
133
|
+
function getFunctionArguments (fn, args = []) {
|
|
134
|
+
if (!fn) return
|
|
135
|
+
if (!args.length) return
|
|
136
|
+
if (args.length === 1) return args[0]
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
let names
|
|
140
|
+
if (memo.has(fn)) {
|
|
141
|
+
names = memo.get(fn)
|
|
142
|
+
} else {
|
|
143
|
+
const fnString = fn.toString()
|
|
144
|
+
const { start, end } = findArgumentsBounds(fnString)
|
|
145
|
+
names = parseArgumentNames(fnString.slice(start + 1, end))
|
|
146
|
+
memo.set(fn, names)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const argsObject = {}
|
|
150
|
+
|
|
151
|
+
for (const argIdx in args) {
|
|
152
|
+
const name = names[argIdx]
|
|
153
|
+
const arg = args[argIdx]
|
|
154
|
+
|
|
155
|
+
const spread = name?.startsWith('...')
|
|
156
|
+
|
|
157
|
+
// this can only be the last argument
|
|
158
|
+
if (spread) {
|
|
159
|
+
argsObject[name.slice(3)] = args.slice(argIdx)
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
argsObject[name] = arg
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return argsObject
|
|
167
|
+
} catch {
|
|
168
|
+
return args
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
encodeUnicode,
|
|
174
|
+
validateKind,
|
|
175
|
+
getFunctionArguments
|
|
176
|
+
}
|