Skip to content

Commit e92e7f0

Browse files
fix: improved instrumentation code & related telemetry (#6910)
* chore: improve instrumentation hooks This improves our instrumentations in multiple ways: - Add JSDoc to many methods that it is easier to work with. - Fix multiple type errors. - Remove code that was only used in testing such as `unhook()`. - Fix small issues in esbuild instrumentation where a module might not return a moduleExports object. - Improve code to run slightly faster (these are not hot code paths). - Automatically instruments unprefixed Node.js modules when defining a instrumentation for a module. This removes the need for much special handling (duplicating the instrumentation, using the symbol to detect already instrumented calls, etc.). - Fix using an array as file for some modules. This did not cause any issues, but it was not correct. - Removed unused isIitm arguments in instrumentations. - Removed unused ritm code. - Removed HOOK_SYMBOL code. * fix: always send telemetry for not instrumented code This makes sure not instrumented code will always send out the proper telemetry. Formerly, it would rely on any part of the file being instrumented. Now, we properly check by version again. To guarantee it is only logging it once, it is send the data on the first flush or before the service ends in case that happens earlier. It also fixes DD_TRACE_DISABLED_INSTRUMENTATIONS to work for prefixed and unprefixed Node automatically in case either is deactivated. Before, both would have to be deactivated. In addition, it simplifies code, adds some JSDoc and makes sure the Node.js version is added to Node.js modules in the telemetry / failures. This also fixes a couple of tests that were not properly checking the behavior as some state was captured in the instrumentation. --------- Co-authored-by: Pablo Erhard <pabloerhard02@gmail.com>
1 parent 91118b7 commit e92e7f0

38 files changed

Lines changed: 566 additions & 403 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@
231231
/integration-tests/bun/ @DataDog/lang-platform-js
232232
/integration-tests/init.spec.js @DataDog/lang-platform-js
233233
/integration-tests/package-guardrails.spec.js @DataDog/lang-platform-js
234+
/integration-tests/package-guardrails/flush.js @DataDog/lang-platform-js
234235
/integration-tests/startup.spec.js @DataDog/lang-platform-js
235236

236237
/packages/datadog-core @DataDog/lang-platform-js

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ export default [
269269
}],
270270
'import/no-useless-path-segments': 'error',
271271
'import/no-webpack-loader-syntax': 'error',
272+
'jsdoc/check-param-names': ['error', { disableMissingParamChecks: true }],
272273
'jsdoc/check-tag-names': ['error', { definedTags: ['datadog'] }],
273274
// TODO: Enable the rules that we want to use.
274275
// no-defaults: This should be activated, since the defaults will not be picked up in a description.

integration-tests/package-guardrails.spec.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const NODE_OPTIONS = '--require dd-trace/init.js'
1414
const DD_TRACE_DEBUG = 'true'
1515
const DD_INJECTION_ENABLED = 'tracing'
1616
const DD_LOG_LEVEL = 'info'
17+
const DD_TRACE_FLUSH_INTERVAL = '0'
1718
const NODE_MAJOR = Number(process.versions.node.split('.')[0])
1819
const FASTIFY_DEP = NODE_MAJOR < 20 ? 'fastify@4' : 'fastify'
1920

@@ -41,6 +42,17 @@ describe('package guardrails', () => {
4142
))
4243
})
4344

