dd-trace 5.65.0 → 5.67.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/index.d.ts +38 -0
- package/package.json +8 -8
- package/packages/datadog-instrumentations/src/express.js +3 -7
- package/packages/datadog-instrumentations/src/graphql.js +10 -6
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/register.js +10 -2
- package/packages/datadog-instrumentations/src/playwright.js +25 -9
- package/packages/datadog-instrumentations/src/prisma.js +8 -10
- package/packages/datadog-instrumentations/src/ws.js +136 -0
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +30 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +9 -6
- package/packages/datadog-plugin-aws-sdk/src/util.js +61 -1
- package/packages/datadog-plugin-express/src/code_origin.js +11 -0
- package/packages/datadog-plugin-graphql/src/index.js +3 -0
- package/packages/datadog-plugin-ws/src/close.js +69 -0
- package/packages/datadog-plugin-ws/src/index.js +26 -0
- package/packages/datadog-plugin-ws/src/producer.js +60 -0
- package/packages/datadog-plugin-ws/src/receiver.js +70 -0
- package/packages/datadog-plugin-ws/src/server.js +79 -0
- package/packages/datadog-shimmer/src/shimmer.js +11 -2
- package/packages/dd-trace/src/appsec/blocking.js +29 -0
- package/packages/dd-trace/src/appsec/channels.js +4 -2
- package/packages/dd-trace/src/appsec/index.js +7 -2
- package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +1 -0
- package/packages/dd-trace/src/appsec/rasp/index.js +25 -7
- package/packages/dd-trace/src/appsec/rasp/lfi.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/utils.js +13 -2
- package/packages/dd-trace/src/config.js +12 -0
- package/packages/dd-trace/src/guardrails/index.js +11 -3
- package/packages/dd-trace/src/guardrails/telemetry.js +15 -16
- package/packages/dd-trace/src/llmobs/index.js +7 -0
- package/packages/dd-trace/src/llmobs/sdk.js +28 -0
- package/packages/dd-trace/src/llmobs/span_processor.js +124 -28
- package/packages/dd-trace/src/llmobs/tagger.js +2 -1
- package/packages/dd-trace/src/llmobs/telemetry.js +7 -1
- package/packages/dd-trace/src/log/writer.js +1 -1
- package/packages/dd-trace/src/plugin_manager.js +8 -2
- package/packages/dd-trace/src/plugins/index.js +2 -1
- package/packages/dd-trace/src/plugins/util/ip_extractor.js +48 -45
- package/packages/dd-trace/src/profiling/profilers/wall.js +9 -3
- package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
- package/packages/dd-trace/src/service-naming/schemas/v0/websocket.js +30 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/websocket.js +30 -0
- package/packages/dd-trace/src/supported-configurations.json +3 -0
|
@@ -47,7 +47,7 @@ function shouldSend (point) {
|
|
|
47
47
|
return true
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function sendTelemetry (name, tags) {
|
|
50
|
+
function sendTelemetry (name, tags, resultMetadata) {
|
|
51
51
|
var points = name
|
|
52
52
|
if (typeof name === 'string') {
|
|
53
53
|
points = [{ name: name, tags: tags || [] }]
|
|
@@ -62,32 +62,31 @@ function sendTelemetry (name, tags) {
|
|
|
62
62
|
if (points.length === 0) {
|
|
63
63
|
return
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
// Update metadata with provided result metadata
|
|
67
|
+
var currentMetadata = {}
|
|
68
|
+
for (var key in metadata) {
|
|
69
|
+
currentMetadata[key] = metadata[key]
|
|
70
|
+
}
|
|
71
|
+
if (resultMetadata) {
|
|
72
|
+
for (var resultKey in resultMetadata) {
|
|
73
|
+
currentMetadata[resultKey] = resultMetadata[resultKey]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
var proc = spawn(process.env.DD_TELEMETRY_FORWARDER_PATH, ['library_entrypoint'], {
|
|
66
78
|
stdio: 'pipe'
|
|
67
79
|
})
|
|
68
80
|
proc.on('error', function () {
|
|
69
81
|
log.error('Failed to spawn telemetry forwarder')
|
|
70
|
-
metadata.result = 'error'
|
|
71
|
-
metadata.result_class = 'internal_error'
|
|
72
|
-
metadata.result_reason = 'Failed to spawn telemetry forwarder'
|
|
73
82
|
})
|
|
74
83
|
proc.on('exit', function (code) {
|
|
75
|
-
if (code
|
|
76
|
-
metadata.result = 'success'
|
|
77
|
-
metadata.result_class = 'success'
|
|
78
|
-
metadata.result_reason = 'Successfully configured ddtrace package'
|
|
79
|
-
} else {
|
|
84
|
+
if (code !== 0) {
|
|
80
85
|
log.error('Telemetry forwarder exited with code', code)
|
|
81
|
-
metadata.result = 'error'
|
|
82
|
-
metadata.result_class = 'internal_error'
|
|
83
|
-
metadata.result_reason = 'Telemetry forwarder exited with code ' + code
|
|
84
86
|
}
|
|
85
87
|
})
|
|
86
88
|
proc.stdin.on('error', function () {
|
|
87
89
|
log.error('Failed to write telemetry data to telemetry forwarder')
|
|
88
|
-
metadata.result = 'error'
|
|
89
|
-
metadata.result_class = 'internal_error'
|
|
90
|
-
metadata.result_reason = 'Failed to write telemetry data to telemetry forwarder'
|
|
91
90
|
})
|
|
92
|
-
proc.stdin.end(JSON.stringify({ metadata:
|
|
91
|
+
proc.stdin.end(JSON.stringify({ metadata: currentMetadata, points: points }))
|
|
93
92
|
}
|
|
@@ -16,6 +16,7 @@ const spanProcessCh = channel('dd-trace:span:process')
|
|
|
16
16
|
const evalMetricAppendCh = channel('llmobs:eval-metric:append')
|
|
17
17
|
const flushCh = channel('llmobs:writers:flush')
|
|
18
18
|
const injectCh = channel('dd-trace:span:inject')
|
|
19
|
+
const registerUserSpanProcessorCh = channel('llmobs:register-processor')
|
|
19
20
|
|
|
20
21
|
const LLMObsEvalMetricsWriter = require('./writers/evaluations')
|
|
21
22
|
const LLMObsTagger = require('./tagger')
|
|
@@ -56,6 +57,7 @@ function enable (config) {
|
|
|
56
57
|
|
|
57
58
|
evalMetricAppendCh.subscribe(handleEvalMetricAppend)
|
|
58
59
|
flushCh.subscribe(handleFlush)
|
|
60
|
+
registerUserSpanProcessorCh.subscribe(handleRegisterProcessor)
|
|
59
61
|
|
|
60
62
|
// span processing
|
|
61
63
|
spanProcessor = new LLMObsSpanProcessor(config)
|
|
@@ -86,6 +88,7 @@ function disable () {
|
|
|
86
88
|
if (flushCh.hasSubscribers) flushCh.unsubscribe(handleFlush)
|
|
87
89
|
if (spanProcessCh.hasSubscribers) spanProcessCh.unsubscribe(handleSpanProcess)
|
|
88
90
|
if (injectCh.hasSubscribers) injectCh.unsubscribe(handleLLMObsParentIdInjection)
|
|
91
|
+
if (registerUserSpanProcessorCh.hasSubscribers) registerUserSpanProcessorCh.unsubscribe(handleRegisterProcessor)
|
|
89
92
|
|
|
90
93
|
spanWriter?.destroy()
|
|
91
94
|
evalWriter?.destroy()
|
|
@@ -126,6 +129,10 @@ function handleFlush () {
|
|
|
126
129
|
telemetry.recordUserFlush(err)
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
function handleRegisterProcessor (userSpanProcessor) {
|
|
133
|
+
spanProcessor.setUserSpanProcessor(userSpanProcessor)
|
|
134
|
+
}
|
|
135
|
+
|
|
129
136
|
function handleSpanProcess (data) {
|
|
130
137
|
spanProcessor.process(data)
|
|
131
138
|
}
|
|
@@ -23,9 +23,16 @@ const LLMObsTagger = require('./tagger')
|
|
|
23
23
|
const { channel } = require('dc-polyfill')
|
|
24
24
|
const evalMetricAppendCh = channel('llmobs:eval-metric:append')
|
|
25
25
|
const flushCh = channel('llmobs:writers:flush')
|
|
26
|
+
const registerUserSpanProcessorCh = channel('llmobs:register-processor')
|
|
26
27
|
const NoopLLMObs = require('./noop')
|
|
27
28
|
|
|
28
29
|
class LLMObs extends NoopLLMObs {
|
|
30
|
+
/**
|
|
31
|
+
* flag representing if a user span processor has been registered
|
|
32
|
+
* @type {boolean}
|
|
33
|
+
*/
|
|
34
|
+
#hasUserSpanProcessor = false
|
|
35
|
+
|
|
29
36
|
constructor (tracer, llmobsModule, config) {
|
|
30
37
|
super(tracer)
|
|
31
38
|
|
|
@@ -309,6 +316,27 @@ class LLMObs extends NoopLLMObs {
|
|
|
309
316
|
}
|
|
310
317
|
}
|
|
311
318
|
|
|
319
|
+
registerProcessor (processor) {
|
|
320
|
+
if (!this.enabled) return
|
|
321
|
+
|
|
322
|
+
if (this.#hasUserSpanProcessor) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
'[LLMObs] Only one user span processor can be registered. ' +
|
|
325
|
+
'To register a new processor, deregister the existing processor first using `llmobs.deregisterProcessor()`.'
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.#hasUserSpanProcessor = true
|
|
330
|
+
registerUserSpanProcessorCh.publish(processor)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
deregisterProcessor () {
|
|
334
|
+
if (!this.enabled) return
|
|
335
|
+
|
|
336
|
+
this.#hasUserSpanProcessor = false
|
|
337
|
+
registerUserSpanProcessorCh.publish(null)
|
|
338
|
+
}
|
|
339
|
+
|
|
312
340
|
submitEvaluation (llmobsSpanContext, options = {}) {
|
|
313
341
|
if (!this.enabled) return
|
|
314
342
|
|
|
@@ -34,25 +34,55 @@ const LLMObsTagger = require('./tagger')
|
|
|
34
34
|
const tracerVersion = require('../../../../package.json').version
|
|
35
35
|
const logger = require('../log')
|
|
36
36
|
|
|
37
|
+
const util = require('node:util')
|
|
38
|
+
|
|
39
|
+
class LLMObservabilitySpan {
|
|
40
|
+
constructor () {
|
|
41
|
+
this.input = []
|
|
42
|
+
this.output = []
|
|
43
|
+
|
|
44
|
+
this._tags = {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getTag (key) {
|
|
48
|
+
return this._tags[key]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
class LLMObsSpanProcessor {
|
|
53
|
+
/** @type {import('../config')} */
|
|
54
|
+
#config
|
|
55
|
+
|
|
56
|
+
/** @type {((span: LLMObservabilitySpan) => LLMObservabilitySpan | null) | null} */
|
|
57
|
+
#userSpanProcessor
|
|
58
|
+
|
|
59
|
+
/** @type {import('./writers/spans')} */
|
|
60
|
+
#writer
|
|
61
|
+
|
|
38
62
|
constructor (config) {
|
|
39
|
-
this
|
|
63
|
+
this.#config = config
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setUserSpanProcessor (userSpanProcessor) {
|
|
67
|
+
this.#userSpanProcessor = userSpanProcessor
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
setWriter (writer) {
|
|
43
|
-
this
|
|
71
|
+
this.#writer = writer
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
// TODO: instead of relying on the tagger's weakmap registry, can we use some namespaced storage correlation?
|
|
47
75
|
process ({ span }) {
|
|
48
|
-
if (!this.
|
|
76
|
+
if (!this.#config.llmobs.enabled) return
|
|
49
77
|
// if the span is not in our private tagger map, it is not an llmobs span
|
|
50
78
|
if (!LLMObsTagger.tagMap.has(span)) return
|
|
51
79
|
|
|
52
80
|
try {
|
|
53
81
|
const formattedEvent = this.format(span)
|
|
54
82
|
telemetry.incrementLLMObsSpanFinishedCount(span)
|
|
55
|
-
|
|
83
|
+
if (formattedEvent == null) return
|
|
84
|
+
|
|
85
|
+
this.#writer.append(formattedEvent)
|
|
56
86
|
} catch (e) {
|
|
57
87
|
// this should be a rare case
|
|
58
88
|
// we protect against unserializable properties in the format function, and in
|
|
@@ -65,6 +95,9 @@ class LLMObsSpanProcessor {
|
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
format (span) {
|
|
98
|
+
const llmObsSpan = new LLMObservabilitySpan()
|
|
99
|
+
let inputType, outputType
|
|
100
|
+
|
|
68
101
|
const spanTags = span.context()._tags
|
|
69
102
|
const mlObsTags = LLMObsTagger.tagMap.get(span)
|
|
70
103
|
|
|
@@ -78,26 +111,29 @@ class LLMObsSpanProcessor {
|
|
|
78
111
|
meta.model_name = mlObsTags[MODEL_NAME] || 'custom'
|
|
79
112
|
meta.model_provider = (mlObsTags[MODEL_PROVIDER] || 'custom').toLowerCase()
|
|
80
113
|
}
|
|
114
|
+
|
|
81
115
|
if (mlObsTags[METADATA]) {
|
|
82
|
-
this
|
|
116
|
+
this.#addObject(mlObsTags[METADATA], meta.metadata = {})
|
|
83
117
|
}
|
|
118
|
+
|
|
84
119
|
if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
|
|
85
|
-
input
|
|
86
|
-
|
|
87
|
-
if (mlObsTags[
|
|
88
|
-
input.value = mlObsTags[INPUT_VALUE]
|
|
89
|
-
}
|
|
90
|
-
if (spanKind === 'llm' && mlObsTags[OUTPUT_MESSAGES]) {
|
|
91
|
-
output.messages = mlObsTags[OUTPUT_MESSAGES]
|
|
92
|
-
}
|
|
93
|
-
if (spanKind === 'embedding' && mlObsTags[INPUT_DOCUMENTS]) {
|
|
120
|
+
llmObsSpan.input = mlObsTags[INPUT_MESSAGES]
|
|
121
|
+
inputType = 'messages'
|
|
122
|
+
} else if (spanKind === 'embedding' && mlObsTags[INPUT_DOCUMENTS]) {
|
|
94
123
|
input.documents = mlObsTags[INPUT_DOCUMENTS]
|
|
124
|
+
} else if (mlObsTags[INPUT_VALUE]) {
|
|
125
|
+
llmObsSpan.input = [{ role: '', content: mlObsTags[INPUT_VALUE] }]
|
|
126
|
+
inputType = 'value'
|
|
95
127
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
128
|
+
|
|
129
|
+
if (spanKind === 'llm' && mlObsTags[OUTPUT_MESSAGES]) {
|
|
130
|
+
llmObsSpan.output = mlObsTags[OUTPUT_MESSAGES]
|
|
131
|
+
outputType = 'messages'
|
|
132
|
+
} else if (spanKind === 'retrieval' && mlObsTags[OUTPUT_DOCUMENTS]) {
|
|
100
133
|
output.documents = mlObsTags[OUTPUT_DOCUMENTS]
|
|
134
|
+
} else if (mlObsTags[OUTPUT_VALUE]) {
|
|
135
|
+
llmObsSpan.output = [{ role: '', content: mlObsTags[OUTPUT_VALUE] }]
|
|
136
|
+
outputType = 'value'
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
const error = spanTags.error || spanTags[ERROR_TYPE]
|
|
@@ -107,9 +143,6 @@ class LLMObsSpanProcessor {
|
|
|
107
143
|
meta[ERROR_STACK] = spanTags[ERROR_STACK] || error.stack
|
|
108
144
|
}
|
|
109
145
|
|
|
110
|
-
if (input) meta.input = input
|
|
111
|
-
if (output) meta.output = output
|
|
112
|
-
|
|
113
146
|
const metrics = mlObsTags[METRICS] || {}
|
|
114
147
|
|
|
115
148
|
const mlApp = mlObsTags[ML_APP]
|
|
@@ -118,12 +151,37 @@ class LLMObsSpanProcessor {
|
|
|
118
151
|
|
|
119
152
|
const name = mlObsTags[NAME] || span._name
|
|
120
153
|
|
|
154
|
+
const tags = this.#getTags(span, mlApp, sessionId, error)
|
|
155
|
+
llmObsSpan._tags = tags
|
|
156
|
+
|
|
157
|
+
const processedSpan = this.#runProcessor(llmObsSpan)
|
|
158
|
+
if (processedSpan === null) return null
|
|
159
|
+
|
|
160
|
+
if (processedSpan.input) {
|
|
161
|
+
if (inputType === 'messages') {
|
|
162
|
+
input.messages = processedSpan.input
|
|
163
|
+
} else if (inputType === 'value') {
|
|
164
|
+
input.value = processedSpan.input[0].content
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (processedSpan.output) {
|
|
169
|
+
if (outputType === 'messages') {
|
|
170
|
+
output.messages = processedSpan.output
|
|
171
|
+
} else if (outputType === 'value') {
|
|
172
|
+
output.value = processedSpan.output[0].content
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (input) meta.input = input
|
|
177
|
+
if (output) meta.output = output
|
|
178
|
+
|
|
121
179
|
const llmObsSpanEvent = {
|
|
122
180
|
trace_id: span.context().toTraceId(true),
|
|
123
181
|
span_id: span.context().toSpanId(),
|
|
124
182
|
parent_id: parentId,
|
|
125
183
|
name,
|
|
126
|
-
tags: this
|
|
184
|
+
tags: this.#objectTagsToStringArrayTags(tags),
|
|
127
185
|
start_ns: Math.round(span._startTime * 1e6),
|
|
128
186
|
duration: Math.round(span._duration * 1e6),
|
|
129
187
|
status: error ? 'error' : 'ok',
|
|
@@ -144,7 +202,7 @@ class LLMObsSpanProcessor {
|
|
|
144
202
|
// However, we want to protect against circular references or BigInts (unserializable)
|
|
145
203
|
// This function can be reused for other fields if needed
|
|
146
204
|
// Messages, Documents, and Metrics are safeguarded in `llmobs/tagger.js`
|
|
147
|
-
|
|
205
|
+
#addObject (obj, carrier) {
|
|
148
206
|
const seenObjects = new WeakSet()
|
|
149
207
|
seenObjects.add(obj) // capture root object
|
|
150
208
|
|
|
@@ -176,12 +234,12 @@ class LLMObsSpanProcessor {
|
|
|
176
234
|
add(obj, carrier)
|
|
177
235
|
}
|
|
178
236
|
|
|
179
|
-
|
|
237
|
+
#getTags (span, mlApp, sessionId, error) {
|
|
180
238
|
let tags = {
|
|
181
|
-
...this.
|
|
182
|
-
version: this.
|
|
183
|
-
env: this.
|
|
184
|
-
service: this.
|
|
239
|
+
...this.#config.parsedDdTags,
|
|
240
|
+
version: this.#config.version,
|
|
241
|
+
env: this.#config.env,
|
|
242
|
+
service: this.#config.service,
|
|
185
243
|
source: 'integration',
|
|
186
244
|
ml_app: mlApp,
|
|
187
245
|
'ddtrace.version': tracerVersion,
|
|
@@ -191,13 +249,51 @@ class LLMObsSpanProcessor {
|
|
|
191
249
|
|
|
192
250
|
const errType = span.context()._tags[ERROR_TYPE] || error?.name
|
|
193
251
|
if (errType) tags.error_type = errType
|
|
252
|
+
|
|
194
253
|
if (sessionId) tags.session_id = sessionId
|
|
254
|
+
|
|
195
255
|
const integration = LLMObsTagger.tagMap.get(span)?.[INTEGRATION]
|
|
196
256
|
if (integration) tags.integration = integration
|
|
257
|
+
|
|
197
258
|
const existingTags = LLMObsTagger.tagMap.get(span)?.[TAGS] || {}
|
|
198
259
|
if (existingTags) tags = { ...tags, ...existingTags }
|
|
260
|
+
|
|
261
|
+
return tags
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#objectTagsToStringArrayTags (tags) {
|
|
199
265
|
return Object.entries(tags).map(([key, value]) => `${key}:${value ?? ''}`)
|
|
200
266
|
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Runs the user span processor, emitting telemetry and adding some guardrails against invalid return types
|
|
270
|
+
* @param {LLMObservabilitySpan} span
|
|
271
|
+
* @returns {LLMObservabilitySpan | null}
|
|
272
|
+
*/
|
|
273
|
+
#runProcessor (span) {
|
|
274
|
+
const processor = this.#userSpanProcessor
|
|
275
|
+
if (!processor) return span
|
|
276
|
+
|
|
277
|
+
let error = false
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const processedLLMObsSpan = processor(span)
|
|
281
|
+
if (processedLLMObsSpan === null) return null
|
|
282
|
+
|
|
283
|
+
if (!(processedLLMObsSpan instanceof LLMObservabilitySpan)) {
|
|
284
|
+
error = true
|
|
285
|
+
logger.warn('User span processor must return an instance of an LLMObservabilitySpan or null, dropping span.')
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return processedLLMObsSpan
|
|
290
|
+
} catch (e) {
|
|
291
|
+
logger.error(`[LLMObs] Error in LLMObs span processor (${util.inspect(processor)}): ${util.inspect(e)}`)
|
|
292
|
+
error = true
|
|
293
|
+
} finally {
|
|
294
|
+
telemetry.recordLLMObsUserProcessorCalled(error)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
201
297
|
}
|
|
202
298
|
|
|
203
299
|
module.exports = LLMObsSpanProcessor
|
|
@@ -65,7 +65,8 @@ class LLMObsTagger {
|
|
|
65
65
|
mlApp ||
|
|
66
66
|
registry.get(parent)?.[ML_APP] ||
|
|
67
67
|
span.context()._trace.tags[PROPAGATED_ML_APP_KEY] ||
|
|
68
|
-
this._config.llmobs.mlApp
|
|
68
|
+
this._config.llmobs.mlApp ||
|
|
69
|
+
this._config.service // this should always have a default
|
|
69
70
|
|
|
70
71
|
if (!spanMlApp) {
|
|
71
72
|
throw new Error(
|
|
@@ -154,6 +154,11 @@ function recordSubmitEvaluation (options, err, value = 1) {
|
|
|
154
154
|
llmobsMetrics.count('evals_submitted', tags).inc(value)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function recordLLMObsUserProcessorCalled (error, value = 1) {
|
|
158
|
+
const tags = { error: error ? 1 : 0 }
|
|
159
|
+
llmobsMetrics.count('user_processor_called', tags).inc(value)
|
|
160
|
+
}
|
|
161
|
+
|
|
157
162
|
module.exports = {
|
|
158
163
|
recordLLMObsEnabled,
|
|
159
164
|
incrementLLMObsSpanStartCount,
|
|
@@ -164,5 +169,6 @@ module.exports = {
|
|
|
164
169
|
recordLLMObsAnnotate,
|
|
165
170
|
recordUserFlush,
|
|
166
171
|
recordExportSpan,
|
|
167
|
-
recordSubmitEvaluation
|
|
172
|
+
recordSubmitEvaluation,
|
|
173
|
+
recordLLMObsUserProcessorCalled
|
|
168
174
|
}
|
|
@@ -71,7 +71,7 @@ function setStackTraceLimitFunction (fn) {
|
|
|
71
71
|
function onError (err) {
|
|
72
72
|
const { formatted, cause } = getErrorLog(err)
|
|
73
73
|
|
|
74
|
-
// calling twice logger.error() because Error cause is only available in
|
|
74
|
+
// calling twice logger.error() because Error cause is only available in Node.js v16.9.0
|
|
75
75
|
// TODO: replace it with Error(message, { cause }) when cause has broad support
|
|
76
76
|
if (formatted) {
|
|
77
77
|
withNoop(() => {
|
|
@@ -145,7 +145,10 @@ module.exports = class PluginManager {
|
|
|
145
145
|
ciVisAgentlessLogSubmissionEnabled,
|
|
146
146
|
isTestDynamicInstrumentationEnabled,
|
|
147
147
|
isServiceUserProvided,
|
|
148
|
-
middlewareTracingEnabled
|
|
148
|
+
middlewareTracingEnabled,
|
|
149
|
+
traceWebsocketMessagesEnabled,
|
|
150
|
+
traceWebsocketMessagesInheritSampling,
|
|
151
|
+
traceWebsocketMessagesSeparateTraces
|
|
149
152
|
} = this._tracerConfig
|
|
150
153
|
|
|
151
154
|
const sharedConfig = {
|
|
@@ -160,7 +163,10 @@ module.exports = class PluginManager {
|
|
|
160
163
|
ciVisibilityTestSessionName,
|
|
161
164
|
ciVisAgentlessLogSubmissionEnabled,
|
|
162
165
|
isTestDynamicInstrumentationEnabled,
|
|
163
|
-
isServiceUserProvided
|
|
166
|
+
isServiceUserProvided,
|
|
167
|
+
traceWebsocketMessagesEnabled,
|
|
168
|
+
traceWebsocketMessagesInheritSampling,
|
|
169
|
+
traceWebsocketMessagesSeparateTraces
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
if (logInjection !== undefined) {
|
|
@@ -102,5 +102,6 @@ module.exports = {
|
|
|
102
102
|
get sharedb () { return require('../../../datadog-plugin-sharedb/src') },
|
|
103
103
|
get tedious () { return require('../../../datadog-plugin-tedious/src') },
|
|
104
104
|
get undici () { return require('../../../datadog-plugin-undici/src') },
|
|
105
|
-
get winston () { return require('../../../datadog-plugin-winston/src') }
|
|
105
|
+
get winston () { return require('../../../datadog-plugin-winston/src') },
|
|
106
|
+
get ws () { return require('../../../datadog-plugin-ws/src') }
|
|
106
107
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { BlockList } = require('net')
|
|
4
3
|
const net = require('net')
|
|
5
4
|
|
|
6
5
|
const FORWARED_HEADER_NAME = 'forwarded'
|
|
@@ -32,7 +31,7 @@ const privateCIDRs = [
|
|
|
32
31
|
'fd00::/8'
|
|
33
32
|
]
|
|
34
33
|
|
|
35
|
-
const privateIPMatcher = new BlockList()
|
|
34
|
+
const privateIPMatcher = new net.BlockList()
|
|
36
35
|
|
|
37
36
|
for (const cidr of privateCIDRs) {
|
|
38
37
|
const [address, prefix] = cidr.split('/')
|
|
@@ -45,7 +44,11 @@ function extractIp (config, req) {
|
|
|
45
44
|
if (config.clientIpHeader) {
|
|
46
45
|
if (!headers) return
|
|
47
46
|
|
|
48
|
-
const
|
|
47
|
+
const ipHeaderName = config.clientIpHeader
|
|
48
|
+
const header = headers[ipHeaderName]
|
|
49
|
+
if (typeof header !== 'string') return
|
|
50
|
+
|
|
51
|
+
const ip = findFirstIp(header, ipHeaderName === FORWARED_HEADER_NAME)
|
|
49
52
|
return ip.public || ip.private
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -53,12 +56,14 @@ function extractIp (config, req) {
|
|
|
53
56
|
if (headers) {
|
|
54
57
|
for (const ipHeaderName of ipHeaderList) {
|
|
55
58
|
const header = headers[ipHeaderName]
|
|
56
|
-
|
|
59
|
+
if (typeof header !== 'string') continue
|
|
60
|
+
|
|
61
|
+
const ip = findFirstIp(header, ipHeaderName === FORWARED_HEADER_NAME)
|
|
57
62
|
|
|
58
|
-
if (
|
|
59
|
-
return
|
|
60
|
-
} else if (!firstPrivateIp &&
|
|
61
|
-
firstPrivateIp =
|
|
63
|
+
if (ip.public) {
|
|
64
|
+
return ip.public
|
|
65
|
+
} else if (!firstPrivateIp && ip.private) {
|
|
66
|
+
firstPrivateIp = ip.private
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
}
|
|
@@ -66,25 +71,39 @@ function extractIp (config, req) {
|
|
|
66
71
|
return firstPrivateIp || req.socket?.remoteAddress
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
function
|
|
70
|
-
return !privateIPMatcher.check(ip, type === 6 ? 'ipv6' : 'ipv4')
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function findFirstIp (str) {
|
|
74
|
+
function findFirstIp (str, isForwardedHeader) {
|
|
74
75
|
const result = {}
|
|
75
76
|
if (!str) return result
|
|
76
77
|
|
|
77
78
|
const splitted = str.split(',')
|
|
78
79
|
|
|
79
|
-
for (
|
|
80
|
-
|
|
80
|
+
for (let chunk of splitted) {
|
|
81
|
+
if (isForwardedHeader) {
|
|
82
|
+
// find "for" directive
|
|
83
|
+
const forDirective = chunk.split(';').find(subchunk => subchunk.trim().toLowerCase().startsWith('for='))
|
|
84
|
+
|
|
85
|
+
// if found remove the "for=" prefix
|
|
86
|
+
// else keep going as is
|
|
87
|
+
if (forDirective) {
|
|
88
|
+
chunk = forDirective.slice(4)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
chunk = chunk.trim()
|
|
93
|
+
|
|
94
|
+
// trim potential double quotes
|
|
95
|
+
if (chunk.startsWith('"') && chunk.endsWith('"')) {
|
|
96
|
+
chunk = chunk.slice(1, -1).trim()
|
|
97
|
+
}
|
|
81
98
|
|
|
82
|
-
// TODO:
|
|
99
|
+
// TODO: when min node support is v24 we can instead use net.SocketAddress.parse()
|
|
100
|
+
chunk = cleanIp(chunk)
|
|
101
|
+
if (!chunk) continue
|
|
83
102
|
|
|
84
103
|
const type = net.isIP(chunk)
|
|
85
104
|
if (!type) continue
|
|
86
105
|
|
|
87
|
-
if (
|
|
106
|
+
if (!privateIPMatcher.check(chunk, type === 6 ? 'ipv6' : 'ipv4')) {
|
|
88
107
|
// it's public, return it immediately
|
|
89
108
|
result.public = chunk
|
|
90
109
|
return result
|
|
@@ -97,37 +116,21 @@ function findFirstIp (str) {
|
|
|
97
116
|
return result
|
|
98
117
|
}
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (!str) return result
|
|
107
|
-
|
|
108
|
-
const splitted = str.split(',')
|
|
109
|
-
|
|
110
|
-
for (const part of splitted) {
|
|
111
|
-
const chunk = part.trim()
|
|
112
|
-
|
|
113
|
-
for (const regex of forwardedRegexps) {
|
|
114
|
-
const ip = regex.exec(chunk)?.[1]
|
|
115
|
-
|
|
116
|
-
const type = net.isIP(ip)
|
|
117
|
-
if (!type) continue
|
|
118
|
-
|
|
119
|
-
if (isPublicIp(ip, type)) {
|
|
120
|
-
// it's public, return it immediately
|
|
121
|
-
result.public = ip
|
|
122
|
-
return result
|
|
123
|
-
}
|
|
119
|
+
function cleanIp (input) {
|
|
120
|
+
const colonIndex = input.indexOf(':')
|
|
121
|
+
if (colonIndex !== -1 && input.includes('.')) {
|
|
122
|
+
// treat it as ipv4 with port
|
|
123
|
+
return input.slice(0, colonIndex).trim()
|
|
124
|
+
}
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const closingBracketIndex = input.indexOf(']')
|
|
127
|
+
if (closingBracketIndex !== -1 && input.startsWith('[')) {
|
|
128
|
+
// treat as ipv6 with brackets
|
|
129
|
+
return input.slice(1, closingBracketIndex).trim()
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
// no need to clean it
|
|
133
|
+
return input
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
module.exports = {
|
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
getThreadLabels,
|
|
14
14
|
encodeProfileAsync
|
|
15
15
|
} = require('./shared')
|
|
16
|
+
const TRACE_ENDPOINT_LABEL = 'trace endpoint'
|
|
16
17
|
|
|
17
18
|
const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils')
|
|
18
19
|
|
|
@@ -251,7 +252,12 @@ class NativeWallProfiler {
|
|
|
251
252
|
this._enter()
|
|
252
253
|
this._lastSampleCount = 0
|
|
253
254
|
}
|
|
254
|
-
|
|
255
|
+
|
|
256
|
+
// Mark thread labels and trace endpoint label as good deduplication candidates
|
|
257
|
+
const lowCardinalityLabels = Object.keys(getThreadLabels())
|
|
258
|
+
lowCardinalityLabels.push(TRACE_ENDPOINT_LABEL)
|
|
259
|
+
|
|
260
|
+
const profile = this._pprof.time.stop(restart, this._generateLabels, lowCardinalityLabels)
|
|
255
261
|
|
|
256
262
|
if (restart) {
|
|
257
263
|
const v8BugDetected = this._pprof.time.v8ProfilerStuckEventLoopDetected()
|
|
@@ -312,10 +318,10 @@ class NativeWallProfiler {
|
|
|
312
318
|
labels[LOCAL_ROOT_SPAN_ID_LABEL] = rootSpanId
|
|
313
319
|
}
|
|
314
320
|
if (webTags !== undefined && Object.keys(webTags).length !== 0) {
|
|
315
|
-
labels[
|
|
321
|
+
labels[TRACE_ENDPOINT_LABEL] = endpointNameFromTags(webTags)
|
|
316
322
|
} else if (endpoint) {
|
|
317
323
|
// fallback to endpoint computed when sample was taken
|
|
318
|
-
labels[
|
|
324
|
+
labels[TRACE_ENDPOINT_LABEL] = endpoint
|
|
319
325
|
}
|
|
320
326
|
|
|
321
327
|
return labels
|
|
@@ -6,5 +6,6 @@ const storage = require('./storage')
|
|
|
6
6
|
const graphql = require('./graphql')
|
|
7
7
|
const web = require('./web')
|
|
8
8
|
const serverless = require('./serverless')
|
|
9
|
+
const websocket = require('./websocket')
|
|
9
10
|
|
|
10
|
-
module.exports = new SchemaDefinition({ messaging, storage, web, graphql, serverless })
|
|
11
|
+
module.exports = new SchemaDefinition({ messaging, storage, web, graphql, serverless, websocket })
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const websocket = {
|
|
4
|
+
request: {
|
|
5
|
+
ws: {
|
|
6
|
+
opName: () => 'web.request',
|
|
7
|
+
serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
producer: {
|
|
11
|
+
ws: {
|
|
12
|
+
opName: () => 'websocket.send',
|
|
13
|
+
serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
consumer: {
|
|
17
|
+
ws: {
|
|
18
|
+
opName: () => 'websocket.receive',
|
|
19
|
+
serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
close: {
|
|
23
|
+
ws: {
|
|
24
|
+
opName: () => 'websocket.close',
|
|
25
|
+
serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = websocket
|
|
@@ -6,5 +6,6 @@ const storage = require('./storage')
|
|
|
6
6
|
const graphql = require('./graphql')
|
|
7
7
|
const web = require('./web')
|
|
8
8
|
const serverless = require('./serverless')
|
|
9
|
+
const websocket = require('./websocket')
|
|
9
10
|
|
|
10
|
-
module.exports = new SchemaDefinition({ messaging, storage, web, graphql, serverless })
|
|
11
|
+
module.exports = new SchemaDefinition({ messaging, storage, web, graphql, serverless, websocket })
|