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.
Files changed (45) hide show
  1. package/index.d.ts +38 -0
  2. package/package.json +8 -8
  3. package/packages/datadog-instrumentations/src/express.js +3 -7
  4. package/packages/datadog-instrumentations/src/graphql.js +10 -6
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  6. package/packages/datadog-instrumentations/src/helpers/register.js +10 -2
  7. package/packages/datadog-instrumentations/src/playwright.js +25 -9
  8. package/packages/datadog-instrumentations/src/prisma.js +8 -10
  9. package/packages/datadog-instrumentations/src/ws.js +136 -0
  10. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +30 -3
  11. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +9 -6
  12. package/packages/datadog-plugin-aws-sdk/src/util.js +61 -1
  13. package/packages/datadog-plugin-express/src/code_origin.js +11 -0
  14. package/packages/datadog-plugin-graphql/src/index.js +3 -0
  15. package/packages/datadog-plugin-ws/src/close.js +69 -0
  16. package/packages/datadog-plugin-ws/src/index.js +26 -0
  17. package/packages/datadog-plugin-ws/src/producer.js +60 -0
  18. package/packages/datadog-plugin-ws/src/receiver.js +70 -0
  19. package/packages/datadog-plugin-ws/src/server.js +79 -0
  20. package/packages/datadog-shimmer/src/shimmer.js +11 -2
  21. package/packages/dd-trace/src/appsec/blocking.js +29 -0
  22. package/packages/dd-trace/src/appsec/channels.js +4 -2
  23. package/packages/dd-trace/src/appsec/index.js +7 -2
  24. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +1 -0
  25. package/packages/dd-trace/src/appsec/rasp/index.js +25 -7
  26. package/packages/dd-trace/src/appsec/rasp/lfi.js +1 -1
  27. package/packages/dd-trace/src/appsec/rasp/utils.js +13 -2
  28. package/packages/dd-trace/src/config.js +12 -0
  29. package/packages/dd-trace/src/guardrails/index.js +11 -3
  30. package/packages/dd-trace/src/guardrails/telemetry.js +15 -16
  31. package/packages/dd-trace/src/llmobs/index.js +7 -0
  32. package/packages/dd-trace/src/llmobs/sdk.js +28 -0
  33. package/packages/dd-trace/src/llmobs/span_processor.js +124 -28
  34. package/packages/dd-trace/src/llmobs/tagger.js +2 -1
  35. package/packages/dd-trace/src/llmobs/telemetry.js +7 -1
  36. package/packages/dd-trace/src/log/writer.js +1 -1
  37. package/packages/dd-trace/src/plugin_manager.js +8 -2
  38. package/packages/dd-trace/src/plugins/index.js +2 -1
  39. package/packages/dd-trace/src/plugins/util/ip_extractor.js +48 -45
  40. package/packages/dd-trace/src/profiling/profilers/wall.js +9 -3
  41. package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
  42. package/packages/dd-trace/src/service-naming/schemas/v0/websocket.js +30 -0
  43. package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
  44. package/packages/dd-trace/src/service-naming/schemas/v1/websocket.js +30 -0
  45. 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 === 0) {
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: metadata, points: points }))
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._config = config
63
+ this.#config = config
64
+ }
65
+
66
+ setUserSpanProcessor (userSpanProcessor) {
67
+ this.#userSpanProcessor = userSpanProcessor
40
68
  }
41
69
 
42
70
  setWriter (writer) {
43
- this._writer = writer
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._config.llmobs.enabled) return
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
- this._writer.append(formattedEvent)
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._addObject(mlObsTags[METADATA], meta.metadata = {})
116
+ this.#addObject(mlObsTags[METADATA], meta.metadata = {})
83
117
  }
118
+
84
119
  if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
85
- input.messages = mlObsTags[INPUT_MESSAGES]
86
- }
87
- if (mlObsTags[INPUT_VALUE]) {
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
- if (mlObsTags[OUTPUT_VALUE]) {
97
- output.value = mlObsTags[OUTPUT_VALUE]
98
- }
99
- if (spanKind === 'retrieval' && mlObsTags[OUTPUT_DOCUMENTS]) {
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._processTags(span, mlApp, sessionId, error),
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
- _addObject (obj, carrier) {
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
- _processTags (span, mlApp, sessionId, error) {
237
+ #getTags (span, mlApp, sessionId, error) {
180
238
  let tags = {
181
- ...this._config.parsedDdTags,
182
- version: this._config.version,
183
- env: this._config.env,
184
- service: this._config.service,
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 nodejs v16.9.0
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 ip = findFirstIp(headers[config.clientIpHeader])
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
- const firstIp = ipHeaderName === FORWARED_HEADER_NAME ? findFirstIpForwardedFormat(header) : findFirstIp(header)
59
+ if (typeof header !== 'string') continue
60
+
61
+ const ip = findFirstIp(header, ipHeaderName === FORWARED_HEADER_NAME)
57
62
 
58
- if (firstIp.public) {
59
- return firstIp.public
60
- } else if (!firstPrivateIp && firstIp.private) {
61
- firstPrivateIp = firstIp.private
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 isPublicIp (ip, type) {
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 (const part of splitted) {
80
- const chunk = part.trim()
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: strip port and interface data ?
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 (isPublicIp(chunk, type)) {
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
- const forwardedForRegexp = /for="?\[?(([0-9]+\.)+[0-9]+|[0-9a-f:]*:[0-9a-f]*)/i
101
- const forwardedByRegexp = /by="?\[?(([0-9]+\.)+[0-9]+|[0-9a-f:]*:[0-9a-f]*)/i
102
- const forwardedRegexps = [forwardedForRegexp, forwardedByRegexp]
103
-
104
- function findFirstIpForwardedFormat (str) {
105
- const result = {}
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
- // it's private, only save the first one found
126
- if (!result.private) result.private = ip
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
- return result
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
- const profile = this._pprof.time.stop(restart, this._generateLabels)
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['trace endpoint'] = endpointNameFromTags(webTags)
321
+ labels[TRACE_ENDPOINT_LABEL] = endpointNameFromTags(webTags)
316
322
  } else if (endpoint) {
317
323
  // fallback to endpoint computed when sample was taken
318
- labels['trace endpoint'] = endpoint
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 })