45+
context('when flushing and DD_INJECTION_ENABLED', () => {
46+
useEnv({ DD_INJECTION_ENABLED, DD_TRACE_FLUSH_INTERVAL })
47+
48+
it('should send abort.integration on first flush via diagnostic channel', () =>
49+
testFile('package-guardrails/flush.js', 'false\n',
50+
['complete', 'injection_forced:false',
51+
'abort.integration', 'integration:bluebird,integration_version:1.0.0',
52+
]
53+
))
54+
})
55+
4456
context('with logging disabled', () => {
4557
it('should not instrument the package', () => runTest('false\n', []))
4658
})
@@ -50,8 +62,9 @@ describe('package guardrails', () => {
5062

5163
it('should not instrument the package', () =>
5264
runTest(`Application instrumentation bootstrapping complete
53-
Found incompatible integration version: bluebird@1.0.0
5465
false
66+
instrumentation source: manual
67+
Found incompatible integration version: bluebird@1.0.0
5568
`, []))
5669
})
5770
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict'
2+
3+
// Remove only the register.js beforeExit handler so this test verifies
4+
// that abort.integration comes from the first flush diagnostic channel.
5+
const beforeExitHandlers = globalThis[Symbol.for('dd-trace')].beforeExitHandlers
6+
for (const handler of beforeExitHandlers) {
7+
if (handler.name === 'logAbortedIntegrations') {
8+
beforeExitHandlers.delete(handler)
9+
}
10+
}
11+
12+
const tracer = require('dd-trace')
13+
const P = require('bluebird')
14+
15+
const isWrapped = P.prototype._then.toString().includes('AsyncResource')
16+
tracer.trace('first.flush.guardrails', () => {})
17+
18+
// eslint-disable-next-line no-console
19+
console.log(isWrapped)

packages/datadog-esbuild/index.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
const { execSync } = require('node:child_process')
44
const fs = require('node:fs')
5-
const RAW_BUILTINS = require('node:module').builtinModules
65
const path = require('node:path')
76
const { pathToFileURL, fileURLToPath } = require('node:url')
87

@@ -25,23 +24,35 @@ for (const hook of Object.values(hooks)) {
2524
}
2625
}
2726

27+
function moduleOfInterestKey (name, file) {
28+
return file ? `${name}/${file}` : name
29+
}
30+
31+
const builtinModules = new Set(require('module').builtinModules)
32+
33+
function addModuleOfInterest (name, file) {
34+
if (!name) return
35+
36+
modulesOfInterest.add(moduleOfInterestKey(name, file))
37+
38+
if (builtinModules.has(name)) {
39+
modulesOfInterest.add(moduleOfInterestKey(`node:${name}`, file))
40+
}
41+
}
42+
2843
const modulesOfInterest = new Set()
2944

