dd-trace 5.44.0 → 5.46.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 (62) hide show
  1. package/LICENSE-3rdparty.csv +1 -1
  2. package/ci/init.js +8 -0
  3. package/ext/exporters.d.ts +2 -1
  4. package/ext/exporters.js +2 -1
  5. package/package.json +3 -3
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  7. package/packages/datadog-instrumentations/src/helpers/register.js +41 -1
  8. package/packages/datadog-instrumentations/src/mariadb.js +19 -0
  9. package/packages/datadog-instrumentations/src/playwright.js +321 -46
  10. package/packages/datadog-instrumentations/src/router.js +1 -7
  11. package/packages/datadog-plugin-mongodb-core/src/index.js +20 -0
  12. package/packages/datadog-plugin-playwright/src/index.js +115 -8
  13. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +39 -15
  14. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -1
  15. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +1 -1
  16. package/packages/dd-trace/src/appsec/rasp/command_injection.js +1 -1
  17. package/packages/dd-trace/src/appsec/rasp/index.js +4 -2
  18. package/packages/dd-trace/src/appsec/rasp/lfi.js +1 -1
  19. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +1 -1
  20. package/packages/dd-trace/src/appsec/rasp/ssrf.js +1 -1
  21. package/packages/dd-trace/src/appsec/rasp/utils.js +12 -7
  22. package/packages/dd-trace/src/appsec/recommended.json +256 -84
  23. package/packages/dd-trace/src/appsec/reporter.js +6 -4
  24. package/packages/dd-trace/src/appsec/sdk/track_event.js +7 -0
  25. package/packages/dd-trace/src/appsec/telemetry/index.js +35 -4
  26. package/packages/dd-trace/src/appsec/telemetry/rasp.js +70 -6
  27. package/packages/dd-trace/src/appsec/telemetry/user.js +9 -1
  28. package/packages/dd-trace/src/appsec/telemetry/waf.js +0 -30
  29. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -0
  30. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +8 -3
  31. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +6 -4
  32. package/packages/dd-trace/src/constants.js +1 -0
  33. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +102 -22
  34. package/packages/dd-trace/src/debugger/devtools_client/condition.js +263 -0
  35. package/packages/dd-trace/src/debugger/devtools_client/index.js +69 -36
  36. package/packages/dd-trace/src/debugger/devtools_client/lock.js +8 -0
  37. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +1 -7
  38. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -2
  39. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +15 -10
  40. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +3 -3
  41. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +69 -62
  42. package/packages/dd-trace/src/debugger/devtools_client/state.js +3 -2
  43. package/packages/dd-trace/src/debugger/index.js +3 -0
  44. package/packages/dd-trace/src/dogstatsd.js +94 -77
  45. package/packages/dd-trace/src/encode/0.4.js +24 -17
  46. package/packages/dd-trace/src/exporter.js +1 -0
  47. package/packages/dd-trace/src/format.js +58 -60
  48. package/packages/dd-trace/src/histogram.js +12 -23
  49. package/packages/dd-trace/src/llmobs/index.js +3 -0
  50. package/packages/dd-trace/src/llmobs/telemetry.js +27 -1
  51. package/packages/dd-trace/src/llmobs/writers/base.js +4 -0
  52. package/packages/dd-trace/src/llmobs/writers/spans/base.js +3 -3
  53. package/packages/dd-trace/src/opentelemetry/span.js +4 -4
  54. package/packages/dd-trace/src/plugin_manager.js +2 -0
  55. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  56. package/packages/dd-trace/src/profiler.js +1 -1
  57. package/packages/dd-trace/src/profiling/config.js +6 -0
  58. package/packages/dd-trace/src/profiling/profiler.js +4 -3
  59. package/packages/dd-trace/src/profiling/profilers/wall.js +10 -8
  60. package/packages/dd-trace/src/remote_config/manager.js +5 -0
  61. package/packages/dd-trace/src/tagger.js +38 -26
  62. package/packages/dd-trace/src/util.js +1 -7
