dd-trace 5.29.1 → 5.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +3 -3
  2. package/packages/datadog-core/src/storage.js +11 -2
  3. package/packages/datadog-instrumentations/src/aerospike.js +1 -1
  4. package/packages/datadog-instrumentations/src/fs.js +3 -0
  5. package/packages/dd-trace/src/appsec/addresses.js +1 -0
  6. package/packages/dd-trace/src/appsec/rasp/command_injection.js +13 -6
  7. package/packages/dd-trace/src/appsec/rasp/lfi.js +3 -1
  8. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +3 -1
  9. package/packages/dd-trace/src/appsec/rasp/ssrf.js +3 -1
  10. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
  11. package/packages/dd-trace/src/appsec/remote_config/index.js +2 -0
  12. package/packages/dd-trace/src/appsec/remote_config/manager.js +11 -1
  13. package/packages/dd-trace/src/appsec/reporter.js +3 -3
  14. package/packages/dd-trace/src/appsec/telemetry.js +7 -2
  15. package/packages/dd-trace/src/appsec/waf/index.js +2 -2
  16. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +2 -2
  17. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
  18. package/packages/dd-trace/src/debugger/devtools_client/index.js +4 -3
  19. package/packages/dd-trace/src/debugger/devtools_client/json-buffer.js +36 -0
  20. package/packages/dd-trace/src/debugger/devtools_client/send.js +33 -10
  21. package/packages/dd-trace/src/debugger/devtools_client/status.js +9 -2
  22. package/packages/dd-trace/src/llmobs/sdk.js +90 -26
  23. package/packages/dd-trace/src/log/index.js +11 -2
  24. package/packages/dd-trace/src/log/writer.js +4 -3
  25. package/packages/dd-trace/src/noop/proxy.js +2 -2
  26. package/packages/dd-trace/src/noop/span.js +1 -1
  27. package/packages/dd-trace/src/opentracing/span.js +12 -2
  28. package/packages/dd-trace/src/opentracing/span_context.js +12 -0
  29. package/packages/dd-trace/src/priority_sampler.js +4 -2
  30. package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +7 -3
  31. package/packages/dd-trace/src/profiling/profilers/event_plugins/fs.js +49 -0
  32. package/packages/dd-trace/src/profiling/profilers/events.js +23 -2
  33. package/packages/dd-trace/src/scope.js +1 -1
  34. 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": "5.29.1",
3
+ "version": "5.31.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.3.0",
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": "^9",
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
- getStore () {
25
- const handle = this._storage.getStore()
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
 
@@ -40,7 +40,7 @@ function wrapProcess (process) {
40
40
  addHook({
41
41
  name: 'aerospike',
42
42
  file: 'lib/commands/command.js',
43
- versions: ['4', '5']
43
+ versions: ['4', '5', '6']
44
44
  },
45
45
  commandFactory => {
46
46
  return shimmer.wrapFunction(commandFactory, f => wrapCreateCommand(f))
@@ -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'],
@@ -31,6 +31,7 @@ module.exports = {
31
31
  DB_STATEMENT: 'server.db.statement',
32
32
  DB_SYSTEM: 'server.db.system',
33
33
 
34
+ EXEC_COMMAND: 'server.sys.exec.cmd',
34
35
  SHELL_COMMAND: 'server.sys.shell.cmd',
35
36
 
36
37
  LOGIN_SUCCESS: 'server.business_logic.users.login.success',
@@ -25,19 +25,26 @@ function disable () {
25
25
  }
26
26
 
27
27
  function analyzeCommandInjection ({ file, fileArgs, shell, abortController }) {
28
- if (!file || !shell) return
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 commandParams = fileArgs ? [file, ...fileArgs] : file
35
-
36
- const persistent = {
37
- [addresses.SHELL_COMMAND]: commandParams
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, RULE_TYPES.COMMAND_INJECTION)
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 result = waf.run({ persistent }, req, RULE_TYPES.LFI)
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 result = waf.run({ persistent }, req, RULE_TYPES.SQL_INJECTION)
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 result = waf.run({ persistent }, req, RULE_TYPES.SSRF)
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)
@@ -25,5 +25,6 @@ module.exports = {
25
25
  ASM_AUTO_USER_INSTRUM_MODE: 1n << 31n,
26
26
  ASM_ENDPOINT_FINGERPRINT: 1n << 32n,
27
27
  ASM_NETWORK_FINGERPRINT: 1n << 34n,
28
- ASM_HEADER_FINGERPRINT: 1n << 35n
28
+ ASM_HEADER_FINGERPRINT: 1n << 35n,
29
+ ASM_RASP_CMDI: 1n << 37n
29
30
  }
@@ -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, raspRuleType) {
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 (raspRuleType) {
113
- updateRaspRequestsMetricTags(metrics, store.req, raspRuleType)
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, raspRuleType) {
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: raspRuleType, waf_version: metrics.wafVersion }
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, raspRuleType) {
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, raspRuleType)
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 }, raspRuleType) {
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
- }, raspRuleType)
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, (err) => {
133
- if (err) log.error('Debugger error', err)
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 MAX_PAYLOAD_SIZE = 1024 * 1024 // 1MB
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
- function send (message, logger, dd, snapshot, cb) {
26
- const opts = {
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 (Buffer.byteLength(json) > MAX_PAYLOAD_SIZE) {
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
- request(json, opts, cb)
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
- JSON.stringify(payload),
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 debugger payload', err)
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
- const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => {
139
- if (!['llm', 'embedding'].includes(kind)) {
140
- llmobs.annotate(span, { inputData: getFunctionArguments(fn, arguments) })
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
- return fn.apply(this, arguments)
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
- if (result && typeof result.then === 'function') {
147
- return result.then(value => {
148
- if (value && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
149
- llmobs.annotate(span, { outputData: value })
150
- }
151
- return value
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
- if (result && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) {
156
- llmobs.annotate(span, { outputData: result })
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, { kind, options } = {}, fn) {
388
+ _activate (span, options, fn) {
342
389
  const parent = this._active()
343
390
  if (this.enabled) storage.enterWith({ span })
344
391
 
345
- this._tagger.registerLLMObsSpan(span, {
346
- ...options,
347
- parent,
348
- kind
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
- const stack = logRecord.stack.split('\n')[1].replace(/^\s+at ([^\s]) .+/, '$1')
64
- traceChannel.publish(Log.parse('Trace', args, { stack }))
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
- if (formatted) withNoop(() => logger.trace(formatted))
95
- if (cause) withNoop(() => logger.trace(cause))
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 Tracer {
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 = Tracer
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.getStore()
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.getStore()
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
- gc: GCDecorator,
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
 
@@ -307,6 +307,8 @@ function updateConfig (changes, config) {
307
307
  if (!config.telemetry.enabled) return
308
308
  if (changes.length === 0) return
309
309
 
310
+ logger.trace(changes)
311
+
310
312
  const application = createAppObject(config)
311
313
  const host = createHostObject()
312
314