30-
for (const instrumentation of Object.values(instrumentations)) {
45+
for (const [name, instrumentation] of Object.entries(instrumentations)) {
3146
for (const entry of instrumentation) {
32-
if (entry.file) {
33-
modulesOfInterest.add(`${entry.name}/${entry.file}`) // e.g. "redis/my/file.js"
34-
} else {
35-
modulesOfInterest.add(entry.name) // e.g. "redis"
36-
}
47+
addModuleOfInterest(name, entry.file)
3748
}
3849
}
3950

4051
const CHANNEL = 'dd-trace:bundler:load'
4152

4253
const builtins = new Set()
4354

44-
for (const builtin of RAW_BUILTINS) {
55+
for (const builtin of builtinModules) {
4556
builtins.add(builtin)
4657
builtins.add(`node:${builtin}`)
4758
}
@@ -247,7 +258,7 @@ ${build.initialOptions.banner.js}`
247258
}
248259

249260
try {
250-
const packageJson = JSON.parse(fs.readFileSync(/** @type {string} */ (pathToPackageJson)).toString())
261+
const packageJson = JSON.parse(fs.readFileSync(/** @type {string} */(pathToPackageJson)).toString())
251262

252263
const isESM = isESMFile(fullPathToModule, pathToPackageJson, packageJson)
253264
if (isESM && !interceptedESMModules.has(fullPathToModule)) {

packages/datadog-instrumentations/src/child_process.js

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ const childProcessChannel = dc.tracingChannel('datadog:child_process:execution')
1414
// ignored exec method because it calls to execFile directly
1515
const execAsyncMethods = ['execFile', 'spawn', 'fork']
1616

17-
const names = ['child_process', 'node:child_process']
18-
19-
// child_process and node:child_process returns the same object instance, we only want to add hooks once
20-
let patched = false
21-
2217
function throwSyncError (error) {
2318
throw error
2419
}
@@ -37,19 +32,14 @@ function returnSpawnSyncError (error, context) {
3732
return context.result
3833
}
3934

40-
for (const name of names) {
41-
addHook({ name }, childProcess => {
42-
if (!patched) {
43-
patched = true
44-
shimmer.massWrap(childProcess, execAsyncMethods, wrapChildProcessAsyncMethod(childProcess.ChildProcess))
45-
shimmer.wrap(childProcess, 'execSync', wrapChildProcessSyncMethod(throwSyncError, true))
46-
shimmer.wrap(childProcess, 'execFileSync', wrapChildProcessSyncMethod(throwSyncError))
47-
shimmer.wrap(childProcess, 'spawnSync', wrapChildProcessSyncMethod(returnSpawnSyncError))
48-
}
35+
addHook({ name: 'child_process' }, childProcess => {
36+
shimmer.massWrap(childProcess, execAsyncMethods, wrapChildProcessAsyncMethod(childProcess.ChildProcess))
37+
shimmer.wrap(childProcess, 'execSync', wrapChildProcessSyncMethod(throwSyncError, true))
38+
shimmer.wrap(childProcess, 'execFileSync', wrapChildProcessSyncMethod(throwSyncError))
39+
shimmer.wrap(childProcess, 'spawnSync', wrapChildProcessSyncMethod(returnSpawnSyncError))
4940

50-
return childProcess
51-
})
52-
}
41+
return childProcess
42+
})
5343

5444
function normalizeArgs (args, shell) {
5545
const childProcessInfo = {

packages/datadog-instrumentations/src/crypto.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ const cryptoCipherCh = channel('datadog:crypto:cipher:start')
1111

1212
const hashMethods = ['createHash', 'createHmac', 'createSign', 'createVerify', 'sign', 'verify']
1313
const cipherMethods = ['createCipheriv', 'createDecipheriv']
14-
const names = ['crypto', 'node:crypto']
1514

16-
addHook({ name: names }, crypto => {
15+
addHook({ name: 'crypto' }, crypto => {
1716
shimmer.massWrap(crypto, hashMethods, wrapCryptoMethod(cryptoHashCh))
1817
shimmer.massWrap(crypto, cipherMethods, wrapCryptoMethod(cryptoCipherCh))
1918
return crypto

packages/datadog-instrumentations/src/dns.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ const rrtypes = {
1818
}
1919

2020
const rrtypeMap = new WeakMap()
21-
const names = ['dns', 'node:dns']
2221

23-
addHook({ name: names }, dns => {
22+
addHook({ name: 'dns' }, dns => {
2423
shimmer.wrap(dns, 'lookup', fn => wrap('apm:dns:lookup', fn, 2))
2524
shimmer.wrap(dns, 'lookupService', fn => wrap('apm:dns:lookup_service', fn, 2))
2625
shimmer.wrap(dns, 'resolve', fn => wrap('apm:dns:resolve', fn, 2))

packages/datadog-instrumentations/src/express.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ function wrapAppUse (use) {
146146
}
147147
}
148148

149-
addHook({ name: 'express', versions: ['>=4'], file: ['lib/express.js'] }, express => {
149+
addHook({ name: 'express', versions: ['>=4'], file: 'lib/express.js' }, express => {
150150
shimmer.wrap(express.application, 'handle', wrapHandle)
151151
shimmer.wrap(express.application, 'all', wrapAppAll)
152152
shimmer.wrap(express.application, 'route', wrapAppRoute)
@@ -224,19 +224,19 @@ function wrapProcessParamsMethod (requestPositionInArguments) {
224224
}
225225
}
226226

227-
addHook({ name: 'express', versions: ['>=4.0.0 <4.3.0'], file: ['lib/express.js'] }, express => {
227+
addHook({ name: 'express', versions: ['>=4.0.0 <4.3.0'], file: 'lib/express.js' }, express => {
228228
shimmer.wrap(express.Router, 'process_params', wrapProcessParamsMethod(1))
229229
return express
230230
})
231231

232-
addHook({ name: 'express', versions: ['>=4.3.0 <5.0.0'], file: ['lib/express.js'] }, express => {
232+
addHook({ name: 'express', versions: ['>=4.3.0 <5.0.0'], file: 'lib/express.js' }, express => {
233233
shimmer.wrap(express.Router, 'process_params', wrapProcessParamsMethod(2))
234234
return express
235235
})
236236

237237
const queryReadCh = channel('datadog:express:query:finish')
238238

239-
addHook({ name: 'express', file: ['lib/request.js'], versions: ['>=5.0.0'] }, request => {
239+
addHook({ name: 'express', file: 'lib/request.js', versions: ['>=5.0.0'] }, request => {
240240
shimmer.wrap(request, 'query', function (originalGet) {
241241
return function wrappedGet () {
242242
const query = originalGet.call(this)

packages/datadog-instrumentations/src/fs.js

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -84,37 +84,35 @@ const paramsByFileHandleMethods = {
8484
writeFile: ['data', 'options'],
8585
writev: ['buffers', 'position'],
8686
}
87-
const names = ['fs', 'node:fs']
88-
for (const name of names) {
89-
addHook({ name }, fs => {
90-
const asyncMethods = Object.keys(paramsByMethod)
91-
const syncMethods = asyncMethods.map(name => `${name}Sync`)
92-
93-
massWrap(fs, asyncMethods, createWrapFunction())
94-
massWrap(fs, syncMethods, createWrapFunction())
95-
massWrap(fs.promises, asyncMethods, createWrapFunction('promises.'))
96-
97-
wrap(fs.realpath, 'native', createWrapFunction('', 'realpath.native'))
98-
wrap(fs.realpathSync, 'native', createWrapFunction('', 'realpath.native'))
99-
wrap(fs.promises.realpath, 'native', createWrapFunction('', 'realpath.native'))
100-
101-
wrap(fs, 'createReadStream', wrapCreateStream)
102-
wrap(fs, 'createWriteStream', wrapCreateStream)
103-
if (fs.Dir) {
104-
wrap(fs.Dir.prototype, 'close', createWrapFunction('dir.'))
105-
wrap(fs.Dir.prototype, 'closeSync', createWrapFunction('dir.'))
106-
wrap(fs.Dir.prototype, 'read', createWrapFunction('dir.'))
107-
wrap(fs.Dir.prototype, 'readSync', createWrapFunction('dir.'))
108-
wrap(fs.Dir.prototype, Symbol.asyncIterator, createWrapDirAsyncIterator())
109-
}
87+
addHook({ name: 'fs' }, fs => {
88+
const asyncMethods = Object.keys(paramsByMethod)
89+
const syncMethods = asyncMethods.map(name => `${name}Sync`)
90+
91+
massWrap(fs, asyncMethods, createWrapFunction())
92+
massWrap(fs, syncMethods, createWrapFunction())
93+
massWrap(fs.promises, asyncMethods, createWrapFunction('promises.'))
94+
95+
wrap(fs.realpath, 'native', createWrapFunction('', 'realpath.native'))
96+
wrap(fs.realpathSync, 'native', createWrapFunction('', 'realpath.native'))
97+
wrap(fs.promises.realpath, 'native', createWrapFunction('', 'realpath.native'))
98+
99+
wrap(fs, 'createReadStream', wrapCreateStream)
100+
wrap(fs, 'createWriteStream', wrapCreateStream)
101+
if (fs.Dir) {
102+
wrap(fs.Dir.prototype, 'close', createWrapFunction('dir.'))
103+
wrap(fs.Dir.prototype, 'closeSync', createWrapFunction('dir.'))
104+
wrap(fs.Dir.prototype, 'read', createWrapFunction('dir.'))
105+
wrap(fs.Dir.prototype, 'readSync', createWrapFunction('dir.'))
106+
wrap(fs.Dir.prototype, Symbol.asyncIterator, createWrapDirAsyncIterator())
107+
}
110108

111-
wrap(fs, 'unwatchFile', createWatchWrapFunction())
112-
wrap(fs, 'watch', createWatchWrapFunction())
113-
wrap(fs, 'watchFile', createWatchWrapFunction())
109+
wrap(fs, 'unwatchFile', createWatchWrapFunction())
110+
wrap(fs, 'watch', createWatchWrapFunction())
111+
wrap(fs, 'watchFile', createWatchWrapFunction())
112+
113+
return fs
114+
})
114115

115-
return fs
116-
})
117-
}
118116
function isFirstMethodReturningFileHandle (original) {
119117
return !kHandle && original.name === 'open'
120118
}

0 commit comments

Comments
 (0)