@@ -194,7 +194,6 @@ class DogStatsDClient {
194
194
  }
195
195
  }
196
196
 
197
- // TODO: Handle arrays of tags and tags translation.
198
197
  class MetricsAggregationClient {
199
198
  constructor (client) {
200
199
  this._client = client
@@ -211,98 +210,132 @@ class MetricsAggregationClient {
211
210
  }
212
211
 
213
212
  reset () {
214
- this._counters = {}
215
- this._gauges = {}
216
- this._histograms = {}
213
+ this._counters = new Map()
214
+ this._gauges = new Map()
215
+ this._histograms = new Map()
217
216
  }
218
217
 
219
- distribution (name, value, tag) {
220
- this._client.distribution(name, value, tag && [tag])
218
+ // TODO: Aggerate with a histogram and send the buckets to the client.
219
+ distribution (name, value, tags) {
220
+ this._client.distribution(name, value, tags)
221
221
  }
222
222
 
223
- boolean (name, value, tag) {
224
- this.gauge(name, value ? 1 : 0, tag)
223
+ boolean (name, value, tags) {
224
+ this.gauge(name, value ? 1 : 0, tags)
225
225
  }
226
226
 
227
- histogram (name, value, tag) {
228
- this._histograms[name] = this._histograms[name] || new Map()
227
+ histogram (name, value, tags) {
228
+ const node = this._ensureTree(this._histograms, name, tags, null)
229
229
 
230
- if (!this._histograms[name].has(tag)) {
231
- this._histograms[name].set(tag, new Histogram())
230
+ if (!node.value) {
231
+ node.value = new Histogram()
232
232
  }
233
233
 
234
- this._histograms[name].get(tag).record(value)
234
+ node.value.record(value)
235
235
  }
236
236
 
237
- count (name, count, tag, monotonic = true) {
238
- if (typeof tag === 'boolean') {
239
- monotonic = tag
240
- tag = undefined
237
+ count (name, count, tags = [], monotonic = true) {
238
+ if (typeof tags === 'boolean') {
239
+ monotonic = tags
240
+ tags = []
241
241
  }
242
242
 
243
- const map = monotonic ? this._counters : this._gauges
243
+ const container = monotonic ? this._counters : this._gauges
244
+ const node = this._ensureTree(container, name, tags, 0)
244
245
 
245
- map[name] = map[name] || new Map()
246
-
247
- const value = map[name].get(tag) || 0
248
-
249
- map[name].set(tag, value + count)
246
+ node.value = node.value + count
250
247
  }
251
248
 
252
- gauge (name, value, tag) {
253
- this._gauges[name] = this._gauges[name] || new Map()
254
- this._gauges[name].set(tag, value)
249
+ gauge (name, value, tags) {
250
+ const node = this._ensureTree(this._gauges, name, tags, 0)
251
+
252
+ node.value = value
255
253
  }
256
254
 
257
- increment (name, count = 1, tag) {
258
- this.count(name, count, tag)
255
+ increment (name, count = 1, tags) {
256
+ this.count(name, count, tags)
259
257
  }
260
258
 
261
- decrement (name, count = 1, tag) {
262
- this.count(name, -count, tag)
259
+ decrement (name, count = 1, tags) {
260
+ this.count(name, -count, tags)
263
261
  }
264
262
 
265
263
  _captureGauges () {
266
- Object.keys(this._gauges).forEach(name => {
267
- this._gauges[name].forEach((value, tag) => {
268
- this._client.gauge(name, value, tag && [tag])
269
- })
264
+ this._captureTree(this._gauges, (node, name, tags) => {
265
+ this._client.gauge(name, node.value, tags)
270
266
  })
271
267
  }
272
268
 
273
269
  _captureCounters () {
274
- Object.keys(this._counters).forEach(name => {
275
- this._counters[name].forEach((value, tag) => {
276
- this._client.increment(name, value, tag && [tag])
277
- })
270
+ this._captureTree(this._counters, (node, name, tags) => {
271
+ this._client.increment(name, node.value, tags)
278
272
  })
279
273
 
280
- this._counters = {}
274
+ this._counters.clear()
281
275
  }
282
276
 
283
277
  _captureHistograms () {
284
- Object.keys(this._histograms).forEach(name => {
285
- this._histograms[name].forEach((stats, tag) => {
286
- const tags = tag && [tag]
278
+ this._captureTree(this._histograms, (node, name, tags) => {
279
+ let stats = node.value
287
280
 
288
- // Stats can contain garbage data when a value was never recorded.
289
- if (stats.count === 0) {
290
- stats = { max: 0, min: 0, sum: 0, avg: 0, median: 0, p95: 0, count: 0, reset: stats.reset }
291
- }
281
+ // Stats can contain garbage data when a value was never recorded.
282
+ if (stats.count === 0) {
283
+ stats = { max: 0, min: 0, sum: 0, avg: 0, median: 0, p95: 0, count: 0 }
284
+ }
292
285
 
293
- this._client.gauge(`${name}.min`, stats.min, tags)
294
- this._client.gauge(`${name}.max`, stats.max, tags)
295
- this._client.increment(`${name}.sum`, stats.sum, tags)
296
- this._client.increment(`${name}.total`, stats.sum, tags)
297
- this._client.gauge(`${name}.avg`, stats.avg, tags)
298
- this._client.increment(`${name}.count`, stats.count, tags)
299
- this._client.gauge(`${name}.median`, stats.median, tags)
300
- this._client.gauge(`${name}.95percentile`, stats.p95, tags)
286
+ this._client.gauge(`${name}.min`, stats.min, tags)
287
+ this._client.gauge(`${name}.max`, stats.max, tags)
288
+ this._client.increment(`${name}.sum`, stats.sum, tags)
289
+ this._client.increment(`${name}.total`, stats.sum, tags)
290
+ this._client.gauge(`${name}.avg`, stats.avg, tags)
291
+ this._client.increment(`${name}.count`, stats.count, tags)
292
+ this._client.gauge(`${name}.median`, stats.median, tags)
293
+ this._client.gauge(`${name}.95percentile`, stats.p95, tags)
301
294
 
302
- stats.reset()
303
- })
295
+ node.value.reset()
304
296
  })
305
297
  }
298
+
299
+ _captureTree (tree, fn) {
300
+ for (const [name, root] of tree) {
301
+ this._captureNode(root, name, [], fn)
302
+ }
303
+ }
304
+
305
+ _captureNode (node, name, tags, fn) {
306
+ if (node.touched) {
307
+ fn(node, name, tags)
308
+ }
309
+
310
+ for (const [tag, next] of node.nodes) {
311
+ this._captureNode(next, name, tags.concat(tag), fn)
312
+ }
313
+ }
314
+
315
+ _ensureTree (tree, name, tags, value) {
316
+ tags = tags ? [].concat(tags) : []
317
+
318
+ let node = this._ensureNode(tree, name, value)
319
+
320
+ for (const tag of tags) {
321
+ node = this._ensureNode(node.nodes, tag, value)
322
+ }
323
+
324
+ node.touched = true
325
+
326
+ return node
327
+ }
328
+
329
+ _ensureNode (container, key, value) {
330
+ let node = container.get(key)
331
+
332
+ if (!node) {
333
+ node = { nodes: new Map(), touched: false, value }
334
+ container.set(key, node)
335
+ }
336
+
337
+ return node
338
+ }
306
339
  }
307
340
 
308
341
  /**
@@ -324,45 +357,29 @@ class CustomMetrics {
324
357
  }
325
358
 
326
359
  increment (stat, value = 1, tags) {
327
- for (const tag of this._normalizeTags(tags)) {
328
- this._client.increment(stat, value, tag)
329
- }
360
+ this._client.increment(stat, value, CustomMetrics.tagTranslator(tags))
330
361
  }
331
362
 
332
363
  decrement (stat, value = 1, tags) {
333
- for (const tag of this._normalizeTags(tags)) {
334
- this._client.decrement(stat, value, tag)
335
- }
364
+ this._client.decrement(stat, value, CustomMetrics.tagTranslator(tags))
336
365
  }
337
366
 
338
367
  gauge (stat, value, tags) {
339
- for (const tag of this._normalizeTags(tags)) {
340
- this._client.gauge(stat, value, tag)
341
- }
368
+ this._client.gauge(stat, value, CustomMetrics.tagTranslator(tags))
342
369
  }
343
370
 
344
371
  distribution (stat, value, tags) {
345
- for (const tag of this._normalizeTags(tags)) {
346
- this._client.distribution(stat, value, tag)
347
- }
372
+ this._client.distribution(stat, value, CustomMetrics.tagTranslator(tags))
348
373
  }
349
374
 
350
375
  histogram (stat, value, tags) {
351
- for (const tag of this._normalizeTags(tags)) {
352
- this._client.histogram(stat, value, tag)
353
- }
376
+ this._client.histogram(stat, value, CustomMetrics.tagTranslator(tags))
354
377
  }
355
378
 
356
379
  flush () {
357
380
  return this._client.flush()
358
381
  }
359
382
 
360
- _normalizeTags (tags) {
361
- tags = CustomMetrics.tagTranslator(tags)
362
-
363
- return tags.length === 0 ? [undefined] : tags
364
- }
365
-
366
383
  /**
367
384
  * Exposing { tagName: 'tagValue' } to the end user
368
385
  * These are translated into [ 'tagName:tagValue' ] for internal use
@@ -357,7 +357,7 @@ function formatSpanEvents (span) {
357
357
  delete spanEvent.attributes[key] // delete from attributes if undefined
358
358
  }
359
359
  }
360
- if (Object.entries(spanEvent.attributes).length === 0) {
360
+ if (Object.keys(spanEvent.attributes).length === 0) {
361
361
  delete spanEvent.attributes
362
362
  }
363
363
  }
@@ -370,48 +370,55 @@ function convertSpanEventAttributeValues (key, value, depth = 0) {
370
370
  type: 0,
371
371
  string_value: value
372
372
  }
373
- } else if (typeof value === 'boolean') {
373
+ }
374
+
375
+ if (typeof value === 'boolean') {
374
376
  return {
375
377
  type: 1,
376
378
  bool_value: value
377
379
  }
378
- } else if (Number.isInteger(value)) {
379
- return {
380
- type: 2,
381
- int_value: value
380
+ }
381
+
382
+ if (typeof value === 'number') {
383
+ if (Number.isInteger(value)) {
384
+ return {
385
+ type: 2,
386
+ int_value: value
387
+ }
382
388
  }
383
- } else if (typeof value === 'number') {
384
389
  return {
385
390
  type: 3,
386
391
  double_value: value
387
392
  }
388
- } else if (Array.isArray(value)) {
393
+ }
394
+
395
+ if (Array.isArray(value)) {
389
396
  if (depth === 0) {
390
- const convertedArray = value
391
- .map((val) => convertSpanEventAttributeValues(key, val, 1))
392
- .filter((convertedVal) => convertedVal !== undefined)
397
+ const convertedArray = []
398
+ for (const val of value) {
399
+ const convertedVal = convertSpanEventAttributeValues(key, val, 1)
400
+ if (convertedVal !== undefined) {
401
+ convertedArray.push(convertedVal)
402
+ }
403
+ }
393
404
 
394
405
  // Only include array_value if there are valid elements
395
406
  if (convertedArray.length > 0) {
396
407
  return {
397
408
  type: 4,
398
- array_value: convertedArray
409
+ array_value: { values: convertedArray }
399
410
  }
400
- } else {
401
- // If all elements were unsupported, return undefined
402
- return undefined
403
411
  }
412
+ // If all elements were unsupported, return undefined
404
413
  } else {
405
414
  memoizedLogDebug(key, 'Encountered nested array data type for span event v0.4 encoding. ' +
406
415
  `Skipping encoding key: ${key}: with value: ${typeof value}.`
407
416
  )
408
- return undefined
409
417
  }
410
418
  } else {
411
419
  memoizedLogDebug(key, 'Encountered unsupported data type for span event v0.4 encoding, key: ' +
412
420
  `${key}: with value: ${typeof value}. Skipping encoding of pair.`
413
421
  )
414
- return undefined
415
422
  }
416
423
  }
417
424
 
@@ -20,6 +20,7 @@ module.exports = name => {
20
20
  case exporters.JEST_WORKER:
21
21
  case exporters.CUCUMBER_WORKER:
22
22
  case exporters.MOCHA_WORKER:
23
+ case exporters.PLAYWRIGHT_WORKER:
23
24
  return require('./ci-visibility/exporters/test-worker')
24
25
  default:
25
26
  return inAWSLambda && !usingLambdaExtension ? require('./exporters/log') : require('./exporters/agent')
@@ -22,7 +22,9 @@ const PROCESS_ID = constants.PROCESS_ID
22
22
  const ERROR_MESSAGE = constants.ERROR_MESSAGE
23
23
  const ERROR_STACK = constants.ERROR_STACK
24
24
  const ERROR_TYPE = constants.ERROR_TYPE
25
+ const { IGNORE_OTEL_ERROR } = constants
25
26
 
27
+ // TODO(BridgeAR)[31.03.2025]: Should these land in the constants file?
26
28
  const map = {
27
29
  'operation.name': 'name',
28
30
  'service.name': 'service',
@@ -69,48 +71,46 @@ function setSingleSpanIngestionTags (span, options) {
69
71
  }
70
72
 
71
73
  function extractSpanLinks (formattedSpan, span) {
72
- const links = []
73
- if (span._links) {
74
- for (const link of span._links) {
75
- const { context, attributes } = link
76
- const formattedLink = {}
77
-
78
- formattedLink.trace_id = context.toTraceId(true)
79
- formattedLink.span_id = context.toSpanId(true)
80
-
81
- if (attributes && Object.keys(attributes).length > 0) {
82
- formattedLink.attributes = attributes
83
- }
84
- if (context?._sampling?.priority >= 0) formattedLink.flags = context._sampling.priority > 0 ? 1 : 0
85
- if (context?._tracestate) formattedLink.tracestate = context._tracestate.toString()
74
+ if (!span._links?.length) {
75
+ return
76
+ }
77
+ const links = span._links.map(link => {
78
+ const { context, attributes } = link
79
+ const formattedLink = {
80
+ trace_id: context.toTraceId(true),
81
+ span_id: context.toSpanId(true)
82
+ }
86
83
 
87
- links.push(formattedLink)
84
+ if (attributes && Object.keys(attributes).length > 0) {
85
+ formattedLink.attributes = attributes
88
86
  }
89
- }
90
- if (links.length > 0) { formattedSpan.meta['_dd.span_links'] = JSON.stringify(links) }
87
+ if (context?._sampling?.priority >= 0) formattedLink.flags = context._sampling.priority > 0 ? 1 : 0
88
+ if (context?._tracestate) formattedLink.tracestate = context._tracestate.toString()
89
+
90
+ return formattedLink
91
+ })
92
+ formattedSpan.meta['_dd.span_links'] = JSON.stringify(links)
91
93
  }
92
94
 
93
95
  function extractSpanEvents (formattedSpan, span) {
94
- const events = []
95
- if (span._events) {
96
- for (const event of span._events) {
97
- const formattedEvent = {
98
- name: event.name,
99
- time_unix_nano: Math.round(event.startTime * 1e6),
100
- attributes: event.attributes && Object.keys(event.attributes).length > 0 ? event.attributes : undefined
101
- }
102
-
103
- events.push(formattedEvent)
104
- }
105
- }
106
- if (events.length > 0) {
107
- formattedSpan.span_events = events
96
+ if (!span._events?.length) {
97
+ return
108
98
  }
99
+ const events = span._events.map(event => {
100
+ return {
101
+ name: event.name,
102
+ time_unix_nano: Math.round(event.startTime * 1e6),
103
+ attributes: event.attributes && Object.keys(event.attributes).length > 0 ? event.attributes : undefined
104
+ }
105
+ })
106
+ formattedSpan.span_events = events
109
107
  }
110
108
 
111
109
  function extractTags (formattedSpan, span) {
112
110
  const context = span.context()
113
111
  const origin = context._trace.origin
112
+ // TODO(BridgeAR)[31.03.2025]: Look into changing the way we store tags. Using
113
+ // a map is likely faster short term.
114
114
  const tags = context._tags
115
115
  const hostname = context._hostname
116
116
  const priority = context._sampling.priority
@@ -126,43 +126,48 @@ function extractTags (formattedSpan, span) {
126
126
  registerExtraService(tags['service.name'])
127
127
  }
128
128
 
129
- for (const tag in tags) {
129
+ for (const [tag, value] of Object.entries(tags)) {
130
+ // TODO(BridgeAR)[31.03.2025]: Check how many tags are defined in average.
131
+ // In case there are more than 2 tags in average, check for all special
132
+ // cases up front and loop over the tags afterwards, skipping the already
133
+ // visited property names by checking a map with these keys.
130
134
  switch (tag) {
131
135
  case 'service.name':
132
136
  case 'span.type':
133
137
  case 'resource.name':
134
- addTag(formattedSpan, {}, map[tag], tags[tag])
138
+ addTag(formattedSpan, {}, map[tag], value)
135
139
  break
136
140
  // HACK: remove when Datadog supports numeric status code
137
141
  case 'http.status_code':
138
- addTag(formattedSpan.meta, {}, tag, tags[tag] && String(tags[tag]))
142
+ addTag(formattedSpan.meta, {}, tag, value && String(value))
139
143
  break
140
144
  case 'analytics.event':
141
- addTag({}, formattedSpan.metrics, ANALYTICS, tags[tag] === undefined || tags[tag] ? 1 : 0)
145
+ addTag({}, formattedSpan.metrics, ANALYTICS, value === undefined || value ? 1 : 0)
142
146
  break
143
147
  case HOSTNAME_KEY:
144
148
  case MEASURED:
145
- addTag({}, formattedSpan.metrics, tag, tags[tag] === undefined || tags[tag] ? 1 : 0)
149
+ addTag({}, formattedSpan.metrics, tag, value === undefined || value ? 1 : 0)
146
150
  break
151
+ // TODO(BridgeAR)[31.03.2025]: How come we use two different ways to pass
152
+ // through errors? Can we just unify the behavior to always use one way?
147
153
  case 'error':
148
154
  if (context._name !== 'fs.operation') {
149
- extractError(formattedSpan, tags[tag])
155
+ extractError(formattedSpan, value)
150
156
  }
151
157
  break
152
158
  case ERROR_TYPE:
153
159
  case ERROR_MESSAGE:
154
160
  case ERROR_STACK:
155
161
  // HACK: remove when implemented in the backend
156
- if (context._name !== 'fs.operation') {
157
- // HACK: to ensure otel.recordException does not influence formattedSpan.error
158
- if (tags.setTraceError) {
159
- formattedSpan.error = 1
160
- }
161
- } else {
162
+ if (context._name === 'fs.operation') {
162
163
  break
163
164
  }
165
+ // otel.recordException should not influence trace.error
166
+ if (!tags[IGNORE_OTEL_ERROR]) {
167
+ formattedSpan.error = 1
168
+ }
164
169
  default: // eslint-disable-line no-fallthrough
165
- addTag(formattedSpan.meta, formattedSpan.metrics, tag, tags[tag])
170
+ addTag(formattedSpan.meta, formattedSpan.metrics, tag, value)
166
171
  }
167
172
  }
168
173
  setSingleSpanIngestionTags(formattedSpan, context._spanSampling)
@@ -193,8 +198,8 @@ function extractChunkTags (formattedSpan, span) {
193
198
 
194
199
  if (!isLocalRoot) return
195
200
 
196
- for (const key in context._trace.tags) {
197
- addTag(formattedSpan.meta, formattedSpan.metrics, key, context._trace.tags[key])
201
+ for (const [key, value] of Object.entries(context._trace.tags)) {
202
+ addTag(formattedSpan.meta, formattedSpan.metrics, key, value)
198
203
  }
199
204
  }
200
205
 
@@ -205,6 +210,8 @@ function extractError (formattedSpan, error) {
205
210
 
206
211
  if (isError(error)) {
207
212
  // AggregateError only has a code and no message.
213
+ // TODO(BridgeAR)[31.03.2025]: An AggregateError can have a message. Should
214
+ // the code just generally be added, if available?
208
215
  addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_MESSAGE, error.message || error.code)
209
216
  addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_TYPE, error.name)
210
217
  addTag(formattedSpan.meta, formattedSpan.metrics, ERROR_STACK, error.stack)
@@ -223,30 +230,21 @@ function addTag (meta, metrics, key, value, nested) {
223
230
  case 'boolean':
224
231
  metrics[key] = value ? 1 : 0
225
232
  break
226
- case 'undefined':
227
- break
228
- case 'object':
229
- if (value === null) break
233
+ default:
234
+ if (value == null) break
230
235
 
231
236
  // Special case for Node.js Buffer and URL
237
+ // TODO(BridgeAR)[31.03.2025]: Figure out if all typed arrays should be treated as buffers.
232
238
  if (isNodeBuffer(value) || isUrl(value)) {
233
239
  metrics[key] = value.toString()
234
240
  } else if (!Array.isArray(value) && !nested) {
235
- for (const prop in value) {
236
- if (!hasOwn(value, prop)) continue
237
-
238
- addTag(meta, metrics, `${key}.${prop}`, value[prop], true)
241
+ for (const [prop, val] of Object.entries(value)) {
242
+ addTag(meta, metrics, `${key}.${prop}`, val, true)
239
243
  }
240
244
  }
241
-
242
- break
243
245
  }
244
246
  }
245
247
 
246
- function hasOwn (object, prop) {
247
- return Object.prototype.hasOwnProperty.call(object, prop)
248
- }
249
-
250
248
  function isNodeBuffer (obj) {
251
249
  return obj.constructor && obj.constructor.name === 'Buffer' &&
252
250
  typeof obj.readInt8 === 'function' &&
@@ -7,39 +7,28 @@ class Histogram {
7
7
  this.reset()
8
8
  }
9
9
 
10
- get min () { return this._min }
11
- get max () { return this._max }
12
- get avg () { return this._count === 0 ? 0 : this._sum / this._count }
13
- get sum () { return this._sum }
14
- get count () { return this._count }
10
+ get min () { return this._sketch.count === 0 ? 0 : this._sketch.min }
11
+ get max () { return this._sketch.count === 0 ? 0 : this._sketch.max }
12
+ get avg () { return this._sketch.count === 0 ? 0 : this._sketch.sum / this._sketch.count }
13
+ get sum () { return this._sketch.sum }
14
+ get count () { return this._sketch.count }
15
15
  get median () { return this.percentile(50) }
16
16
  get p95 () { return this.percentile(95) }
17
17
 
18
18
  percentile (percentile) {
19
- return this._histogram.getValueAtQuantile(percentile / 100) || 0
19
+ return this._sketch.getValueAtQuantile(percentile / 100) || 0
20
20
  }
21
21
 
22
- record (value) {
23
- if (this._count === 0) {
24
- this._min = this._max = value
25
- } else {
26
- this._min = Math.min(this._min, value)
27
- this._max = Math.max(this._max, value)
28
- }
29
-
30
- this._count++
31
- this._sum += value
22
+ merge (histogram) {
23
+ return this._sketch.merge(histogram._sketch)
24
+ }
32
25
 
33
- this._histogram.accept(value)
26
+ record (value) {
27
+ this._sketch.accept(value)
34
28
  }
35
29
 
36
30
  reset () {
37
- this._min = 0
38
- this._max = 0
39
- this._sum = 0
40
- this._count = 0
41
-
42
- this._histogram = new DDSketch()
31
+ this._sketch = new DDSketch()
43
32
  }
44
33
  }
45
34
 
@@ -6,6 +6,7 @@ const { storage } = require('./storage')
6
6
 
7
7
  const LLMObsSpanProcessor = require('./span_processor')
8
8
 
9
+ const telemetry = require('./telemetry')
9
10
  const { channel } = require('dc-polyfill')
10
11
  const spanProcessCh = channel('dd-trace:span:process')
11
12
  const evalMetricAppendCh = channel('llmobs:eval-metric:append')
@@ -29,6 +30,7 @@ let spanWriter
29
30
  let evalWriter
30
31
 
31
32
  function enable (config) {
33
+ const startTime = performance.now()
32
34
  // create writers and eval writer append and flush channels
33
35
  // span writer append is handled by the span processor
34
36
  evalWriter = new LLMObsEvalMetricsWriter(config)
@@ -44,6 +46,7 @@ function enable (config) {
44
46
 
45
47
  // distributed tracing for llmobs
46
48
  injectCh.subscribe(handleLLMObsParentIdInjection)
49
+ telemetry.recordLLMObsEnabled(startTime, config)
47
50
  }
48
51
 
49
52
  function disable () {
@@ -74,6 +74,23 @@ function incrementLLMObsSpanFinishedCount (span, value = 1) {
74
74
  llmobsMetrics.count('span.finished', tags).inc(value)
75
75
  }
76
76
 
77
+ function recordLLMObsEnabled (startTime, config, value = 1) {
78
+ const initTimeMs = performance.now() - startTime
79
+ // There isn't an easy way to determine if a user automatically enabled LLMObs via
80
+ // in-code or command line setup. We'll use the presence of DD_LLMOBS_ENABLED env var
81
+ // as a rough heuristic, but note that this isn't perfect since
82
+ // a user may have env vars but enable manually in code.
83
+ const autoEnabled = !!config._env?.['llmobs.enabled']
84
+ const tags = {
85
+ error: 0,
86
+ agentless: Number(config.llmobs.agentlessEnabled),
87
+ site: config.site,
88
+ auto: Number(autoEnabled)
89
+ }
90
+ llmobsMetrics.count('product_enabled', tags).inc(value)
91
+ llmobsMetrics.distribution('init_time', tags).track(initTimeMs)
92
+ }
93
+
77
94
  function recordLLMObsRawSpanSize (event, rawEventSize) {
78
95
  const tags = extractTagsFromSpanEvent(event)
79
96
  llmobsMetrics.distribution('span.raw_size', tags).track(rawEventSize)
@@ -85,9 +102,18 @@ function recordLLMObsSpanSize (event, eventSize, shouldTruncate) {
85
102
  llmobsMetrics.distribution('span.size', tags).track(eventSize)
86
103
  }
87
104
 
105
+ function recordDroppedPayload (numEvents, eventType, error) {
106
+ if (eventType !== 'span' && eventType !== 'evaluation_metric') return
107
+ const metricName = eventType === 'span' ? 'dropped_span_event' : 'dropped_eval_event'
108
+ const tags = { error }
109
+ llmobsMetrics.count(metricName, tags).inc(numEvents)
110
+ }
111
+
88
112
  module.exports = {
113
+ recordLLMObsEnabled,
89
114
  incrementLLMObsSpanStartCount,
90
115
  incrementLLMObsSpanFinishedCount,
91
116
  recordLLMObsRawSpanSize,
92
- recordLLMObsSpanSize
117
+ recordLLMObsSpanSize,
118
+ recordDroppedPayload
93
119
  }