Skip to content
Draft
3 changes: 2 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ export default [
'import/no-duplicates': 'error',
'import/no-named-default': 'error',
'import/no-webpack-loader-syntax': 'error',
'jsdoc/check-tag-names': ['error', { definedTags: ['datadog'] }],
'jsdoc/check-param-names': ['warn', { disableMissingParamChecks: true }],
'jsdoc/check-tag-names': ['warn', { definedTags: ['datadog'] }],
// TODO: Enable the rules that we want to use.
'jsdoc/check-types': 'off', // Should be activated, but it needs a couple of fixes.
// no-defaults: This should be activated, since the defaults will not be picked up in a description.
Expand Down
3 changes: 2 additions & 1 deletion integration-tests/package-guardrails.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ describe('package guardrails', () => {

it('should not instrument the package', () =>
runTest(`Application instrumentation bootstrapping complete
Found incompatible integration version: bluebird@1.0.0
false
instrumentation source: manual
Found incompatible integration version: bluebird@1.0.0
`, []))
})
})
Expand Down
25 changes: 18 additions & 7 deletions packages/datadog-esbuild/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,38 @@ for (const hook of Object.values(hooks)) {
}
}

function moduleOfInterestKey (name, file) {
return file ? `${name}/${file}` : name
}

const builtinModules = new Set(require('module').builtinModules)

function addModuleOfInterest (name, file) {
if (!name) return

modulesOfInterest.add(moduleOfInterestKey(name, file))

if (builtinModules.has(name)) {
modulesOfInterest.add(moduleOfInterestKey(`node:${name}`, file))
}
}

const modulesOfInterest = new Set()

for (const instrumentation of Object.values(instrumentations)) {
for (const entry of instrumentation) {
if (!entry.file) {
modulesOfInterest.add(entry.name) // e.g. "redis"
} else {
modulesOfInterest.add(`${entry.name}/${entry.file}`) // e.g. "redis/my/file.js"
}
addModuleOfInterest(entry.name, entry.file)
}
}

const RAW_BUILTINS = require('module').builtinModules
const CHANNEL = 'dd-trace:bundler:load'
const path = require('path')
const fs = require('fs')
const { execSync } = require('child_process')

const builtins = new Set()

