dd-trace 5.49.1 → 5.50.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/README.md +3 -13
- package/index.d.ts +1 -0
- package/package.json +2 -3
- package/packages/datadog-core/src/storage.js +4 -3
- package/packages/datadog-shimmer/src/shimmer.js +76 -68
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -17
- package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
- package/packages/dd-trace/src/config.js +23 -31
- package/packages/dd-trace/src/datastreams/processor.js +3 -5
- package/packages/dd-trace/src/dogstatsd.js +11 -4
- package/packages/dd-trace/src/llmobs/index.js +4 -1
- package/packages/dd-trace/src/llmobs/sdk.js +146 -112
- package/packages/dd-trace/src/llmobs/tagger.js +13 -9
- package/packages/dd-trace/src/llmobs/telemetry.js +50 -1
- package/packages/dd-trace/src/payload-tagging/jsonpath-plus.js +1 -1
- package/packages/dd-trace/src/profiling/config.js +0 -6
- package/packages/dd-trace/src/profiling/profilers/wall.js +8 -12
- package/packages/dd-trace/src/span_stats.js +2 -2
package/README.md
CHANGED
|
@@ -69,21 +69,11 @@ Changes associated with each individual release are documented on the [GitHub Re
|
|
|
69
69
|
Please read the [CONTRIBUTING.md](https://github.com/DataDog/dd-trace-js/blob/master/CONTRIBUTING.md) document before contributing to this open source project.
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
##
|
|
72
|
+
## ECMAScript Modules (ESM) Support
|
|
73
73
|
|
|
74
|
-
ESM support requires an
|
|
74
|
+
ESM support requires an _additional_ command line argument when starting the Node.js process.
|
|
75
|
+
For more information, see the [section on ESM support](https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/nodejs/#esm-applications-only-import-the-loader) in the Node.js tracer documentation.
|
|
75
76
|
|
|
76
|
-
Node.js < v20.6
|
|
77
|
-
|
|
78
|
-
```sh
|
|
79
|
-
node --loader dd-trace/loader-hook.mjs entrypoint.js
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Node.js >= v20.6
|
|
83
|
-
|
|
84
|
-
```sh
|
|
85
|
-
node --import dd-trace/register.js entrypoint.js
|
|
86
|
-
```
|
|
87
77
|
|
|
88
78
|
## Serverless / Lambda
|
|
89
79
|
|
package/index.d.ts
CHANGED
|
@@ -2251,6 +2251,7 @@ declare namespace tracer {
|
|
|
2251
2251
|
/**
|
|
2252
2252
|
* Defines the pattern to ignore cookie names in the vulnerability hash calculation
|
|
2253
2253
|
* @default ".{32,}"
|
|
2254
|
+
* @deprecated This property has no effect because hash calculation algorithm has been updated for cookie vulnerabilities
|
|
2254
2255
|
*/
|
|
2255
2256
|
cookieFilterPattern?: string,
|
|
2256
2257
|
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.50.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"env": "bash ./plugin-env",
|
|
9
9
|
"preinstall": "node scripts/preinstall.js",
|
|
10
|
-
"bench": "node benchmark",
|
|
11
|
-
"bench:e2e": "SERVICES=mongo yarn services && cd benchmark/e2e && node benchmark-run.js --duration=30",
|
|
10
|
+
"bench": "node benchmark/index.js",
|
|
12
11
|
"bench:e2e:ci-visibility": "node benchmark/e2e-ci/benchmark-run.js",
|
|
13
12
|
"type:doc": "cd docs && yarn && yarn build",
|
|
14
13
|
"type:test": "cd docs && yarn && yarn test",
|
|
@@ -47,15 +47,16 @@ class DatadogStorage extends AsyncLocalStorage {
|
|
|
47
47
|
* key. This is useful if you've stashed a handle somewhere and want to
|
|
48
48
|
* retrieve the store with it.
|
|
49
49
|
*
|
|
50
|
-
* @param handle {{}}
|
|
50
|
+
* @param [handle] {{}}
|
|
51
51
|
* @returns {T | undefined}
|
|
52
52
|
*/
|
|
53
53
|
getStore (handle) {
|
|
54
54
|
if (!handle) {
|
|
55
55
|
handle = super.getStore()
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if (handle) {
|
|
58
|
+
return stores.get(handle)
|
|
59
|
+
}
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/**
|
|
@@ -1,85 +1,105 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const skipMethods = new Set([
|
|
4
|
+
'caller',
|
|
5
|
+
'arguments',
|
|
6
|
+
'name',
|
|
7
|
+
'length'
|
|
8
|
+
])
|
|
9
|
+
|
|
3
10
|
function copyProperties (original, wrapped) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const proto = Object.getPrototypeOf(original)
|
|
7
|
-
if (proto !== Function.prototype) {
|
|
11
|
+
if (original.constructor !== wrapped.constructor) {
|
|
12
|
+
const proto = Object.getPrototypeOf(original)
|
|
8
13
|
Object.setPrototypeOf(wrapped, proto)
|
|
9
14
|
}
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
|
|
16
|
+
const ownKeys = Reflect.ownKeys(original)
|
|
17
|
+
if (original.length !== wrapped.length) {
|
|
18
|
+
Object.defineProperty(wrapped, 'length', { value: original.length, configurable: true })
|
|
19
|
+
}
|
|
20
|
+
if (original.name !== wrapped.name) {
|
|
21
|
+
Object.defineProperty(wrapped, 'name', { value: original.name, configurable: true })
|
|
22
|
+
}
|
|
23
|
+
if (ownKeys.length !== 2) {
|
|
24
|
+
for (const key of ownKeys) {
|
|
25
|
+
if (skipMethods.has(key)) continue
|
|
26
|
+
const descriptor = Object.getOwnPropertyDescriptor(original, key)
|
|
27
|
+
if (descriptor.writable && descriptor.enumerable && descriptor.configurable) {
|
|
28
|
+
wrapped[key] = original[key]
|
|
29
|
+
} else if (descriptor.writable || descriptor.configurable || !Object.hasOwn(wrapped, key)) {
|
|
30
|
+
Object.defineProperty(wrapped, key, descriptor)
|
|
31
|
+
}
|
|
18
32
|
}
|
|
19
33
|
}
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
function wrapFunction (original, wrapper) {
|
|
23
|
-
if (typeof original === 'function') assertNotClass(original)
|
|
24
|
-
|
|
25
37
|
const wrapped = wrapper(original)
|
|
26
38
|
|
|
27
|
-
if (typeof original === 'function')
|
|
39
|
+
if (typeof original === 'function') {
|
|
40
|
+
assertNotClass(original)
|
|
41
|
+
copyProperties(original, wrapped)
|
|
42
|
+
}
|
|
28
43
|
|
|
29
44
|
return wrapped
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
function wrapMethod (target, name, wrapper, noAssert) {
|
|
37
|
-
if (!noAssert) {
|
|
38
|
-
assertMethod(target, name)
|
|
39
|
-
assertFunction(wrapper)
|
|
47
|
+
function wrap (target, name, wrapper) {
|
|
48
|
+
assertMethod(target, name)
|
|
49
|
+
if (typeof wrapper !== 'function') {
|
|
50
|
+
throw new Error(wrapper ? 'Target is not a function' : 'No function provided')
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
const original = target[name]
|
|
43
54
|
const wrapped = wrapper(original)
|
|
44
55
|
|
|
45
|
-
const descriptor = Object.getOwnPropertyDescriptor(target, name)
|
|
46
|
-
|
|
47
|
-
const attributes = {
|
|
48
|
-
configurable: true,
|
|
49
|
-
...descriptor
|
|
50
|
-
}
|
|
51
|
-
|
|
52
56
|
if (typeof original === 'function') copyProperties(original, wrapped)
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
let descriptor = Object.getOwnPropertyDescriptor(target, name)
|
|
59
|
+
|
|
60
|
+
// No descriptor means original was on the prototype
|
|
61
|
+
if (descriptor === undefined) {
|
|
62
|
+
descriptor = {
|
|
63
|
+
value: wrapped,
|
|
64
|
+
writable: true,
|
|
65
|
+
configurable: true,
|
|
66
|
+
enumerable: false
|
|
67
|
+
}
|
|
68
|
+
} else if (descriptor.writable) {
|
|
69
|
+
// Fast path for assigned properties.
|
|
70
|
+
if (descriptor.configurable && descriptor.enumerable) {
|
|
71
|
+
target[name] = wrapped
|
|
72
|
+
return target
|
|
73
|
+
}
|
|
74
|
+
descriptor.value = wrapped
|
|
75
|
+
} else {
|
|
55
76
|
if (descriptor.get || descriptor.set) {
|
|
56
|
-
|
|
77
|
+
// TODO(BridgeAR): What happens in case there is a setter? This seems wrong?
|
|
78
|
+
// What happens in case the user does indeed set this to a different value?
|
|
79
|
+
// In that case the getter would potentially return the wrong value?
|
|
80
|
+
descriptor.get = () => wrapped
|
|
57
81
|
} else {
|
|
58
|
-
|
|
82
|
+
descriptor.value = wrapped
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
// TODO: create a single object for multiple wrapped methods
|
|
62
85
|
if (descriptor.configurable === false) {
|
|
86
|
+
// TODO(BridgeAR): Bail out instead (throw). It is unclear if the newly
|
|
87
|
+
// created object is actually used. If it's not used, the wrapping would
|
|
88
|
+
// have had no effect without noticing. It is also unclear what would happen
|
|
89
|
+
// in case user code would check for properties to be own properties. That
|
|
90
|
+
// would fail with this code. A function being replaced with an object is
|
|
91
|
+
// also not possible.
|
|
63
92
|
return Object.create(target, {
|
|
64
|
-
[name]:
|
|
93
|
+
[name]: descriptor
|
|
65
94
|
})
|
|
66
95
|
}
|
|
67
|
-
} else { // no descriptor means original was on the prototype
|
|
68
|
-
attributes.value = wrapped
|
|
69
|
-
attributes.writable = true
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
Object.defineProperty(target, name,
|
|
98
|
+
Object.defineProperty(target, name, descriptor)
|
|
73
99
|
|
|
74
100
|
return target
|
|
75
101
|
}
|
|
76
102
|
|
|
77
|
-
function wrap (target, name, wrapper) {
|
|
78
|
-
return typeof name === 'function'
|
|
79
|
-
? wrapFn(target, name)
|
|
80
|
-
: wrapMethod(target, name, wrapper)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
103
|
function massWrap (targets, names, wrapper) {
|
|
84
104
|
targets = toArray(targets)
|
|
85
105
|
names = toArray(names)
|
|
@@ -96,30 +116,18 @@ function toArray (maybeArray) {
|
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
function assertMethod (target, name) {
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (typeof target[name] !== 'function') {
|
|
112
|
-
throw new Error(`Original method ${name} is not a function.`)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function assertFunction (target) {
|
|
117
|
-
if (!target) {
|
|
118
|
-
throw new Error('No function provided.')
|
|
119
|
-
}
|
|
119
|
+
if (typeof target?.[name] !== 'function') {
|
|
120
|
+
let message = 'No target object provided'
|
|
121
|
+
|
|
122
|
+
if (target) {
|
|
123
|
+
if (typeof target !== 'object' && typeof target !== 'function') {
|
|
124
|
+
message = 'Invalid target'
|
|
125
|
+
} else {
|
|
126
|
+
message = target[name] ? `Original method ${name} is not a function` : `No original method ${name}`
|
|
127
|
+
}
|
|
128
|
+
}
|
|
120
129
|
|
|
121
|
-
|
|
122
|
-
throw new Error('Target is not a function.')
|
|
130
|
+
throw new Error(message)
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const Analyzer = require('./vulnerability-analyzer')
|
|
4
4
|
const { getNodeModulesPaths } = require('../path-line')
|
|
5
|
-
const log = require('../../../log')
|
|
6
5
|
|
|
7
6
|
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
|
|
8
7
|
|
|
@@ -12,14 +11,7 @@ class CookieAnalyzer extends Analyzer {
|
|
|
12
11
|
this.propertyToBeSafe = propertyToBeSafe.toLowerCase()
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
onConfigure (
|
|
16
|
-
try {
|
|
17
|
-
this.cookieFilterRegExp = new RegExp(config.iast.cookieFilterPattern)
|
|
18
|
-
} catch {
|
|
19
|
-
log.error('[ASM] Invalid regex in cookieFilterPattern')
|
|
20
|
-
this.cookieFilterRegExp = /.{32,}/
|
|
21
|
-
}
|
|
22
|
-
|
|
14
|
+
onConfigure () {
|
|
23
15
|
this.addSub(
|
|
24
16
|
{ channelName: 'datadog:iast:set-cookie', moduleName: 'http' },
|
|
25
17
|
(cookieInfo) => this.analyze(cookieInfo)
|
|
@@ -35,14 +27,6 @@ class CookieAnalyzer extends Analyzer {
|
|
|
35
27
|
return { value: cookieName }
|
|
36
28
|
}
|
|
37
29
|
|
|
38
|
-
_createHashSource (type, evidence, location) {
|
|
39
|
-
if (typeof evidence.value === 'string' && evidence.value.match(this.cookieFilterRegExp)) {
|
|
40
|
-
return 'FILTERED_' + this._type
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return `${type}:${evidence.value}`
|
|
44
|
-
}
|
|
45
|
-
|
|
46
30
|
_getExcludedPaths () {
|
|
47
31
|
return EXCLUDED_PATHS
|
|
48
32
|
}
|
|
@@ -181,8 +181,7 @@ function validateNamingVersion (versionString) {
|
|
|
181
181
|
* If a blank path is provided a null is returned to signal that the feature is disabled.
|
|
182
182
|
* An empty array means the feature is enabled but that no rules need to be applied.
|
|
183
183
|
*
|
|
184
|
-
* @param {string} input
|
|
185
|
-
* @returns {[string]|null}
|
|
184
|
+
* @param {string | string[]} input
|
|
186
185
|
*/
|
|
187
186
|
function splitJSONPathRules (input) {
|
|
188
187
|
if (!input) return null
|
|
@@ -289,8 +288,7 @@ class Config {
|
|
|
289
288
|
}
|
|
290
289
|
const PROPAGATION_STYLE_INJECT = propagationStyle(
|
|
291
290
|
'inject',
|
|
292
|
-
options.tracePropagationStyle
|
|
293
|
-
this._getDefaultPropagationStyle(options)
|
|
291
|
+
options.tracePropagationStyle
|
|
294
292
|
)
|
|
295
293
|
|
|
296
294
|
validateOtelPropagators(PROPAGATION_STYLE_INJECT)
|
|
@@ -299,8 +297,6 @@ class Config {
|
|
|
299
297
|
options.appsec = {
|
|
300
298
|
enabled: options.appsec
|
|
301
299
|
}
|
|
302
|
-
} else if (options.appsec == null) {
|
|
303
|
-
options.appsec = {}
|
|
304
300
|
}
|
|
305
301
|
|
|
306
302
|
const DD_INSTRUMENTATION_INSTALL_ID = coalesce(
|
|
@@ -505,7 +501,6 @@ class Config {
|
|
|
505
501
|
this._setValue(defaults, 'grpc.server.error.statuses', GRPC_SERVER_ERROR_STATUSES)
|
|
506
502
|
this._setValue(defaults, 'headerTags', [])
|
|
507
503
|
this._setValue(defaults, 'hostname', '127.0.0.1')
|
|
508
|
-
this._setValue(defaults, 'iast.cookieFilterPattern', '.{32,}')
|
|
509
504
|
this._setValue(defaults, 'iast.dbRowsToTaint', 1)
|
|
510
505
|
this._setValue(defaults, 'iast.deduplicationEnabled', true)
|
|
511
506
|
this._setValue(defaults, 'iast.enabled', false)
|
|
@@ -680,7 +675,6 @@ class Config {
|
|
|
680
675
|
DD_GRPC_CLIENT_ERROR_STATUSES,
|
|
681
676
|
DD_GRPC_SERVER_ERROR_STATUSES,
|
|
682
677
|
JEST_WORKER_ID,
|
|
683
|
-
DD_IAST_COOKIE_FILTER_PATTERN,
|
|
684
678
|
DD_IAST_DB_ROWS_TO_TAINT,
|
|
685
679
|
DD_IAST_DEDUPLICATION_ENABLED,
|
|
686
680
|
DD_IAST_ENABLED,
|
|
@@ -855,7 +849,6 @@ class Config {
|
|
|
855
849
|
this._setIntegerRangeSet(env, 'grpc.server.error.statuses', DD_GRPC_SERVER_ERROR_STATUSES)
|
|
856
850
|
this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS)
|
|
857
851
|
this._setString(env, 'hostname', coalesce(DD_AGENT_HOST, DD_TRACE_AGENT_HOSTNAME))
|
|
858
|
-
this._setString(env, 'iast.cookieFilterPattern', DD_IAST_COOKIE_FILTER_PATTERN)
|
|
859
852
|
this._setValue(env, 'iast.dbRowsToTaint', maybeInt(DD_IAST_DB_ROWS_TO_TAINT))
|
|
860
853
|
this._setBoolean(env, 'iast.deduplicationEnabled', DD_IAST_DEDUPLICATION_ENABLED)
|
|
861
854
|
this._setBoolean(env, 'iast.enabled', DD_IAST_ENABLED)
|
|
@@ -1010,27 +1003,27 @@ class Config {
|
|
|
1010
1003
|
options.apmTracingEnabled,
|
|
1011
1004
|
options.experimental?.appsec?.standalone && !options.experimental.appsec.standalone.enabled
|
|
1012
1005
|
))
|
|
1013
|
-
this._setBoolean(opts, 'appsec.apiSecurity.enabled', options.appsec
|
|
1014
|
-
this._setValue(opts, 'appsec.blockedTemplateGraphql', maybeFile(options.appsec
|
|
1015
|
-
this._setValue(opts, 'appsec.blockedTemplateHtml', maybeFile(options.appsec
|
|
1016
|
-
this._optsUnprocessed['appsec.blockedTemplateHtml'] = options.appsec
|
|
1017
|
-
this._setValue(opts, 'appsec.blockedTemplateJson', maybeFile(options.appsec
|
|
1018
|
-
this._optsUnprocessed['appsec.blockedTemplateJson'] = options.appsec
|
|
1019
|
-
this._setBoolean(opts, 'appsec.enabled', options.appsec
|
|
1020
|
-
this._setString(opts, 'appsec.eventTracking.mode', options.appsec
|
|
1021
|
-
this._setString(opts, 'appsec.obfuscatorKeyRegex', options.appsec
|
|
1022
|
-
this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec
|
|
1023
|
-
this._setBoolean(opts, 'appsec.rasp.enabled', options.appsec
|
|
1024
|
-
this._setValue(opts, 'appsec.rateLimit', maybeInt(options.appsec
|
|
1025
|
-
this._optsUnprocessed['appsec.rateLimit'] = options.appsec
|
|
1026
|
-
this._setString(opts, 'appsec.rules', options.appsec
|
|
1027
|
-
this._setBoolean(opts, 'appsec.stackTrace.enabled', options.appsec
|
|
1028
|
-
this._setValue(opts, 'appsec.stackTrace.maxDepth', maybeInt(options.appsec
|
|
1029
|
-
this._optsUnprocessed['appsec.stackTrace.maxDepth'] = options.appsec
|
|
1030
|
-
this._setValue(opts, 'appsec.stackTrace.maxStackTraces', maybeInt(options.appsec
|
|
1031
|
-
this._optsUnprocessed['appsec.stackTrace.maxStackTraces'] = options.appsec
|
|
1032
|
-
this._setValue(opts, 'appsec.wafTimeout', maybeInt(options.appsec
|
|
1033
|
-
this._optsUnprocessed['appsec.wafTimeout'] = options.appsec
|
|
1006
|
+
this._setBoolean(opts, 'appsec.apiSecurity.enabled', options.appsec?.apiSecurity?.enabled)
|
|
1007
|
+
this._setValue(opts, 'appsec.blockedTemplateGraphql', maybeFile(options.appsec?.blockedTemplateGraphql))
|
|
1008
|
+
this._setValue(opts, 'appsec.blockedTemplateHtml', maybeFile(options.appsec?.blockedTemplateHtml))
|
|
1009
|
+
this._optsUnprocessed['appsec.blockedTemplateHtml'] = options.appsec?.blockedTemplateHtml
|
|
1010
|
+
this._setValue(opts, 'appsec.blockedTemplateJson', maybeFile(options.appsec?.blockedTemplateJson))
|
|
1011
|
+
this._optsUnprocessed['appsec.blockedTemplateJson'] = options.appsec?.blockedTemplateJson
|
|
1012
|
+
this._setBoolean(opts, 'appsec.enabled', options.appsec?.enabled)
|
|
1013
|
+
this._setString(opts, 'appsec.eventTracking.mode', options.appsec?.eventTracking?.mode)
|
|
1014
|
+
this._setString(opts, 'appsec.obfuscatorKeyRegex', options.appsec?.obfuscatorKeyRegex)
|
|
1015
|
+
this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec?.obfuscatorValueRegex)
|
|
1016
|
+
this._setBoolean(opts, 'appsec.rasp.enabled', options.appsec?.rasp?.enabled)
|
|
1017
|
+
this._setValue(opts, 'appsec.rateLimit', maybeInt(options.appsec?.rateLimit))
|
|
1018
|
+
this._optsUnprocessed['appsec.rateLimit'] = options.appsec?.rateLimit
|
|
1019
|
+
this._setString(opts, 'appsec.rules', options.appsec?.rules)
|
|
1020
|
+
this._setBoolean(opts, 'appsec.stackTrace.enabled', options.appsec?.stackTrace?.enabled)
|
|
1021
|
+
this._setValue(opts, 'appsec.stackTrace.maxDepth', maybeInt(options.appsec?.stackTrace?.maxDepth))
|
|
1022
|
+
this._optsUnprocessed['appsec.stackTrace.maxDepth'] = options.appsec?.stackTrace?.maxDepth
|
|
1023
|
+
this._setValue(opts, 'appsec.stackTrace.maxStackTraces', maybeInt(options.appsec?.stackTrace?.maxStackTraces))
|
|
1024
|
+
this._optsUnprocessed['appsec.stackTrace.maxStackTraces'] = options.appsec?.stackTrace?.maxStackTraces
|
|
1025
|
+
this._setValue(opts, 'appsec.wafTimeout', maybeInt(options.appsec?.wafTimeout))
|
|
1026
|
+
this._optsUnprocessed['appsec.wafTimeout'] = options.appsec?.wafTimeout
|
|
1034
1027
|
this._setBoolean(opts, 'clientIpEnabled', options.clientIpEnabled)
|
|
1035
1028
|
this._setString(opts, 'clientIpHeader', options.clientIpHeader?.toLowerCase())
|
|
1036
1029
|
this._setValue(opts, 'baggageMaxBytes', options.baggageMaxBytes)
|
|
@@ -1063,7 +1056,6 @@ class Config {
|
|
|
1063
1056
|
this._optsUnprocessed.flushMinSpans = options.flushMinSpans
|
|
1064
1057
|
this._setArray(opts, 'headerTags', options.headerTags)
|
|
1065
1058
|
this._setString(opts, 'hostname', options.hostname)
|
|
1066
|
-
this._setString(opts, 'iast.cookieFilterPattern', options.iast?.cookieFilterPattern)
|
|
1067
1059
|
this._setValue(opts, 'iast.dbRowsToTaint', maybeInt(options.iast?.dbRowsToTaint))
|
|
1068
1060
|
this._setBoolean(opts, 'iast.deduplicationEnabled', options.iast && options.iast.deduplicationEnabled)
|
|
1069
1061
|
this._setBoolean(opts, 'iast.enabled',
|
|
@@ -13,16 +13,14 @@ const log = require('../log')
|
|
|
13
13
|
|
|
14
14
|
const ENTRY_PARENT_HASH = Buffer.from('0000000000000000', 'hex')
|
|
15
15
|
|
|
16
|
-
const HIGH_ACCURACY_DISTRIBUTION = 0.0075
|
|
17
|
-
|
|
18
16
|
class StatsPoint {
|
|
19
17
|
constructor (hash, parentHash, edgeTags) {
|
|
20
18
|
this.hash = hash.readBigUInt64BE()
|
|
21
19
|
this.parentHash = parentHash.readBigUInt64BE()
|
|
22
20
|
this.edgeTags = edgeTags
|
|
23
|
-
this.edgeLatency = new LogCollapsingLowestDenseDDSketch(
|
|
24
|
-
this.pathwayLatency = new LogCollapsingLowestDenseDDSketch(
|
|
25
|
-
this.payloadSize = new LogCollapsingLowestDenseDDSketch(
|
|
21
|
+
this.edgeLatency = new LogCollapsingLowestDenseDDSketch()
|
|
22
|
+
this.pathwayLatency = new LogCollapsingLowestDenseDDSketch()
|
|
23
|
+
this.payloadSize = new LogCollapsingLowestDenseDDSketch()
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
addLatencies (checkpoint) {
|
|
@@ -308,12 +308,16 @@ class MetricsAggregationClient {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
for (const [tag, next] of node.nodes) {
|
|
311
|
-
|
|
311
|
+
tags.push(tag)
|
|
312
|
+
this._captureNode(next, name, tags, fn)
|
|
313
|
+
tags.pop()
|
|
312
314
|
}
|
|
313
315
|
}
|
|
314
316
|
|
|
315
|
-
_ensureTree (tree, name, tags, value) {
|
|
316
|
-
|
|
317
|
+
_ensureTree (tree, name, tags = [], value) {
|
|
318
|
+
if (!Array.isArray(tags)) {
|
|
319
|
+
tags = [tags]
|
|
320
|
+
}
|
|
317
321
|
|
|
318
322
|
let node = this._ensureNode(tree, name, value)
|
|
319
323
|
|
|
@@ -331,7 +335,10 @@ class MetricsAggregationClient {
|
|
|
331
335
|
|
|
332
336
|
if (!node) {
|
|
333
337
|
node = { nodes: new Map(), touched: false, value }
|
|
334
|
-
|
|
338
|
+
|
|
339
|
+
if (typeof key === 'string') {
|
|
340
|
+
container.set(key, node)
|
|
341
|
+
}
|
|
335
342
|
}
|
|
336
343
|
|
|
337
344
|
return node
|
|
@@ -4,9 +4,9 @@ const log = require('../log')
|
|
|
4
4
|
const { PROPAGATED_PARENT_ID_KEY } = require('./constants/tags')
|
|
5
5
|
const { storage } = require('./storage')
|
|
6
6
|
|
|
7
|
+
const telemetry = require('./telemetry')
|
|
7
8
|
const LLMObsSpanProcessor = require('./span_processor')
|
|
8
9
|
|
|
9
|
-
const telemetry = require('./telemetry')
|
|
10
10
|
const { channel } = require('dc-polyfill')
|
|
11
11
|
const spanProcessCh = channel('dd-trace:span:process')
|
|
12
12
|
const evalMetricAppendCh = channel('llmobs:eval-metric:append')
|
|
@@ -94,12 +94,15 @@ function handleLLMObsParentIdInjection ({ carrier }) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function handleFlush () {
|
|
97
|
+
let err = ''
|
|
97
98
|
try {
|
|
98
99
|
spanWriter.flush()
|
|
99
100
|
evalWriter.flush()
|
|
100
101
|
} catch (e) {
|
|
102
|
+
err = 'writer_flush_error'
|
|
101
103
|
log.warn(`Failed to flush LLMObs spans and evaluation metrics: ${e.message}`)
|
|
102
104
|
}
|
|
105
|
+
telemetry.recordUserFlush(err)
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
function handleSpanProcess (data) {
|
|
@@ -201,7 +201,7 @@ class LLMObs extends NoopLLMObs {
|
|
|
201
201
|
return this._tracer.wrap(name, spanOptions, wrapped)
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
annotate (span, options) {
|
|
204
|
+
annotate (span, options, autoinstrumented = false) {
|
|
205
205
|
if (!this.enabled) return
|
|
206
206
|
|
|
207
207
|
if (!span) {
|
|
@@ -213,150 +213,184 @@ class LLMObs extends NoopLLMObs {
|
|
|
213
213
|
span = this._active()
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
throw new Error('No span provided and no active LLMObs-generated span found')
|
|
218
|
-
}
|
|
219
|
-
if (!options) {
|
|
220
|
-
throw new Error('No options provided for annotation.')
|
|
221
|
-
}
|
|
216
|
+
let err = ''
|
|
222
217
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
218
|
+
try {
|
|
219
|
+
if (!span) {
|
|
220
|
+
err = 'invalid_span_no_active_spans'
|
|
221
|
+
throw new Error('No span provided and no active LLMObs-generated span found')
|
|
222
|
+
}
|
|
223
|
+
if (!options) {
|
|
224
|
+
err = 'invalid_options'
|
|
225
|
+
throw new Error('No options provided for annotation.')
|
|
226
|
+
}
|
|
229
227
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
228
|
+
if (!LLMObsTagger.tagMap.has(span)) {
|
|
229
|
+
err = 'invalid_span_type'
|
|
230
|
+
throw new Error('Span must be an LLMObs-generated span')
|
|
231
|
+
}
|
|
232
|
+
if (span._duration !== undefined) {
|
|
233
|
+
err = 'invalid_finished_span'
|
|
234
|
+
throw new Error('Cannot annotate a finished span')
|
|
235
|
+
}
|
|
234
236
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
this._tagger.tagLLMIO(span, inputData, outputData)
|
|
240
|
-
} else if (spanKind === 'embedding') {
|
|
241
|
-
this._tagger.tagEmbeddingIO(span, inputData, outputData)
|
|
242
|
-
} else if (spanKind === 'retrieval') {
|
|
243
|
-
this._tagger.tagRetrievalIO(span, inputData, outputData)
|
|
244
|
-
} else {
|
|
245
|
-
this._tagger.tagTextIO(span, inputData, outputData)
|
|
237
|
+
const spanKind = LLMObsTagger.tagMap.get(span)[SPAN_KIND]
|
|
238
|
+
if (!spanKind) {
|
|
239
|
+
err = 'invalid_no_span_kind'
|
|
240
|
+
throw new Error('LLMObs span must have a span kind specified')
|
|
246
241
|
}
|
|
247
|
-
}
|
|
248
242
|
|
|
249
|
-
|
|
250
|
-
this._tagger.tagMetadata(span, metadata)
|
|
251
|
-
}
|
|
243
|
+
const { inputData, outputData, metadata, metrics, tags } = options
|
|
252
244
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
245
|
+
if (inputData || outputData) {
|
|
246
|
+
if (spanKind === 'llm') {
|
|
247
|
+
this._tagger.tagLLMIO(span, inputData, outputData)
|
|
248
|
+
} else if (spanKind === 'embedding') {
|
|
249
|
+
this._tagger.tagEmbeddingIO(span, inputData, outputData)
|
|
250
|
+
} else if (spanKind === 'retrieval') {
|
|
251
|
+
this._tagger.tagRetrievalIO(span, inputData, outputData)
|
|
252
|
+
} else {
|
|
253
|
+
this._tagger.tagTextIO(span, inputData, outputData)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
if (metadata) {
|
|
258
|
+
this._tagger.tagMetadata(span, metadata)
|
|
259
|
+
}
|
|
260
|
+
if (metrics) {
|
|
261
|
+
this._tagger.tagMetrics(span, metrics)
|
|
262
|
+
}
|
|
263
|
+
if (tags) {
|
|
264
|
+
this._tagger.tagSpanTags(span, tags)
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
if (e.ddErrorTag) {
|
|
268
|
+
err = e.ddErrorTag
|
|
269
|
+
}
|
|
270
|
+
throw e
|
|
271
|
+
} finally {
|
|
272
|
+
if (autoinstrumented === false) {
|
|
273
|
+
telemetry.recordLLMObsAnnotate(span, err)
|
|
274
|
+
}
|
|
259
275
|
}
|
|
260
276
|
}
|
|
261
277
|
|
|
262
278
|
exportSpan (span) {
|
|
263
279
|
span = span || this._active()
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
280
|
+
let err = ''
|
|
281
|
+
try {
|
|
282
|
+
if (!span) {
|
|
283
|
+
err = 'no_active_span'
|
|
284
|
+
throw new Error('No span provided and no active LLMObs-generated span found')
|
|
285
|
+
}
|
|
286
|
+
if (!(span instanceof Span)) {
|
|
287
|
+
err = 'invalid_span'
|
|
288
|
+
throw new Error('Span must be a valid Span object.')
|
|
289
|
+
}
|
|
290
|
+
if (!LLMObsTagger.tagMap.has(span)) {
|
|
291
|
+
err = 'invalid_span'
|
|
292
|
+
throw new Error('Span must be an LLMObs-generated span')
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
telemetry.recordExportSpan(span, err)
|
|
296
|
+
throw e
|
|
275
297
|
}
|
|
276
|
-
|
|
277
298
|
try {
|
|
278
299
|
return {
|
|
279
300
|
traceId: span.context().toTraceId(true),
|
|
280
301
|
spanId: span.context().toSpanId()
|
|
281
302
|
}
|
|
282
303
|
} catch {
|
|
283
|
-
|
|
304
|
+
err = 'invalid_span'
|
|
305
|
+
logger.warn('Failed to export span. Span must be a valid Span object.')
|
|
306
|
+
} finally {
|
|
307
|
+
telemetry.recordExportSpan(span, err)
|
|
284
308
|
}
|
|
285
309
|
}
|
|
286
310
|
|
|
287
311
|
submitEvaluation (llmobsSpanContext, options = {}) {
|
|
288
312
|
if (!this.enabled) return
|
|
289
313
|
|
|
314
|
+
let err = ''
|
|
290
315
|
const { traceId, spanId } = llmobsSpanContext
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (typeof timestampMs !== 'number' || timestampMs < 0) {
|
|
306
|
-
throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
|
|
307
|
-
}
|
|
316
|
+
try {
|
|
317
|
+
if (!traceId || !spanId) {
|
|
318
|
+
err = 'invalid_span'
|
|
319
|
+
throw new Error(
|
|
320
|
+
'spanId and traceId must both be specified for the given evaluation metric to be submitted.'
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
const mlApp = options.mlApp || this._config.llmobs.mlApp
|
|
324
|
+
if (!mlApp) {
|
|
325
|
+
err = 'missing_ml_app'
|
|
326
|
+
throw new Error(
|
|
327
|
+
'ML App name is required for sending evaluation metrics. Evaluation metric data will not be sent.'
|
|
328
|
+
)
|
|
329
|
+
}
|
|
308
330
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (!metricType || !['categorical', 'score'].includes(metricType)) {
|
|
315
|
-
throw new Error('metricType must be one of "categorical" or "score"')
|
|
316
|
-
}
|
|
331
|
+
const timestampMs = options.timestampMs || Date.now()
|
|
332
|
+
if (typeof timestampMs !== 'number' || timestampMs < 0) {
|
|
333
|
+
err = 'invalid_timestamp'
|
|
334
|
+
throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
|
|
335
|
+
}
|
|
317
336
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
337
|
+
const { label, value, tags } = options
|
|
338
|
+
const metricType = options.metricType?.toLowerCase()
|
|
339
|
+
if (!label) {
|
|
340
|
+
err = 'invalid_metric_label'
|
|
341
|
+
throw new Error('label must be the specified name of the evaluation metric')
|
|
342
|
+
}
|
|
343
|
+
if (!metricType || !['categorical', 'score'].includes(metricType)) {
|
|
344
|
+
err = 'invalid_metric_type'
|
|
345
|
+
throw new Error('metricType must be one of "categorical" or "score"')
|
|
346
|
+
}
|
|
347
|
+
if (metricType === 'categorical' && typeof value !== 'string') {
|
|
348
|
+
err = 'invalid_metric_value'
|
|
349
|
+
throw new Error('value must be a string for a categorical metric.')
|
|
350
|
+
}
|
|
351
|
+
if (metricType === 'score' && typeof value !== 'number') {
|
|
352
|
+
err = 'invalid_metric_value'
|
|
353
|
+
throw new Error('value must be a number for a score metric.')
|
|
354
|
+
}
|
|
324
355
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
356
|
+
const evaluationTags = {
|
|
357
|
+
'ddtrace.version': tracerVersion,
|
|
358
|
+
ml_app: mlApp
|
|
359
|
+
}
|
|
329
360
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
361
|
+
if (tags) {
|
|
362
|
+
for (const key in tags) {
|
|
363
|
+
const tag = tags[key]
|
|
364
|
+
if (typeof tag === 'string') {
|
|
365
|
+
evaluationTags[key] = tag
|
|
366
|
+
} else if (typeof tag.toString === 'function') {
|
|
367
|
+
evaluationTags[key] = tag.toString()
|
|
368
|
+
} else if (tag == null) {
|
|
369
|
+
evaluationTags[key] = Object.prototype.toString.call(tag)
|
|
370
|
+
} else {
|
|
371
|
+
// should be a rare case
|
|
372
|
+
// every object in JS has a toString, otherwise every primitive has its own toString
|
|
373
|
+
// null and undefined are handled above
|
|
374
|
+
err = 'invalid_tags'
|
|
375
|
+
throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
|
|
376
|
+
}
|
|
344
377
|
}
|
|
345
378
|
}
|
|
346
|
-
}
|
|
347
379
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
380
|
+
const payload = {
|
|
381
|
+
span_id: spanId,
|
|
382
|
+
trace_id: traceId,
|
|
383
|
+
label,
|
|
384
|
+
metric_type: metricType,
|
|
385
|
+
ml_app: mlApp,
|
|
386
|
+
[`${metricType}_value`]: value,
|
|
387
|
+
timestamp_ms: timestampMs,
|
|
388
|
+
tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
|
|
389
|
+
}
|
|
390
|
+
evalMetricAppendCh.publish(payload)
|
|
391
|
+
} finally {
|
|
392
|
+
telemetry.recordSubmitEvaluation(options, err)
|
|
357
393
|
}
|
|
358
|
-
|
|
359
|
-
evalMetricAppendCh.publish(payload)
|
|
360
394
|
}
|
|
361
395
|
|
|
362
396
|
flush () {
|
|
@@ -375,7 +409,7 @@ class LLMObs extends NoopLLMObs {
|
|
|
375
409
|
annotations.outputData = output
|
|
376
410
|
}
|
|
377
411
|
|
|
378
|
-
this.annotate(span, annotations)
|
|
412
|
+
this.annotate(span, annotations, true)
|
|
379
413
|
}
|
|
380
414
|
|
|
381
415
|
_active () {
|
|
@@ -135,7 +135,7 @@ class LLMObsTagger {
|
|
|
135
135
|
if (typeof value === 'number') {
|
|
136
136
|
filterdMetrics[processedKey] = value
|
|
137
137
|
} else {
|
|
138
|
-
this._handleFailure(`Value for metric '${key}' must be a number, instead got ${value}
|
|
138
|
+
this._handleFailure(`Value for metric '${key}' must be a number, instead got ${value}`, 'invalid_metrics')
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -169,7 +169,7 @@ class LLMObsTagger {
|
|
|
169
169
|
this._setTag(span, key, JSON.stringify(data))
|
|
170
170
|
} catch {
|
|
171
171
|
const type = key === INPUT_VALUE ? 'input' : 'output'
|
|
172
|
-
this._handleFailure(`Failed to parse ${type} value, must be JSON serializable
|
|
172
|
+
this._handleFailure(`Failed to parse ${type} value, must be JSON serializable.`, 'invalid_io_text')
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
}
|
|
@@ -187,7 +187,7 @@ class LLMObsTagger {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
if (document == null || typeof document !== 'object') {
|
|
190
|
-
this._handleFailure('Documents must be a string, object, or list of objects.')
|
|
190
|
+
this._handleFailure('Documents must be a string, object, or list of objects.', 'invalid_embedding_io')
|
|
191
191
|
return undefined
|
|
192
192
|
}
|
|
193
193
|
|
|
@@ -195,7 +195,7 @@ class LLMObsTagger {
|
|
|
195
195
|
let validDocument = true
|
|
196
196
|
|
|
197
197
|
if (typeof text !== 'string') {
|
|
198
|
-
this._handleFailure('Document text must be a string.')
|
|
198
|
+
this._handleFailure('Document text must be a string.', 'invalid_embedding_io')
|
|
199
199
|
validDocument = false
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -226,7 +226,7 @@ class LLMObsTagger {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
if (message == null || typeof message !== 'object') {
|
|
229
|
-
this._handleFailure('Messages must be a string, object, or list of objects')
|
|
229
|
+
this._handleFailure('Messages must be a string, object, or list of objects', 'invalid_io_messages')
|
|
230
230
|
return undefined
|
|
231
231
|
}
|
|
232
232
|
|
|
@@ -237,7 +237,7 @@ class LLMObsTagger {
|
|
|
237
237
|
const messageObj = { content }
|
|
238
238
|
|
|
239
239
|
if (typeof content !== 'string') {
|
|
240
|
-
this._handleFailure('Message content must be a string.')
|
|
240
|
+
this._handleFailure('Message content must be a string.', 'invalid_io_messages')
|
|
241
241
|
validMessage = false
|
|
242
242
|
}
|
|
243
243
|
|
|
@@ -250,7 +250,7 @@ class LLMObsTagger {
|
|
|
250
250
|
|
|
251
251
|
const filteredToolCalls = toolCalls.map(toolCall => {
|
|
252
252
|
if (typeof toolCall !== 'object') {
|
|
253
|
-
this._handleFailure('Tool call must be an object.')
|
|
253
|
+
this._handleFailure('Tool call must be an object.', 'invalid_io_messages')
|
|
254
254
|
return undefined
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -313,11 +313,15 @@ class LLMObsTagger {
|
|
|
313
313
|
|
|
314
314
|
// any public-facing LLMObs APIs using this tagger should not soft fail
|
|
315
315
|
// auto-instrumentation should soft fail
|
|
316
|
-
_handleFailure (msg) {
|
|
316
|
+
_handleFailure (msg, errorTag) {
|
|
317
317
|
if (this.softFail) {
|
|
318
318
|
log.warn(msg)
|
|
319
319
|
} else {
|
|
320
|
-
|
|
320
|
+
const err = new Error(msg)
|
|
321
|
+
if (errorTag) {
|
|
322
|
+
Object.defineProperty(err, 'ddErrorTag', { get () { return errorTag } })
|
|
323
|
+
}
|
|
324
|
+
throw err
|
|
321
325
|
}
|
|
322
326
|
}
|
|
323
327
|
|
|
@@ -109,11 +109,60 @@ function recordDroppedPayload (numEvents, eventType, error) {
|
|
|
109
109
|
llmobsMetrics.count(metricName, tags).inc(numEvents)
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function recordLLMObsAnnotate (span, err, value = 1) {
|
|
113
|
+
const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
|
|
114
|
+
const spanKind = mlObsTags[SPAN_KIND] || 'N/A'
|
|
115
|
+
const isRootSpan = mlObsTags[PARENT_ID_KEY] === ROOT_PARENT_ID
|
|
116
|
+
|
|
117
|
+
const tags = {
|
|
118
|
+
error: Number(!!err),
|
|
119
|
+
span_kind: spanKind,
|
|
120
|
+
is_root_span: Number(isRootSpan)
|
|
121
|
+
}
|
|
122
|
+
if (err) tags.error_type = err
|
|
123
|
+
llmobsMetrics.count('annotations', tags).inc(value)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function recordUserFlush (err, value = 1) {
|
|
127
|
+
const tags = { error: Number(!!err) }
|
|
128
|
+
if (err) tags.error_type = err
|
|
129
|
+
llmobsMetrics.count('user_flush', tags).inc(value)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function recordExportSpan (span, err, value = 1) {
|
|
133
|
+
const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
|
|
134
|
+
const spanKind = mlObsTags[SPAN_KIND] || 'N/A'
|
|
135
|
+
const isRootSpan = mlObsTags[PARENT_ID_KEY] === ROOT_PARENT_ID
|
|
136
|
+
|
|
137
|
+
const tags = {
|
|
138
|
+
error: Number(!!err),
|
|
139
|
+
span_kind: spanKind,
|
|
140
|
+
is_root_span: Number(isRootSpan)
|
|
141
|
+
}
|
|
142
|
+
if (err) tags.error_type = err
|
|
143
|
+
llmobsMetrics.count('spans_exported', tags).inc(value)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function recordSubmitEvaluation (options, err, value = 1) {
|
|
147
|
+
const tags = {
|
|
148
|
+
error: Number(!!err),
|
|
149
|
+
custom_joining_key: 0
|
|
150
|
+
}
|
|
151
|
+
const metricType = options?.metricType?.toLowerCase()
|
|
152
|
+
if (metricType !== 'categorical' && metricType !== 'score') tags.metric_type = 'other'
|
|
153
|
+
if (err) tags.error_type = err
|
|
154
|
+
llmobsMetrics.count('evals_submitted', tags).inc(value)
|
|
155
|
+
}
|
|
156
|
+
|
|
112
157
|
module.exports = {
|
|
113
158
|
recordLLMObsEnabled,
|
|
114
159
|
incrementLLMObsSpanStartCount,
|
|
115
160
|
incrementLLMObsSpanFinishedCount,
|
|
116
161
|
recordLLMObsRawSpanSize,
|
|
117
162
|
recordLLMObsSpanSize,
|
|
118
|
-
recordDroppedPayload
|
|
163
|
+
recordDroppedPayload,
|
|
164
|
+
recordLLMObsAnnotate,
|
|
165
|
+
recordUserFlush,
|
|
166
|
+
recordExportSpan,
|
|
167
|
+
recordSubmitEvaluation
|
|
119
168
|
}
|
|
@@ -1694,7 +1694,7 @@ JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
|
|
|
1694
1694
|
* @param {string} parentPropName
|
|
1695
1695
|
* @param {JSONPathCallback} callback
|
|
1696
1696
|
* @param {boolean} hasArrExpr
|
|
1697
|
-
* @param {boolean} literalPriority
|
|
1697
|
+
* @param {boolean} [literalPriority]
|
|
1698
1698
|
* @returns {ReturnObject|ReturnObject[]}
|
|
1699
1699
|
*/
|
|
1700
1700
|
JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback, hasArrExpr, literalPriority) {
|
|
@@ -15,7 +15,6 @@ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags')
|
|
|
15
15
|
const { tagger } = require('./tagger')
|
|
16
16
|
const { isFalse, isTrue } = require('../util')
|
|
17
17
|
const { getAzureTagsFromMetadata, getAzureAppMetadata } = require('../azure_metadata')
|
|
18
|
-
const satisfies = require('semifies')
|
|
19
18
|
|
|
20
19
|
class Config {
|
|
21
20
|
constructor (options = {}) {
|
|
@@ -23,7 +22,6 @@ class Config {
|
|
|
23
22
|
DD_AGENT_HOST,
|
|
24
23
|
DD_ENV,
|
|
25
24
|
DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, // used for testing
|
|
26
|
-
DD_PROFILING_ASYNC_ID_ENABLED,
|
|
27
25
|
DD_PROFILING_CODEHOTSPOTS_ENABLED,
|
|
28
26
|
DD_PROFILING_CPU_ENABLED,
|
|
29
27
|
DD_PROFILING_DEBUG_SOURCE_MAPS,
|
|
@@ -181,10 +179,6 @@ class Config {
|
|
|
181
179
|
this.timelineSamplingEnabled = isTrue(coalesce(options.timelineSamplingEnabled,
|
|
182
180
|
DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, true))
|
|
183
181
|
|
|
184
|
-
// Async ID gathering only works reliably on Node >= 22.10.0
|
|
185
|
-
this.asyncIdEnabled = isTrue(coalesce(options.asyncIdEnabled,
|
|
186
|
-
DD_PROFILING_ASYNC_ID_ENABLED, this.timelineEnabled && satisfies(process.versions.node, '>=22.10.0')))
|
|
187
|
-
|
|
188
182
|
this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
|
|
189
183
|
DD_PROFILING_CODEHOTSPOTS_ENABLED,
|
|
190
184
|
DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, samplingContextsAvailable))
|
|
@@ -70,16 +70,12 @@ function ensureChannelsActivated () {
|
|
|
70
70
|
class NativeWallProfiler {
|
|
71
71
|
constructor (options = {}) {
|
|
72
72
|
this.type = 'wall'
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this._asyncIdEnabled = !!options.asyncIdEnabled && require('worker_threads').isMainThread
|
|
73
|
+
this._samplingIntervalMicros = options.samplingInterval || 1e6 / 99 // 99hz
|
|
74
|
+
this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
|
|
76
75
|
this._codeHotspotsEnabled = !!options.codeHotspotsEnabled
|
|
77
|
-
this._cpuProfilingEnabled = !!options.cpuProfilingEnabled
|
|
78
76
|
this._endpointCollectionEnabled = !!options.endpointCollectionEnabled
|
|
79
|
-
this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds
|
|
80
|
-
this._samplingIntervalMicros = options.samplingInterval || 1e6 / 99 // 99hz
|
|
81
77
|
this._timelineEnabled = !!options.timelineEnabled
|
|
82
|
-
this.
|
|
78
|
+
this._cpuProfilingEnabled = !!options.cpuProfilingEnabled
|
|
83
79
|
// We need to capture span data into the sample context for either code hotspots
|
|
84
80
|
// or endpoint collection.
|
|
85
81
|
this._captureSpanData = this._codeHotspotsEnabled || this._endpointCollectionEnabled
|
|
@@ -88,6 +84,7 @@ class NativeWallProfiler {
|
|
|
88
84
|
// timestamps require the sample contexts feature in the pprof wall profiler), or
|
|
89
85
|
// cpu profiling is enabled.
|
|
90
86
|
this._withContexts = this._captureSpanData || this._timelineEnabled || this._cpuProfilingEnabled
|
|
87
|
+
this._v8ProfilerBugWorkaroundEnabled = !!options.v8ProfilerBugWorkaroundEnabled
|
|
91
88
|
this._mapper = undefined
|
|
92
89
|
this._pprof = undefined
|
|
93
90
|
|
|
@@ -128,14 +125,13 @@ class NativeWallProfiler {
|
|
|
128
125
|
}
|
|
129
126
|
|
|
130
127
|
this._pprof.time.start({
|
|
131
|
-
collectAsyncId: this._asyncIdEnabled,
|
|
132
|
-
collectCpuTime: this._cpuProfilingEnabled,
|
|
133
|
-
durationMillis: this._flushIntervalMillis,
|
|
134
128
|
intervalMicros: this._samplingIntervalMicros,
|
|
135
|
-
|
|
129
|
+
durationMillis: this._flushIntervalMillis,
|
|
136
130
|
sourceMapper: this._mapper,
|
|
137
131
|
withContexts: this._withContexts,
|
|
138
|
-
|
|
132
|
+
lineNumbers: false,
|
|
133
|
+
workaroundV8Bug: this._v8ProfilerBugWorkaroundEnabled,
|
|
134
|
+
collectCpuTime: this._cpuProfilingEnabled
|
|
139
135
|
})
|
|
140
136
|
|
|
141
137
|
if (this._withContexts) {
|
|
@@ -23,8 +23,8 @@ class SpanAggStats {
|
|
|
23
23
|
this.topLevelHits = 0
|
|
24
24
|
this.errors = 0
|
|
25
25
|
this.duration = 0
|
|
26
|
-
this.okDistribution = new LogCollapsingLowestDenseDDSketch(
|
|
27
|
-
this.errorDistribution = new LogCollapsingLowestDenseDDSketch(
|
|
26
|
+
this.okDistribution = new LogCollapsingLowestDenseDDSketch()
|
|
27
|
+
this.errorDistribution = new LogCollapsingLowestDenseDDSketch()
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
record (span) {
|