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.
- package/LICENSE-3rdparty.csv +1 -1
- package/ci/init.js +8 -0
- package/ext/exporters.d.ts +2 -1
- package/ext/exporters.js +2 -1
- package/package.json +3 -3
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +41 -1
- package/packages/datadog-instrumentations/src/mariadb.js +19 -0
- package/packages/datadog-instrumentations/src/playwright.js +321 -46
- package/packages/datadog-instrumentations/src/router.js +1 -7
- package/packages/datadog-plugin-mongodb-core/src/index.js +20 -0
- package/packages/datadog-plugin-playwright/src/index.js +115 -8
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +39 -15
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/command_injection.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/index.js +4 -2
- package/packages/dd-trace/src/appsec/rasp/lfi.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/sql_injection.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/utils.js +12 -7
- package/packages/dd-trace/src/appsec/recommended.json +256 -84
- package/packages/dd-trace/src/appsec/reporter.js +6 -4
- package/packages/dd-trace/src/appsec/sdk/track_event.js +7 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +35 -4
- package/packages/dd-trace/src/appsec/telemetry/rasp.js +70 -6
- package/packages/dd-trace/src/appsec/telemetry/user.js +9 -1
- package/packages/dd-trace/src/appsec/telemetry/waf.js +0 -30
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +8 -3
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +6 -4
- package/packages/dd-trace/src/constants.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +102 -22
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +263 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +69 -36
- package/packages/dd-trace/src/debugger/devtools_client/lock.js +8 -0
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +1 -7
- package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -2
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +15 -10
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +3 -3
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +69 -62
- package/packages/dd-trace/src/debugger/devtools_client/state.js +3 -2
- package/packages/dd-trace/src/debugger/index.js +3 -0
- package/packages/dd-trace/src/dogstatsd.js +94 -77
- package/packages/dd-trace/src/encode/0.4.js +24 -17
- package/packages/dd-trace/src/exporter.js +1 -0
- package/packages/dd-trace/src/format.js +58 -60
- package/packages/dd-trace/src/histogram.js +12 -23
- package/packages/dd-trace/src/llmobs/index.js +3 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +27 -1
- package/packages/dd-trace/src/llmobs/writers/base.js +4 -0
- package/packages/dd-trace/src/llmobs/writers/spans/base.js +3 -3
- package/packages/dd-trace/src/opentelemetry/span.js +4 -4
- package/packages/dd-trace/src/plugin_manager.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/config.js +6 -0
- package/packages/dd-trace/src/profiling/profiler.js +4 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +10 -8
- package/packages/dd-trace/src/remote_config/manager.js +5 -0
- package/packages/dd-trace/src/tagger.js +38 -26
- 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
|
-
|
|
220
|
-
|
|
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,
|
|
224
|
-
this.gauge(name, value ? 1 : 0,
|
|
223
|
+
boolean (name, value, tags) {
|
|
224
|
+
this.gauge(name, value ? 1 : 0, tags)
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
histogram (name, value,
|
|
228
|
-
|
|
227
|
+
histogram (name, value, tags) {
|
|
228
|
+
const node = this._ensureTree(this._histograms, name, tags, null)
|
|
229
229
|
|
|
230
|
-
if (!
|
|
231
|
-
|
|
230
|
+
if (!node.value) {
|
|
231
|
+
node.value = new Histogram()
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
node.value.record(value)
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
count (name, count,
|
|
238
|
-
if (typeof
|
|
239
|
-
monotonic =
|
|
240
|
-
|
|
237
|
+
count (name, count, tags = [], monotonic = true) {
|
|
238
|
+
if (typeof tags === 'boolean') {
|
|
239
|
+
monotonic = tags
|
|
240
|
+
tags = []
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
const
|
|
243
|
+
const container = monotonic ? this._counters : this._gauges
|
|
244
|
+
const node = this._ensureTree(container, name, tags, 0)
|
|
244
245
|
|
|
245
|
-
|
|
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,
|
|
253
|
-
|
|
254
|
-
|
|
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,
|
|
258
|
-
this.count(name, count,
|
|
255
|
+
increment (name, count = 1, tags) {
|
|
256
|
+
this.count(name, count, tags)
|
|
259
257
|
}
|
|
260
258
|
|
|
261
|
-
decrement (name, count = 1,
|
|
262
|
-
this.count(name, -count,
|
|
259
|
+
decrement (name, count = 1, tags) {
|
|
260
|
+
this.count(name, -count, tags)
|
|
263
261
|
}
|
|
264
262
|
|
|
265
263
|
_captureGauges () {
|
|
266
|
-
|
|
267
|
-
this.
|
|
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
|
-
|
|
275
|
-
this.
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
const tags = tag && [tag]
|
|
278
|
+
this._captureTree(this._histograms, (node, name, tags) => {
|
|
279
|
+
let stats = node.value
|
|
287
280
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (typeof value === 'boolean') {
|
|
374
376
|
return {
|
|
375
377
|
type: 1,
|
|
376
378
|
bool_value: value
|
|
377
379
|
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (Array.isArray(value)) {
|
|
389
396
|
if (depth === 0) {
|
|
390
|
-
const convertedArray =
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
+
if (attributes && Object.keys(attributes).length > 0) {
|
|
85
|
+
formattedLink.attributes = attributes
|
|
88
86
|
}
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
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],
|
|
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,
|
|
142
|
+
addTag(formattedSpan.meta, {}, tag, value && String(value))
|
|
139
143
|
break
|
|
140
144
|
case 'analytics.event':
|
|
141
|
-
addTag({}, formattedSpan.metrics, ANALYTICS,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
197
|
-
addTag(formattedSpan.meta, formattedSpan.metrics, 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
|
-
|
|
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
|
|
236
|
-
|
|
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.
|
|
11
|
-
get max () { return this.
|
|
12
|
-
get avg () { return this.
|
|
13
|
-
get sum () { return this.
|
|
14
|
-
get count () { return this.
|
|
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.
|
|
19
|
+
return this._sketch.getValueAtQuantile(percentile / 100) || 0
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
26
|
+
record (value) {
|
|
27
|
+
this._sketch.accept(value)
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
reset () {
|
|
37
|
-
this.
|
|
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
|
}
|