for (const builtin of RAW_BUILTINS) {
for (const builtin of builtinModules) {
builtins.add(builtin)
builtins.add(`node:${builtin}`)
}
Expand Down
22 changes: 6 additions & 16 deletions packages/datadog-instrumentations/src/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ const childProcessChannel = dc.tracingChannel('datadog:child_process:execution')
// ignored exec method because it calls to execFile directly
const execAsyncMethods = ['execFile', 'spawn']

const names = ['child_process', 'node:child_process']

// child_process and node:child_process returns the same object instance, we only want to add hooks once
let patched = false

function throwSyncError (error) {
throw error
}
Expand All @@ -37,18 +32,13 @@ function returnSpawnSyncError (error, context) {
return context.result
}

names.forEach(name => {
addHook({ name }, childProcess => {
if (!patched) {
patched = true
shimmer.massWrap(childProcess, execAsyncMethods, wrapChildProcessAsyncMethod(childProcess.ChildProcess))
shimmer.wrap(childProcess, 'execSync', wrapChildProcessSyncMethod(throwSyncError, true))
shimmer.wrap(childProcess, 'execFileSync', wrapChildProcessSyncMethod(throwSyncError))
shimmer.wrap(childProcess, 'spawnSync', wrapChildProcessSyncMethod(returnSpawnSyncError))
}
addHook({ name: 'child_process' }, childProcess => {
shimmer.massWrap(childProcess, execAsyncMethods, wrapChildProcessAsyncMethod(childProcess.ChildProcess))
shimmer.wrap(childProcess, 'execSync', wrapChildProcessSyncMethod(throwSyncError, true))
shimmer.wrap(childProcess, 'execFileSync', wrapChildProcessSyncMethod(throwSyncError))
shimmer.wrap(childProcess, 'spawnSync', wrapChildProcessSyncMethod(returnSpawnSyncError))

return childProcess
})
return childProcess
})

function normalizeArgs (args, shell) {
Expand Down
3 changes: 1 addition & 2 deletions packages/datadog-instrumentations/src/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ const cryptoCipherCh = channel('datadog:crypto:cipher:start')

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

addHook({ name: names }, crypto => {
addHook({ name: 'crypto' }, crypto => {
shimmer.massWrap(crypto, hashMethods, wrapCryptoMethod(cryptoHashCh))
shimmer.massWrap(crypto, cipherMethods, wrapCryptoMethod(cryptoCipherCh))
return crypto
Expand Down
3 changes: 1 addition & 2 deletions packages/datadog-instrumentations/src/dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ const rrtypes = {
}

const rrtypeMap = new WeakMap()
const names = ['dns', 'node:dns']

addHook({ name: names }, dns => {
addHook({ name: 'dns' }, dns => {
shimmer.wrap(dns, 'lookup', fn => wrap('apm:dns:lookup', fn, 2))
shimmer.wrap(dns, 'lookupService', fn => wrap('apm:dns:lookup_service', fn, 2))
shimmer.wrap(dns, 'resolve', fn => wrap('apm:dns:resolve', fn, 2))
Expand Down
8 changes: 4 additions & 4 deletions packages/datadog-instrumentations/src/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ function wrapAppUse (use) {
}
}

addHook({ name: 'express', versions: ['>=4'], file: ['lib/express.js'] }, express => {
addHook({ name: 'express', versions: ['>=4'], file: 'lib/express.js' }, express => {
shimmer.wrap(express.application, 'handle', wrapHandle)
shimmer.wrap(express.application, 'all', wrapAppAll)
shimmer.wrap(express.application, 'route', wrapAppRoute)
Expand Down Expand Up @@ -224,19 +224,19 @@ function wrapProcessParamsMethod (requestPositionInArguments) {
}
}

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

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

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

addHook({ name: 'express', file: ['lib/request.js'], versions: ['>=5.0.0'] }, request => {
addHook({ name: 'express', file: 'lib/request.js', versions: ['>=5.0.0'] }, request => {
shimmer.wrap(request, 'query', function (originalGet) {
return function wrappedGet () {
const query = originalGet.call(this)
Expand Down
54 changes: 26 additions & 28 deletions packages/datadog-instrumentations/src/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,37 +84,35 @@ const paramsByFileHandleMethods = {
writeFile: ['data', 'options'],
writev: ['buffers', 'position']
}
const names = ['fs', 'node:fs']
names.forEach(name => {
addHook({ name }, fs => {
const asyncMethods = Object.keys(paramsByMethod)
const syncMethods = asyncMethods.map(name => `${name}Sync`)

massWrap(fs, asyncMethods, createWrapFunction())
massWrap(fs, syncMethods, createWrapFunction())
massWrap(fs.promises, asyncMethods, createWrapFunction('promises.'))

wrap(fs.realpath, 'native', createWrapFunction('', 'realpath.native'))
wrap(fs.realpathSync, 'native', createWrapFunction('', 'realpath.native'))
wrap(fs.promises.realpath, 'native', createWrapFunction('', 'realpath.native'))

wrap(fs, 'createReadStream', wrapCreateStream)
wrap(fs, 'createWriteStream', wrapCreateStream)
if (fs.Dir) {
wrap(fs.Dir.prototype, 'close', createWrapFunction('dir.'))
wrap(fs.Dir.prototype, 'closeSync', createWrapFunction('dir.'))
wrap(fs.Dir.prototype, 'read', createWrapFunction('dir.'))
wrap(fs.Dir.prototype, 'readSync', createWrapFunction('dir.'))
wrap(fs.Dir.prototype, Symbol.asyncIterator, createWrapDirAsyncIterator())
}
addHook({ name: 'fs' }, fs => {
const asyncMethods = Object.keys(paramsByMethod)
const syncMethods = asyncMethods.map(name => `${name}Sync`)

massWrap(fs, asyncMethods, createWrapFunction())
massWrap(fs, syncMethods, createWrapFunction())
massWrap(fs.promises, asyncMethods, createWrapFunction('promises.'))

wrap(fs.realpath, 'native', createWrapFunction('', 'realpath.native'))
wrap(fs.realpathSync, 'native', createWrapFunction('', 'realpath.native'))
wrap(fs.promises.realpath, 'native', createWrapFunction('', 'realpath.native'))

wrap(fs, 'createReadStream', wrapCreateStream)
wrap(fs, 'createWriteStream', wrapCreateStream)
if (fs.Dir) {
wrap(fs.Dir.prototype, 'close', createWrapFunction('dir.'))
wrap(fs.Dir.prototype, 'closeSync', createWrapFunction('dir.'))
wrap(fs.Dir.prototype, 'read', createWrapFunction('dir.'))
wrap(fs.Dir.prototype, 'readSync', createWrapFunction('dir.'))
wrap(fs.Dir.prototype, Symbol.asyncIterator, createWrapDirAsyncIterator())
}

wrap(fs, 'unwatchFile', createWatchWrapFunction())
wrap(fs, 'watch', createWatchWrapFunction())
wrap(fs, 'watchFile', createWatchWrapFunction())
wrap(fs, 'unwatchFile', createWatchWrapFunction())
wrap(fs, 'watch', createWatchWrapFunction())
wrap(fs, 'watchFile', createWatchWrapFunction())

return fs
})
return fs
})

function isFirstMethodReturningFileHandle (original) {
return !kHandle && original.name === 'open'
}
Expand Down
55 changes: 42 additions & 13 deletions packages/datadog-instrumentations/src/helpers/bundler-register.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

/** @type {typeof import('node:diagnostics_channel')} */
const dc = require('dc-polyfill')

const {
Expand All @@ -22,45 +23,73 @@ if (!dc.unsubscribe) {
dc.unsubscribe = (channel, cb) => {
if (dc.channel(channel).hasSubscribers) {
dc.channel(channel).unsubscribe(cb)
return true
}
return false
}
}

function doHook (payload) {
const hook = hooks[payload.package]
/**
* @param {string} name
*/
function doHook (name) {
const hook = hooks[name] ?? hooks[`node:${name}`]
if (!hook) {
log.error('esbuild-wrapped %s missing in list of hooks', payload.package)
log.error('esbuild-wrapped %s missing in list of hooks', name)
return
}

const hookFn = hook.fn ?? hook
if (typeof hookFn !== 'function') {
log.error('esbuild-wrapped hook %s is not a function', payload.package)
log.error('esbuild-wrapped hook %s is not a function', name)
return
}

try {
hookFn()
} catch {
log.error('esbuild-wrapped %s hook failed', payload.package)
log.error('esbuild-wrapped %s hook failed', name)
}
}

dc.subscribe(CHANNEL, (payload) => {
doHook(payload)
/** @type {Set<string>} */
const instrumentedNodeModules = new Set()

if (!instrumentations[payload.package]) {
log.error('esbuild-wrapped %s missing in list of instrumentations', payload.package)
/** @typedef {{ package: string, module: unknown, version: string, path: string }} Payload */
dc.subscribe(CHANNEL, (message) => {
const payload = /** @type {Payload} */ (message)
const name = payload.package

const isPrefixedWithNode = name.startsWith('node:')

const isNodeModule = isPrefixedWithNode || !hooks[name]

if (isNodeModule) {
const nodeName = isPrefixedWithNode ? name.slice(5) : name
// Used for node: prefixed modules to prevent double instrumentation.
if (instrumentedNodeModules.has(nodeName)) {
return
}
instrumentedNodeModules.add(nodeName)
}

doHook(name)

const instrumentation = instrumentations[name] ?? instrumentations[`node:${name}`]

if (!instrumentation) {
log.error('esbuild-wrapped %s missing in list of instrumentations', name)
return
}

for (const { name, file, versions, hook } of instrumentations[payload.package]) {
if (payload.path !== filename(name, file)) continue
if (!matchVersion(payload.version, versions)) continue
for (const { name, file, versions, hook } of instrumentation) {
if (payload.path !== filename(name, file) || !matchVersion(payload.version, versions)) {
continue
}

try {
loadChannel.publish({ name, version: payload.version, file })
payload.module = hook(payload.module, payload.version)
payload.module = hook(payload.module, payload.version) ?? payload.module
} catch (e) {
log.error('Error executing bundler hook', e)
}
Expand Down
34 changes: 28 additions & 6 deletions packages/datadog-instrumentations/src/helpers/hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,37 @@
const iitm = require('../../../dd-trace/src/iitm')
const path = require('path')
const ritm = require('../../../dd-trace/src/ritm')
const log = require('../../../dd-trace/src/log')
const requirePackageJson = require('../../../dd-trace/src/require-package-json')

/**
* @param {string} moduleBaseDir
* @returns {string|undefined}
*/
function getVersion (moduleBaseDir) {
if (moduleBaseDir) {
return requirePackageJson(moduleBaseDir, /** @type {import('module').Module} */ (module)).version
}
return process.version
}

/**
* This is called for every package/internal-module that dd-trace supports instrumentation for
* In practice, `modules` is always an array with a single entry.
*
* @overload
* @param {string[]} modules list of modules to hook into
* @param {object} hookOptions hook options
* @param {Function} onrequire callback to be executed upon encountering module
*/
/**
* @overload
* @param {string[]} modules list of modules to hook into
* @param {object} hookOptions hook options
* @param {Function} onrequire callback to be executed upon encountering module
*/
function Hook (modules, hookOptions, onrequire) {
// TODO: Rewrite this to use class syntax. The same should be done for ritm.
if (!(this instanceof Hook)) return new Hook(modules, hookOptions, onrequire)

if (typeof hookOptions === 'function') {
Expand All @@ -32,6 +53,13 @@ function Hook (modules, hookOptions, onrequire) {

let defaultWrapResult

try {
moduleVersion ||= getVersion(moduleBaseDir)
} catch (error) {
log.error('Error getting version for "%s": %s', moduleName, error.message, error)
return
}

if (
isIitm &&
moduleExports.default &&
Expand Down Expand Up @@ -60,10 +88,4 @@ function Hook (modules, hookOptions, onrequire) {
})
}

Hook.prototype.unhook = function () {
this._ritmHook.unhook()
this._iitmHook.unhook()
this._patched = Object.create(null)
}

module.exports = Hook
Loading
Loading