dd-trace 4.53.1 → 4.55.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/package.json +3 -3
- package/packages/datadog-core/src/storage.js +11 -2
- package/packages/datadog-instrumentations/src/aerospike.js +1 -1
- package/packages/datadog-instrumentations/src/fs.js +3 -0
- package/packages/dd-trace/src/appsec/addresses.js +1 -0
- package/packages/dd-trace/src/appsec/rasp/command_injection.js +13 -6
- package/packages/dd-trace/src/appsec/rasp/lfi.js +3 -1
- package/packages/dd-trace/src/appsec/rasp/sql_injection.js +3 -1
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +3 -1
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +2 -0
- package/packages/dd-trace/src/appsec/remote_config/manager.js +11 -1
- package/packages/dd-trace/src/appsec/reporter.js +3 -3
- package/packages/dd-trace/src/appsec/telemetry.js +7 -2
- package/packages/dd-trace/src/appsec/waf/index.js +2 -2
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +2 -2
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/index.js +4 -3
- package/packages/dd-trace/src/debugger/devtools_client/json-buffer.js +36 -0
- package/packages/dd-trace/src/debugger/devtools_client/send.js +33 -10
- package/packages/dd-trace/src/debugger/devtools_client/status.js +9 -2
- package/packages/dd-trace/src/llmobs/sdk.js +90 -26
- package/packages/dd-trace/src/log/index.js +11 -2
- package/packages/dd-trace/src/log/writer.js +4 -3
- package/packages/dd-trace/src/noop/proxy.js +2 -2
- package/packages/dd-trace/src/noop/span.js +1 -1
- package/packages/dd-trace/src/opentracing/span.js +12 -2
- package/packages/dd-trace/src/opentracing/span_context.js +12 -0
- package/packages/dd-trace/src/priority_sampler.js +4 -2
- package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +7 -3
- package/packages/dd-trace/src/profiling/profilers/event_plugins/fs.js +49 -0
- package/packages/dd-trace/src/profiling/profilers/events.js +23 -2
- package/packages/dd-trace/src/scope.js +1 -1
- package/packages/dd-trace/src/telemetry/index.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.55.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
84
|
"@datadog/libdatadog": "^0.3.0",
|
|
85
|
-
"@datadog/native-appsec": "8.
|
|
85
|
+
"@datadog/native-appsec": "8.4.0",
|
|
86
86
|
"@datadog/native-iast-rewriter": "2.6.1",
|
|
87
87
|
"@datadog/native-iast-taint-tracking": "3.2.0",
|
|
88
88
|
"@datadog/native-metrics": "^3.1.0",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"jszip": "^3.5.0",
|
|
146
146
|
"knex": "^2.4.2",
|
|
147
147
|
"mkdirp": "^3.0.1",
|
|
148
|
-
"mocha": "^
|
|
148
|
+
"mocha": "^10",
|
|
149
149
|
"msgpack-lite": "^0.1.26",
|
|
150
150
|
"multer": "^1.4.5-lts.1",
|
|
151
151
|
"nock": "^11.3.3",
|
|
@@ -21,8 +21,16 @@ class DatadogStorage {
|
|
|
21
21
|
this._storage.exit(callback, ...args)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// TODO: Refactor the Scope class to use a span-only store and remove this.
|
|
25
|
+
getHandle () {
|
|
26
|
+
return this._storage.getStore()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getStore (handle) {
|
|
30
|
+
if (!handle) {
|
|
31
|
+
handle = this._storage.getStore()
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
return stores.get(handle)
|
|
27
35
|
}
|
|
28
36
|
|
|
@@ -50,6 +58,7 @@ const storage = function (namespace) {
|
|
|
50
58
|
storage.disable = legacyStorage.disable.bind(legacyStorage)
|
|
51
59
|
storage.enterWith = legacyStorage.enterWith.bind(legacyStorage)
|
|
52
60
|
storage.exit = legacyStorage.exit.bind(legacyStorage)
|
|
61
|
+
storage.getHandle = legacyStorage.getHandle.bind(legacyStorage)
|
|
53
62
|
storage.getStore = legacyStorage.getStore.bind(legacyStorage)
|
|
54
63
|
storage.run = legacyStorage.run.bind(legacyStorage)
|
|
55
64
|
|
|
@@ -13,6 +13,9 @@ const errorChannel = channel('apm:fs:operation:error')
|
|
|
13
13
|
const ddFhSym = Symbol('ddFileHandle')
|
|
14
14
|
let kHandle, kDirReadPromisified, kDirClosePromisified
|
|
15
15
|
|
|
16
|
+
// Update packages/dd-trace/src/profiling/profilers/event_plugins/fs.js if you make changes to param names in any of
|
|
17
|
+
// the following objects.
|
|
18
|
+
|
|
16
19
|
const paramsByMethod = {
|
|
17
20
|
access: ['path', 'mode'],
|
|
18
21
|
appendFile: ['path', 'data', 'options'],
|
|
@@ -25,19 +25,26 @@ function disable () {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function analyzeCommandInjection ({ file, fileArgs, shell, abortController }) {
|
|
28
|
-
if (!file
|
|
28
|
+
if (!file) return
|
|
29
29
|
|
|
30
30
|
const store = storage.getStore()
|
|
31
31
|
const req = store?.req
|
|
32
32
|
if (!req) return
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
34
|
+
const persistent = {}
|
|
35
|
+
const raspRule = { type: RULE_TYPES.COMMAND_INJECTION }
|
|
36
|
+
const params = fileArgs ? [file, ...fileArgs] : file
|
|
37
|
+
|
|
38
|
+
if (shell) {
|
|
39
|
+
persistent[addresses.SHELL_COMMAND] = params
|
|
40
|
+
raspRule.variant = 'shell'
|
|
41
|
+
} else {
|
|
42
|
+
const commandParams = Array.isArray(params) ? params : [params]
|
|
43
|
+
persistent[addresses.EXEC_COMMAND] = commandParams
|
|
44
|
+
raspRule.variant = 'exec'
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
const result = waf.run({ persistent }, req,
|
|
47
|
+
const result = waf.run({ persistent }, req, raspRule)
|
|
41
48
|
|
|
42
49
|
const res = store?.res
|
|
43
50
|
handleResult(result, req, res, abortController, config)
|
|
@@ -58,7 +58,9 @@ function analyzeLfi (ctx) {
|
|
|
58
58
|
[FS_OPERATION_PATH]: path
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const
|
|
61
|
+
const raspRule = { type: RULE_TYPES.LFI }
|
|
62
|
+
|
|
63
|
+
const result = waf.run({ persistent }, req, raspRule)
|
|
62
64
|
handleResult(result, req, res, ctx.abortController, config)
|
|
63
65
|
})
|
|
64
66
|
}
|
|
@@ -72,7 +72,9 @@ function analyzeSqlInjection (query, dbSystem, abortController) {
|
|
|
72
72
|
[addresses.DB_SYSTEM]: dbSystem
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const
|
|
75
|
+
const raspRule = { type: RULE_TYPES.SQL_INJECTION }
|
|
76
|
+
|
|
77
|
+
const result = waf.run({ persistent }, req, raspRule)
|
|
76
78
|
|
|
77
79
|
handleResult(result, req, res, abortController, config)
|
|
78
80
|
}
|
|
@@ -29,7 +29,9 @@ function analyzeSsrf (ctx) {
|
|
|
29
29
|
[addresses.HTTP_OUTGOING_URL]: outgoingUrl
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const raspRule = { type: RULE_TYPES.SSRF }
|
|
33
|
+
|
|
34
|
+
const result = waf.run({ persistent }, req, raspRule)
|
|
33
35
|
|
|
34
36
|
const res = store?.res
|
|
35
37
|
handleResult(result, req, res, ctx.abortController, config)
|
|
@@ -101,6 +101,7 @@ function enableWafUpdate (appsecConfig) {
|
|
|
101
101
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, true)
|
|
102
102
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, true)
|
|
103
103
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, true)
|
|
104
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, true)
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
// TODO: delete noop handlers and kPreUpdate and replace with batched handlers
|
|
@@ -133,6 +134,7 @@ function disableWafUpdate () {
|
|
|
133
134
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, false)
|
|
134
135
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, false)
|
|
135
136
|
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, false)
|
|
137
|
+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, false)
|
|
136
138
|
|
|
137
139
|
rc.removeProductHandler('ASM_DATA')
|
|
138
140
|
rc.removeProductHandler('ASM_DD')
|
|
@@ -9,6 +9,7 @@ const log = require('../../log')
|
|
|
9
9
|
const { getExtraServices } = require('../../service-naming/extra-services')
|
|
10
10
|
const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('./apply_states')
|
|
11
11
|
const Scheduler = require('./scheduler')
|
|
12
|
+
const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags')
|
|
12
13
|
|
|
13
14
|
const clientId = uuid()
|
|
14
15
|
|
|
@@ -33,6 +34,14 @@ class RemoteConfigManager extends EventEmitter {
|
|
|
33
34
|
port: config.port
|
|
34
35
|
}))
|
|
35
36
|
|
|
37
|
+
const tags = config.repositoryUrl
|
|
38
|
+
? {
|
|
39
|
+
...config.tags,
|
|
40
|
+
[GIT_REPOSITORY_URL]: config.repositoryUrl,
|
|
41
|
+
[GIT_COMMIT_SHA]: config.commitSHA
|
|
42
|
+
}
|
|
43
|
+
: config.tags
|
|
44
|
+
|
|
36
45
|
this._handlers = new Map()
|
|
37
46
|
const appliedConfigs = this.appliedConfigs = new Map()
|
|
38
47
|
|
|
@@ -67,7 +76,8 @@ class RemoteConfigManager extends EventEmitter {
|
|
|
67
76
|
service: config.service,
|
|
68
77
|
env: config.env,
|
|
69
78
|
app_version: config.version,
|
|
70
|
-
extra_services: []
|
|
79
|
+
extra_services: [],
|
|
80
|
+
tags: Object.entries(tags).map((pair) => pair.join(':'))
|
|
71
81
|
},
|
|
72
82
|
capabilities: DEFAULT_CAPABILITY // updated by `updateCapabilities()`
|
|
73
83
|
},
|
|
@@ -101,7 +101,7 @@ function reportWafInit (wafVersion, rulesVersion, diagnosticsRules = {}) {
|
|
|
101
101
|
incrementWafInitMetric(wafVersion, rulesVersion)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function reportMetrics (metrics,
|
|
104
|
+
function reportMetrics (metrics, raspRule) {
|
|
105
105
|
const store = storage.getStore()
|
|
106
106
|
const rootSpan = store?.req && web.root(store.req)
|
|
107
107
|
if (!rootSpan) return
|
|
@@ -109,8 +109,8 @@ function reportMetrics (metrics, raspRuleType) {
|
|
|
109
109
|
if (metrics.rulesVersion) {
|
|
110
110
|
rootSpan.setTag('_dd.appsec.event_rules.version', metrics.rulesVersion)
|
|
111
111
|
}
|
|
112
|
-
if (
|
|
113
|
-
updateRaspRequestsMetricTags(metrics, store.req,
|
|
112
|
+
if (raspRule) {
|
|
113
|
+
updateRaspRequestsMetricTags(metrics, store.req, raspRule)
|
|
114
114
|
} else {
|
|
115
115
|
updateWafRequestsMetricTags(metrics, store.req)
|
|
116
116
|
}
|
|
@@ -79,7 +79,7 @@ function getOrCreateMetricTags (store, versionsTags) {
|
|
|
79
79
|
return metricTags
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function updateRaspRequestsMetricTags (metrics, req,
|
|
82
|
+
function updateRaspRequestsMetricTags (metrics, req, raspRule) {
|
|
83
83
|
if (!req) return
|
|
84
84
|
|
|
85
85
|
const store = getStore(req)
|
|
@@ -89,7 +89,12 @@ function updateRaspRequestsMetricTags (metrics, req, raspRuleType) {
|
|
|
89
89
|
|
|
90
90
|
if (!enabled) return
|
|
91
91
|
|
|
92
|
-
const tags = { rule_type:
|
|
92
|
+
const tags = { rule_type: raspRule.type, waf_version: metrics.wafVersion }
|
|
93
|
+
|
|
94
|
+
if (raspRule.variant) {
|
|
95
|
+
tags.rule_variant = raspRule.variant
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
appsecMetrics.count('rasp.rule.eval', tags).inc(1)
|
|
94
99
|
|
|
95
100
|
if (metrics.wafTimeout) {
|
|
@@ -46,7 +46,7 @@ function update (newRules) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function run (data, req,
|
|
49
|
+
function run (data, req, raspRule) {
|
|
50
50
|
if (!req) {
|
|
51
51
|
const store = storage.getStore()
|
|
52
52
|
if (!store || !store.req) {
|
|
@@ -59,7 +59,7 @@ function run (data, req, raspRuleType) {
|
|
|
59
59
|
|
|
60
60
|
const wafContext = waf.wafManager.getWAFContext(req)
|
|
61
61
|
|
|
62
|
-
return wafContext.run(data,
|
|
62
|
+
return wafContext.run(data, raspRule)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function disposeContext (req) {
|
|
@@ -21,7 +21,7 @@ class WAFContextWrapper {
|
|
|
21
21
|
this.knownAddresses = knownAddresses
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
run ({ persistent, ephemeral },
|
|
24
|
+
run ({ persistent, ephemeral }, raspRule) {
|
|
25
25
|
if (this.ddwafContext.disposed) {
|
|
26
26
|
log.warn('[ASM] Calling run on a disposed context')
|
|
27
27
|
return
|
|
@@ -87,7 +87,7 @@ class WAFContextWrapper {
|
|
|
87
87
|
blockTriggered,
|
|
88
88
|
wafVersion: this.wafVersion,
|
|
89
89
|
wafTimeout: result.timeout
|
|
90
|
-
},
|
|
90
|
+
}, raspRule)
|
|
91
91
|
|
|
92
92
|
if (ruleTriggered) {
|
|
93
93
|
Reporter.reportAttack(JSON.stringify(result.events))
|
|
@@ -9,7 +9,8 @@ const config = module.exports = {
|
|
|
9
9
|
service: parentConfig.service,
|
|
10
10
|
commitSHA: parentConfig.commitSHA,
|
|
11
11
|
repositoryUrl: parentConfig.repositoryUrl,
|
|
12
|
-
parentThreadId
|
|
12
|
+
parentThreadId,
|
|
13
|
+
maxTotalPayloadSize: 5 * 1024 * 1024 // 5MB
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
updateUrl(parentConfig)
|
|
@@ -129,9 +129,8 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// TODO: Process template (DEBUG-2628)
|
|
132
|
-
send(probe.template, logger, dd, snapshot, (
|
|
133
|
-
|
|
134
|
-
else ackEmitting(probe)
|
|
132
|
+
send(probe.template, logger, dd, snapshot, () => {
|
|
133
|
+
ackEmitting(probe)
|
|
135
134
|
})
|
|
136
135
|
}
|
|
137
136
|
})
|
|
@@ -141,6 +140,8 @@ function highestOrUndefined (num, max) {
|
|
|
141
140
|
}
|
|
142
141
|
|
|
143
142
|
async function getDD (callFrameId) {
|
|
143
|
+
// TODO: Consider if an `objectGroup` should be used, so it can be explicitly released using
|
|
144
|
+
// `Runtime.releaseObjectGroup`
|
|
144
145
|
const { result } = await session.post('Debugger.evaluateOnCallFrame', {
|
|
145
146
|
callFrameId,
|
|
146
147
|
expression,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
class JSONBuffer {
|
|
4
|
+
constructor ({ size, timeout, onFlush }) {
|
|
5
|
+
this._maxSize = size
|
|
6
|
+
this._timeout = timeout
|
|
7
|
+
this._onFlush = onFlush
|
|
8
|
+
this._reset()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_reset () {
|
|
12
|
+
clearTimeout(this._timer)
|
|
13
|
+
this._timer = null
|
|
14
|
+
this._partialJson = null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_flush () {
|
|
18
|
+
const json = `${this._partialJson}]`
|
|
19
|
+
this._reset()
|
|
20
|
+
this._onFlush(json)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
write (str, size = Buffer.byteLength(str)) {
|
|
24
|
+
if (this._timer === null) {
|
|
25
|
+
this._partialJson = `[${str}`
|
|
26
|
+
this._timer = setTimeout(() => this._flush(), this._timeout)
|
|
27
|
+
} else if (Buffer.byteLength(this._partialJson) + size + 2 > this._maxSize) {
|
|
28
|
+
this._flush()
|
|
29
|
+
this.write(str, size)
|
|
30
|
+
} else {
|
|
31
|
+
this._partialJson += `,${str}`
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = JSONBuffer
|
|
@@ -4,32 +4,35 @@ const { hostname: getHostname } = require('os')
|
|
|
4
4
|
const { stringify } = require('querystring')
|
|
5
5
|
|
|
6
6
|
const config = require('./config')
|
|
7
|
+
const JSONBuffer = require('./json-buffer')
|
|
7
8
|
const request = require('../../exporters/common/request')
|
|
8
9
|
const { GIT_COMMIT_SHA, GIT_REPOSITORY_URL } = require('../../plugins/util/tags')
|
|
10
|
+
const log = require('../../log')
|
|
11
|
+
const { version } = require('../../../../../package.json')
|
|
9
12
|
|
|
10
13
|
module.exports = send
|
|
11
14
|
|
|
12
|
-
const
|
|
15
|
+
const MAX_LOG_PAYLOAD_SIZE = 1024 * 1024 // 1MB
|
|
13
16
|
|
|
14
17
|
const ddsource = 'dd_debugger'
|
|
15
18
|
const hostname = getHostname()
|
|
16
19
|
const service = config.service
|
|
17
20
|
|
|
18
21
|
const ddtags = [
|
|
22
|
+
['env', process.env.DD_ENV],
|
|
23
|
+
['version', process.env.DD_VERSION],
|
|
24
|
+
['debugger_version', version],
|
|
25
|
+
['host_name', hostname],
|
|
19
26
|
[GIT_COMMIT_SHA, config.commitSHA],
|
|
20
27
|
[GIT_REPOSITORY_URL, config.repositoryUrl]
|
|
21
28
|
].map((pair) => pair.join(':')).join(',')
|
|
22
29
|
|
|
23
30
|
const path = `/debugger/v1/input?${stringify({ ddtags })}`
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
method: 'POST',
|
|
28
|
-
url: config.url,
|
|
29
|
-
path,
|
|
30
|
-
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
|
31
|
-
}
|
|
32
|
+
let callbacks = []
|
|
33
|
+
const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush })
|
|
32
34
|
|
|
35
|
+
function send (message, logger, dd, snapshot, cb) {
|
|
33
36
|
const payload = {
|
|
34
37
|
ddsource,
|
|
35
38
|
hostname,
|
|
@@ -41,8 +44,9 @@ function send (message, logger, dd, snapshot, cb) {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
let json = JSON.stringify(payload)
|
|
47
|
+
let size = Buffer.byteLength(json)
|
|
44
48
|
|
|
45
|
-
if (
|
|
49
|
+
if (size > MAX_LOG_PAYLOAD_SIZE) {
|
|
46
50
|
// TODO: This is a very crude way to handle large payloads. Proper pruning will be implemented later (DEBUG-2624)
|
|
47
51
|
const line = Object.values(payload['debugger.snapshot'].captures.lines)[0]
|
|
48
52
|
line.locals = {
|
|
@@ -50,7 +54,26 @@ function send (message, logger, dd, snapshot, cb) {
|
|
|
50
54
|
size: Object.keys(line.locals).length
|
|
51
55
|
}
|
|
52
56
|
json = JSON.stringify(payload)
|
|
57
|
+
size = Buffer.byteLength(json)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
jsonBuffer.write(json, size)
|
|
61
|
+
callbacks.push(cb)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onFlush (payload) {
|
|
65
|
+
const opts = {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
url: config.url,
|
|
68
|
+
path,
|
|
69
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
const _callbacks = callbacks
|
|
73
|
+
callbacks = []
|
|
74
|
+
|
|
75
|
+
request(payload, opts, (err) => {
|
|
76
|
+
if (err) log.error('Could not send debugger payload', err)
|
|
77
|
+
else _callbacks.forEach(cb => cb())
|
|
78
|
+
})
|
|
56
79
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const LRUCache = require('lru-cache')
|
|
4
4
|
const config = require('./config')
|
|
5
|
+
const JSONBuffer = require('./json-buffer')
|
|
5
6
|
const request = require('../../exporters/common/request')
|
|
6
7
|
const FormData = require('../../exporters/common/form-data')
|
|
7
8
|
const log = require('../../log')
|
|
@@ -25,6 +26,8 @@ const cache = new LRUCache({
|
|
|
25
26
|
ttlAutopurge: true
|
|
26
27
|
})
|
|
27
28
|
|
|
29
|
+
const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush })
|
|
30
|
+
|
|
28
31
|
const STATUSES = {
|
|
29
32
|
RECEIVED: 'RECEIVED',
|
|
30
33
|
INSTALLED: 'INSTALLED',
|
|
@@ -71,11 +74,15 @@ function ackError (err, { id: probeId, version }) {
|
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
function send (payload) {
|
|
77
|
+
jsonBuffer.write(JSON.stringify(payload))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onFlush (payload) {
|
|
74
81
|
const form = new FormData()
|
|
75
82
|
|
|
76
83
|
form.append(
|
|
77
84
|
'event',
|
|
78
|
-
|
|
85
|
+
payload,
|
|
79
86
|
{ filename: 'event.json', contentType: 'application/json; charset=utf-8' }
|
|
80
87
|
)
|
|
81
88
|
|
|
@@ -87,7 +94,7 @@ function send (payload) {
|
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
request(form, options, (err) => {
|
|
90
|
-
if (err) log.error('[debugger:devtools_client] Error sending
|
|
97
|
+
if (err) log.error('[debugger:devtools_client] Error sending probe payload', err)
|
|
91
98
|
})
|
|
92
99
|
}
|
|
93
100
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { SPAN_KIND, OUTPUT_VALUE } = require('./constants/tags')
|
|
3
|
+
const { SPAN_KIND, OUTPUT_VALUE, INPUT_VALUE } = require('./constants/tags')
|
|
4
4
|
|
|
5
5
|
const {
|
|
6
6
|
getFunctionArguments,
|
|
7
7
|
validateKind
|
|
8
8
|
} = require('./util')
|
|
9
|
-
const { isTrue } = require('../util')
|
|
9
|
+
const { isTrue, isError } = require('../util')
|
|
10
10
|
|
|
11
11
|
const { storage } = require('./storage')
|
|
12
12
|
|
|
@@ -134,29 +134,63 @@ class LLMObs extends NoopLLMObs {
|
|
|
134
134
|
|
|
135
135
|
function wrapped () {
|
|
136
136
|
const span = llmobs._tracer.scope().active()
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
const fnArgs = arguments
|
|
138
|
+
|
|
139
|
+
const lastArgId = fnArgs.length - 1
|
|
140
|
+
const cb = fnArgs[lastArgId]
|
|
141
|
+
const hasCallback = typeof cb === 'function'
|
|
142
|
+
|
|
143
|
+
if (hasCallback) {
|
|
144
|
+
const scopeBoundCb = llmobs._bind(cb)
|
|
145
|
+
fnArgs[lastArgId] = function () {
|
|
146
|
+
// it is standard practice to follow the callback signature (err, result)
|
|
147
|
+
// however, we try to parse the arguments to determine if the first argument is an error
|
|
148
|
+
// if it is not, and is not undefined, we will use that for the output value
|
|
149
|
+
const maybeError = arguments[0]
|
|
150
|
+
const maybeResult = arguments[1]
|
|
151
|
+
|
|
152
|
+
llmobs._autoAnnotate(
|
|
153
|
+
span,
|
|
154
|
+
kind,
|
|
155
|
+
getFunctionArguments(fn, fnArgs),
|
|
156
|
+
isError(maybeError) || maybeError == null ? maybeResult : maybeError
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return scopeBoundCb.apply(this, arguments)
|
|
141
160
|
}
|
|
161
|
+
}
|
|
142
162
|
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
try {
|
|
164
|
+
const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => fn.apply(this, fnArgs))
|
|
165
|
+
|
|
166
|
+
if (result && typeof result.then === 'function') {
|
|
167
|
+
return result.then(
|
|
168
|
+
value => {
|
|
169
|
+
if (!hasCallback) {
|
|
170
|
+
llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), value)
|
|
171
|
+
}
|
|
172
|
+
return value
|
|
173
|
+
},
|
|
174
|
+
err => {
|
|
175
|
+
llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
|
|
176
|
+
throw err
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
}
|
|
145
180
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
}
|
|
181
|
+
// it is possible to return a value and have a callback
|
|
182
|
+
// however, since the span finishes when the callback is called, it is possible that
|
|
183
|
+
// the callback is called before the function returns (although unlikely)
|
|
184
|
+
// we do not want to throw for "annotating a finished span" in this case
|
|
185
|
+
if (!hasCallback) {
|
|
186
|
+
llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), result)
|
|
187
|
+
}
|
|
154
188
|
|
|
155
|
-
|
|
156
|
-
|
|
189
|
+
return result
|
|
190
|
+
} catch (e) {
|
|
191
|
+
llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs))
|
|
192
|
+
throw e
|
|
157
193
|
}
|
|
158
|
-
|
|
159
|
-
return result
|
|
160
194
|
}
|
|
161
195
|
|
|
162
196
|
return this._tracer.wrap(name, spanOptions, wrapped)
|
|
@@ -333,20 +367,34 @@ class LLMObs extends NoopLLMObs {
|
|
|
333
367
|
flushCh.publish()
|
|
334
368
|
}
|
|
335
369
|
|
|
370
|
+
_autoAnnotate (span, kind, input, output) {
|
|
371
|
+
const annotations = {}
|
|
372
|
+
if (input && !['llm', 'embedding'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[INPUT_VALUE]) {
|
|
373
|
+
annotations.inputData = input
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (output && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
|
|
377
|
+
annotations.outputData = output
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.annotate(span, annotations)
|
|
381
|
+
}
|
|
382
|
+
|
|
336
383
|
_active () {
|
|
337
384
|
const store = storage.getStore()
|
|
338
385
|
return store?.span
|
|
339
386
|
}
|
|
340
387
|
|
|
341
|
-
_activate (span,
|
|
388
|
+
_activate (span, options, fn) {
|
|
342
389
|
const parent = this._active()
|
|
343
390
|
if (this.enabled) storage.enterWith({ span })
|
|
344
391
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
392
|
+
if (options) {
|
|
393
|
+
this._tagger.registerLLMObsSpan(span, {
|
|
394
|
+
...options,
|
|
395
|
+
parent
|
|
396
|
+
})
|
|
397
|
+
}
|
|
350
398
|
|
|
351
399
|
try {
|
|
352
400
|
return fn()
|
|
@@ -355,6 +403,22 @@ class LLMObs extends NoopLLMObs {
|
|
|
355
403
|
}
|
|
356
404
|
}
|
|
357
405
|
|
|
406
|
+
// bind function to active LLMObs span
|
|
407
|
+
_bind (fn) {
|
|
408
|
+
if (typeof fn !== 'function') return fn
|
|
409
|
+
|
|
410
|
+
const llmobs = this
|
|
411
|
+
const activeSpan = llmobs._active()
|
|
412
|
+
|
|
413
|
+
const bound = function () {
|
|
414
|
+
return llmobs._activate(activeSpan, null, () => {
|
|
415
|
+
return fn.apply(this, arguments)
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return bound
|
|
420
|
+
}
|
|
421
|
+
|
|
358
422
|
_extractOptions (options) {
|
|
359
423
|
const {
|
|
360
424
|
modelName,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const coalesce = require('koalas')
|
|
4
|
+
const { inspect } = require('util')
|
|
4
5
|
const { isTrue } = require('../util')
|
|
5
6
|
const { traceChannel, debugChannel, infoChannel, warnChannel, errorChannel } = require('./channels')
|
|
6
7
|
const logWriter = require('./writer')
|
|
@@ -59,9 +60,17 @@ const log = {
|
|
|
59
60
|
trace (...args) {
|
|
60
61
|
if (traceChannel.hasSubscribers) {
|
|
61
62
|
const logRecord = {}
|
|
63
|
+
|
|
62
64
|
Error.captureStackTrace(logRecord, this.trace)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
|
|
66
|
+
const stack = logRecord.stack.split('\n')
|
|
67
|
+
const fn = stack[1].replace(/^\s+at ([^\s]+) .+/, '$1')
|
|
68
|
+
const options = { depth: 2, breakLength: Infinity, compact: true, maxArrayLength: Infinity }
|
|
69
|
+
const params = args.map(a => inspect(a, options)).join(', ')
|
|
70
|
+
|
|
71
|
+
stack[0] = `Trace: ${fn}(${params})`
|
|
72
|
+
|
|
73
|
+
traceChannel.publish(Log.parse(stack.join('\n')))
|
|
65
74
|
}
|
|
66
75
|
return this
|
|
67
76
|
},
|
|
@@ -4,7 +4,6 @@ const { storage } = require('../../../datadog-core')
|
|
|
4
4
|
const { LogChannel } = require('./channels')
|
|
5
5
|
const { Log } = require('./log')
|
|
6
6
|
const defaultLogger = {
|
|
7
|
-
trace: msg => console.trace(msg), /* eslint-disable-line no-console */
|
|
8
7
|
debug: msg => console.debug(msg), /* eslint-disable-line no-console */
|
|
9
8
|
info: msg => console.info(msg), /* eslint-disable-line no-console */
|
|
10
9
|
warn: msg => console.warn(msg), /* eslint-disable-line no-console */
|
|
@@ -91,8 +90,10 @@ function onDebug (log) {
|
|
|
91
90
|
|
|
92
91
|
function onTrace (log) {
|
|
93
92
|
const { formatted, cause } = getErrorLog(log)
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
// Using logger.debug() because not all loggers have trace level,
|
|
94
|
+
// and console.trace() has a completely different meaning.
|
|
95
|
+
if (formatted) withNoop(() => logger.debug(formatted))
|
|
96
|
+
if (cause) withNoop(() => logger.debug(cause))
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
function error (...args) {
|
|
@@ -10,7 +10,7 @@ const noopAppsec = new NoopAppsecSdk()
|
|
|
10
10
|
const noopDogStatsDClient = new NoopDogStatsDClient()
|
|
11
11
|
const noopLLMObs = new NoopLLMObsSDK(noop)
|
|
12
12
|
|
|
13
|
-
class
|
|
13
|
+
class NoopProxy {
|
|
14
14
|
constructor () {
|
|
15
15
|
this._tracer = noop
|
|
16
16
|
this.appsec = noopAppsec
|
|
@@ -91,4 +91,4 @@ class Tracer {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
module.exports =
|
|
94
|
+
module.exports = NoopProxy
|
|
@@ -6,7 +6,7 @@ const { storage } = require('../../../datadog-core') // TODO: noop storage?
|
|
|
6
6
|
|
|
7
7
|
class NoopSpan {
|
|
8
8
|
constructor (tracer, parent) {
|
|
9
|
-
this._store = storage.
|
|
9
|
+
this._store = storage.getHandle()
|
|
10
10
|
this._noopTracer = tracer
|
|
11
11
|
this._noopContext = this._createContext(parent)
|
|
12
12
|
}
|
|
@@ -14,6 +14,7 @@ const { storage } = require('../../../datadog-core')
|
|
|
14
14
|
const telemetryMetrics = require('../telemetry/metrics')
|
|
15
15
|
const { channel } = require('dc-polyfill')
|
|
16
16
|
const spanleak = require('../spanleak')
|
|
17
|
+
const util = require('util')
|
|
17
18
|
|
|
18
19
|
const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
|
|
19
20
|
|
|
@@ -64,7 +65,7 @@ class DatadogSpan {
|
|
|
64
65
|
this._debug = debug
|
|
65
66
|
this._processor = processor
|
|
66
67
|
this._prioritySampler = prioritySampler
|
|
67
|
-
this._store = storage.
|
|
68
|
+
this._store = storage.getHandle()
|
|
68
69
|
this._duration = undefined
|
|
69
70
|
|
|
70
71
|
this._events = []
|
|
@@ -105,9 +106,18 @@ class DatadogSpan {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
[util.inspect.custom] () {
|
|
110
|
+
return {
|
|
111
|
+
...this,
|
|
112
|
+
_parentTracer: `[${this._parentTracer.constructor.name}]`,
|
|
113
|
+
_prioritySampler: `[${this._prioritySampler.constructor.name}]`,
|
|
114
|
+
_processor: `[${this._processor.constructor.name}]`
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
toString () {
|
|
109
119
|
const spanContext = this.context()
|
|
110
|
-
const resourceName = spanContext._tags['resource.name']
|
|
120
|
+
const resourceName = spanContext._tags['resource.name'] || ''
|
|
111
121
|
const resource = resourceName.length > 100
|
|
112
122
|
? `${resourceName.substring(0, 97)}...`
|
|
113
123
|
: resourceName
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const util = require('util')
|
|
3
4
|
const { AUTO_KEEP } = require('../../../../ext/priority')
|
|
4
5
|
|
|
5
6
|
// the lowercase, hex encoded upper 64 bits of a 128-bit trace id, if present
|
|
@@ -31,6 +32,17 @@ class DatadogSpanContext {
|
|
|
31
32
|
this._otelSpanContext = undefined
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
[util.inspect.custom] () {
|
|
36
|
+
return {
|
|
37
|
+
...this,
|
|
38
|
+
_trace: {
|
|
39
|
+
...this._trace,
|
|
40
|
+
started: '[Array]',
|
|
41
|
+
finished: '[Array]'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
toTraceId (get128bitId = false) {
|
|
35
47
|
if (get128bitId) {
|
|
36
48
|
return this._traceId.toBuffer().length <= 8 && this._trace.tags[TRACE_ID_128]
|
|
@@ -120,13 +120,15 @@ class PrioritySampler {
|
|
|
120
120
|
if (!span || !this.validate(samplingPriority)) return
|
|
121
121
|
|
|
122
122
|
const context = this._getContext(span)
|
|
123
|
+
const root = context._trace.started[0]
|
|
124
|
+
|
|
125
|
+
if (!root) return // noop span
|
|
123
126
|
|
|
124
127
|
context._sampling.priority = samplingPriority
|
|
125
128
|
context._sampling.mechanism = mechanism
|
|
126
129
|
|
|
127
|
-
const root = context._trace.started[0]
|
|
128
|
-
|
|
129
130
|
log.trace(span, samplingPriority, mechanism)
|
|
131
|
+
|
|
130
132
|
this._addDecisionMaker(root)
|
|
131
133
|
}
|
|
132
134
|
|
|
@@ -32,11 +32,11 @@ class EventPlugin extends TracingPlugin {
|
|
|
32
32
|
if (!store) return
|
|
33
33
|
|
|
34
34
|
const { startEvent, startTime, error } = store
|
|
35
|
-
if (error) {
|
|
36
|
-
return // don't emit perf events for failed operations
|
|
35
|
+
if (error || this.ignoreEvent(startEvent)) {
|
|
36
|
+
return // don't emit perf events for failed operations or ignored events
|
|
37
37
|
}
|
|
38
|
-
const duration = performance.now() - startTime
|
|
39
38
|
|
|
39
|
+
const duration = performance.now() - startTime
|
|
40
40
|
const event = {
|
|
41
41
|
entryType: this.entryType,
|
|
42
42
|
startTime,
|
|
@@ -53,6 +53,10 @@ class EventPlugin extends TracingPlugin {
|
|
|
53
53
|
|
|
54
54
|
this.eventHandler(this.extendEvent(event, startEvent))
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
ignoreEvent () {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
module.exports = EventPlugin
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const EventPlugin = require('./event')
|
|
2
|
+
|
|
3
|
+
// Values taken from parameter names in datadog-instrumentations/src/fs.js.
|
|
4
|
+
// Known param names that are disallowed because they can be strings and have arbitrary sizes:
|
|
5
|
+
// 'data'
|
|
6
|
+
// Known param names that are disallowed because they are never a string or number:
|
|
7
|
+
// 'buffer', 'buffers', 'listener'
|
|
8
|
+
const allowedParams = new Set([
|
|
9
|
+
'atime', 'dest',
|
|
10
|
+
'existingPath', 'fd', 'file',
|
|
11
|
+
'flag', 'gid', 'len',
|
|
12
|
+
'length', 'mode', 'mtime',
|
|
13
|
+
'newPath', 'offset', 'oldPath',
|
|
14
|
+
'operation', 'options', 'path',
|
|
15
|
+
'position', 'prefix', 'src',
|
|
16
|
+
'target', 'type', 'uid'
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
class FilesystemPlugin extends EventPlugin {
|
|
20
|
+
static get id () {
|
|
21
|
+
return 'fs'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static get operation () {
|
|
25
|
+
return 'operation'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static get entryType () {
|
|
29
|
+
return 'fs'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ignoreEvent (event) {
|
|
33
|
+
// Don't care about sync events, they show up in the event loop samples anyway
|
|
34
|
+
return event.operation?.endsWith('Sync')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
extendEvent (event, detail) {
|
|
38
|
+
const d = { ...detail }
|
|
39
|
+
Object.entries(d).forEach(([k, v]) => {
|
|
40
|
+
if (!(allowedParams.has(k) && (typeof v === 'string' || typeof v === 'number'))) {
|
|
41
|
+
delete d[k]
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
event.detail = d
|
|
45
|
+
|
|
46
|
+
return event
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
module.exports = FilesystemPlugin
|
|
@@ -133,11 +133,32 @@ class NetDecorator {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
class FilesystemDecorator {
|
|
137
|
+
constructor (stringTable) {
|
|
138
|
+
this.stringTable = stringTable
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
decorateSample (sampleInput, item) {
|
|
142
|
+
const labels = sampleInput.label
|
|
143
|
+
const stringTable = this.stringTable
|
|
144
|
+
Object.entries(item.detail).forEach(([k, v]) => {
|
|
145
|
+
switch (typeof v) {
|
|
146
|
+
case 'string':
|
|
147
|
+
labels.push(labelFromStrStr(stringTable, k, v))
|
|
148
|
+
break
|
|
149
|
+
case 'number':
|
|
150
|
+
labels.push(new Label({ key: stringTable.dedup(k), num: v }))
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
136
156
|
// Keys correspond to PerformanceEntry.entryType, values are constructor
|
|
137
157
|
// functions for type-specific decorators.
|
|
138
158
|
const decoratorTypes = {
|
|
139
|
-
|
|
159
|
+
fs: FilesystemDecorator,
|
|
140
160
|
dns: DNSDecorator,
|
|
161
|
+
gc: GCDecorator,
|
|
141
162
|
net: NetDecorator
|
|
142
163
|
}
|
|
143
164
|
|
|
@@ -255,7 +276,7 @@ class NodeApiEventSource {
|
|
|
255
276
|
|
|
256
277
|
class DatadogInstrumentationEventSource {
|
|
257
278
|
constructor (eventHandler, eventFilter) {
|
|
258
|
-
this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'net'].map(m => {
|
|
279
|
+
this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'fs', 'net'].map(m => {
|
|
259
280
|
const Plugin = require(`./event_plugins/${m}`)
|
|
260
281
|
return new Plugin(eventHandler, eventFilter)
|
|
261
282
|
})
|
|
@@ -17,7 +17,7 @@ class Scope {
|
|
|
17
17
|
if (typeof callback !== 'function') return callback
|
|
18
18
|
|
|
19
19
|
const oldStore = storage.getStore()
|
|
20
|
-
const newStore = span ? span._store : oldStore
|
|
20
|
+
const newStore = span ? storage.getStore(span._store) : oldStore
|
|
21
21
|
|
|
22
22
|
storage.enterWith({ ...newStore, span })
|
|
23
23
|
|