Skip to content

Commit

Permalink
instrumentation-tracker
Browse files Browse the repository at this point in the history
  • Loading branch information
jsumners-nr committed Mar 19, 2024
1 parent 6fddc8b commit 8e7e560
Show file tree
Hide file tree
Showing 10 changed files with 850 additions and 934 deletions.
25 changes: 0 additions & 25 deletions jsdoc-conf.json

This file was deleted.

37 changes: 37 additions & 0 deletions jsdoc-conf.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"opts": {
"destination": "out/",
"readme": "./README.md",
"template": "./node_modules/clean-jsdoc-theme",
"theme_opts": {
"search": false
},
"tutorials": "examples/shim",
"recurse": true
},
"source": {
"exclude": ["node_modules", "test"],
"includePattern": ".+\\.js(doc)?$",
// Note: we cannot add patterns to the "include" strings. They must be
// explicit strings that point to either specific files or directories.
// Recursion is enabled for the `jsdoc` command, thus any directories
// specified here will be processed recursively. So make sure any files in
// these directories should be included in our public docs.
"include": [
"lib/shim/",
"lib/transaction/handle.js",
"lib/instrumentation-descriptor.js"
]
},
"plugins": [
"plugins/markdown"
],
"templates": {
"cleverLinks": true,
"showInheritedInNav": false
},
"markdown": {
"hardwrap": false,
"idInHeadings": true
}
}
150 changes: 69 additions & 81 deletions lib/shimmer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const shims = require('./shim')
const { Hook } = require('@newrelic/ritm')
const IitmHook = require('import-in-the-middle')
const { nrEsmProxy } = require('./symbols')
const moduleIds = new (require('./util/idgen'))()
const InstrumentationDescriptor = require('./instrumentation-descriptor')
const InstrumentationTracker = require('./instrumentation-tracker')
let pkgsToHook = []

const NAMES = require('./metrics/names')
Expand Down Expand Up @@ -362,6 +363,7 @@ const shimmer = (module.exports = {
_firstPartyInstrumentation(agent, filePath, shim, uninstrumented, mojule)
}
},

registerHooks(agent) {
this._ritm = new Hook(pkgsToHook, function onHook(exports, name, basedir) {
return _postLoad(agent, exports, name, basedir)
Expand All @@ -370,6 +372,7 @@ const shimmer = (module.exports = {
return _postLoad(agent, exports, name, basedir, true)
})
},

removeHooks() {
if (this._ritm) {
this._ritm.unhook()
Expand All @@ -395,10 +398,11 @@ const shimmer = (module.exports = {
return
}

const registeredInstrumentation = shimmer.registeredInstrumentations[opts.moduleName]
const registeredInstrumentation = shimmer.registeredInstrumentations.getAllByName(
opts.moduleName
)

if (!registeredInstrumentation) {
shimmer.registeredInstrumentations[opts.moduleName] = []
// In cases where a customer is trying to instrument a file
// that is not within node_modules, they must provide the absolutePath
// so require-in-the-middle can call our callback. the moduleName
Expand All @@ -413,14 +417,13 @@ const shimmer = (module.exports = {
}
}

const instrumentation = {
...opts,
instrumentationId: moduleIds.idFor(opts.moduleName)
}
shimmer.registeredInstrumentations[opts.moduleName].push(instrumentation)
shimmer.registeredInstrumentations.track(
opts.moduleName,
new InstrumentationDescriptor({ ...opts })
)
},

registeredInstrumentations: Object.create(null),
registeredInstrumentations: new InstrumentationTracker(),

/**
* NOT FOR USE IN PRODUCTION CODE
Expand Down Expand Up @@ -470,20 +473,26 @@ const shimmer = (module.exports = {
* only if every hook succeeded.
*
* @param {string} moduleName name of registered instrumentation
* @param {object} nodule the module that is being instrumented
* @returns {boolean} if all instrumentation hooks ran for a given version
* @param {object} resolvedName the fully resolve path to the module
* @returns {boolean} if all instrumentation hooks successfully ran for a
* module
*/
isInstrumented(moduleName, nodule) {
const instrumentations = shimmer.registeredInstrumentations[moduleName] ?? []
return nodule?.[symbols.instrumented]?.length === instrumentations.length
isInstrumented(moduleName, resolvedName) {
const items = shimmer.registeredInstrumentations
.getAllByName(moduleName)
.filter(
(item) =>
item.instrumentation.resolvedName === resolvedName && item.meta.instrumented === true
)
return items.length > 0
},

instrumentPostLoad(agent, module, moduleName, resolvedName, returnModule = false) {
const result = _postLoad(agent, module, moduleName, resolvedName)
// This is to not break the public API
// previously it would just call instrumentation
// and not check the result
return returnModule ? result : shimmer.isInstrumented(moduleName, module)
return returnModule ? result : shimmer.isInstrumented(moduleName, resolvedName)
},

/**
Expand All @@ -494,8 +503,15 @@ const shimmer = (module.exports = {
*/
getPackageVersion(moduleName) {
try {
const { basedir } = shimmer.registeredInstrumentations[moduleName]
const pkg = require(path.resolve(basedir, 'package.json'))
const trackedItems = shimmer.registeredInstrumentations.getAllByName(moduleName)
if (trackedItems === undefined) {
throw Error(`no tracked items for module '${moduleName}'`)
}
const item = trackedItems.find((item) => item.instrumentation.resolvedName !== undefined)
if (item === undefined) {
return process.version
}
const pkg = require(path.resolve(item.instrumentation.resolvedName, 'package.json'))
return pkg.version
} catch (err) {
logger.debug('Failed to get version for `%s`, reason: %s', moduleName, err.message)
Expand All @@ -512,8 +528,6 @@ function applyDebugState(shim, nodule, inEsm) {
instrumented.push({
[symbols.unwrap]: function unwrapNodule() {
delete nodule[symbols.shim]
delete nodule[symbols.instrumented]
delete nodule[symbols.instrumentedErrored]
}
})
nodule[symbols.shim] = shim
Expand All @@ -536,18 +550,17 @@ function applyDebugState(shim, nodule, inEsm) {
function instrumentPostLoad(agent, nodule, moduleName, resolvedName, esmResolver) {
// default to Node.js version, this occurs for core libraries
const pkgVersion = resolvedName ? shimmer.getPackageVersion(moduleName) : process.version
const instrumentations = shimmer.registeredInstrumentations[moduleName]
instrumentations.forEach((instrumentation) => {
const isInstrumented =
nodule[symbols.instrumented]?.includes(instrumentation.instrumentationId) === true
const failedInstrumentation =
nodule[symbols.instrumentedErrored]?.includes(instrumentation.instrumentationId) === true
const trackedInstrumentations = shimmer.registeredInstrumentations.getAllByName(moduleName)
trackedInstrumentations.forEach((trackedInstrumentation) => {
const isInstrumented = trackedInstrumentation.meta.instrumented === true
const failedInstrumentation = trackedInstrumentation.meta.didError === true
if (isInstrumented === true || failedInstrumentation === true) {
const msg = isInstrumented ? 'Already instrumented' : 'Failed to instrument'
logger.trace(`${msg} ${moduleName}@${pkgVersion}, skipping registering instrumentation`)
return
}

const { instrumentation } = trackedInstrumentation
const resolvedNodule = resolveNodule({ nodule, instrumentation, esmResolver })
const shim = shims.createShimFromType({
type: instrumentation.type,
Expand All @@ -559,18 +572,13 @@ function instrumentPostLoad(agent, nodule, moduleName, resolvedName, esmResolver
})

applyDebugState(shim, resolvedNodule, esmResolver)
trackInstrumentationUsage(
agent,
shim,
instrumentation.specifier || moduleName,
NAMES.FEATURES.INSTRUMENTATION.ON_REQUIRE
)
trackInstrumentationUsage(agent, shim, moduleName, NAMES.FEATURES.INSTRUMENTATION.ON_REQUIRE)

// Tracking instrumentation is only used to add the supportability metrics
// that occur directly above this. No reason to attempt to load instrumentation
// as it does not exist.
if (instrumentation.type === MODULE_TYPE.TRACKING) {
registerHook(nodule, instrumentation)
shimmer.registeredInstrumentations.setHookSuccess(trackedInstrumentation)
return
}

Expand Down Expand Up @@ -648,28 +656,20 @@ function resolveNodule({ nodule, instrumentation, esmResolver }) {
* @param {object} params wrapping object to function
* @param {*} params.shim The instance of the shim used to instrument the module.
* @param {object} params.nodule Class or module containing the function to wrap.
* @param {object} params.resolvedNodule returns xport of the default property
* @param {string} params.pkgVersion version of module
* @param {object} params.resolvedNodule returns export of the default property
* @param {string} params.moduleName module name
* @param {object} params.instrumentation hooks for a give module
* @returns {object} updated xport module
* @param {InstrumentationDescriptor} params.instrumentation hooks for a give module
* @returns {object} updated export module
*/
function loadInstrumentation({
shim,
resolvedNodule,
pkgVersion,
moduleName,
nodule,
instrumentation
}) {
function loadInstrumentation({ shim, resolvedNodule, moduleName, nodule, instrumentation }) {
const trackedItem = shimmer.registeredInstrumentations.getTrackedItem(moduleName, instrumentation)
try {
if (instrumentation.onRequire(shim, resolvedNodule, moduleName) !== false) {
instrumentation.instrumentationId = moduleIds.idFor(moduleName)
shimmer.registeredInstrumentations.setHookSuccess(trackedItem)
nodule = shim.getExport(nodule)
registerHook(nodule, instrumentation)
}
} catch (instrumentationError) {
registerHook(nodule, instrumentation, symbols.instrumentedErrored)
shimmer.registeredInstrumentations.setHookFailure(trackedItem)
if (instrumentation.onError) {
try {
instrumentation.onError(instrumentationError)
Expand Down Expand Up @@ -711,20 +711,34 @@ function _firstPartyInstrumentation(agent, fileName, shim, nodule, moduleName) {
}
}

/**
* Invoked directly after a module is loaded via `require` (or `import`). This
* is the first opportunity for us to work with the newly loaded module. Prior
* to this, the only information we have is the simple name (the string used
* to load the module), as well as the `onRequire` and `onError` hooks to
* attach to the module.
*
* @param {object} agent
* @param {object} nodule The newly loaded module.
* @param {string} name The simple name used to load the module.
* @param {string} resolvedName The full file system path to the module.
* @param {object} [esmResolver]
* @returns {*|Object|undefined}
* @private
*/
function _postLoad(agent, nodule, name, resolvedName, esmResolver) {
const instrumentation = shimmer.getInstrumentationNameFromModuleName(name)
const registeredInstrumentation = shimmer.registeredInstrumentations[instrumentation]
const simpleName = shimmer.getInstrumentationNameFromModuleName(name)
const registeredInstrumentations = shimmer.registeredInstrumentations.getAllByName(simpleName)
const hasPostLoadInstrumentation =
registeredInstrumentation &&
registeredInstrumentation.length &&
registeredInstrumentation.filter((hook) => hook.onRequire).length
registeredInstrumentations &&
registeredInstrumentations.length &&
registeredInstrumentations.filter((ri) => ri.instrumentation.onRequire).length

// Check if this is a known instrumentation and then run it.
if (hasPostLoadInstrumentation) {
// Add the basedir to the instrumentation to be used later to parse version from package.json
registeredInstrumentation.basedir = resolvedName
shimmer.registeredInstrumentations.setResolvedName(simpleName, resolvedName)
logger.trace('Instrumenting %s with onRequire (module loaded) hook.', name)
return instrumentPostLoad(agent, nodule, instrumentation, resolvedName, esmResolver)
return instrumentPostLoad(agent, nodule, simpleName, resolvedName, esmResolver)
}

return nodule
Expand Down Expand Up @@ -791,29 +805,3 @@ function tryGetVersion(shim) {

return shim.pkgVersion
}

/**
* When a module is being instrumented, this function is used to track all of
* the hooks being registered for that module. As an example, the core agent
* may register a set of `onRequire` and `onError` hooks and then the security
* agent may also register its own set of `onRequire` and `onError` hooks. We
* need to keep track of all such hooks, and this utility function allows us
* to do that without replicating this code in all of the places we end up
* doing the registration (which is, unfortunately, in more than one place).
*
* @param {object} nodule The module being instrumented.
* @param {object} instrumentation The instrumentation configuration. It has
* properties that represent the hooks being registered, in addition to a
* unique identifier that identifies the instrumentation configuration defining
* those hooks.
* @param {Symbol} [sym] The symbol to use when updating
* the module's registered hooks. Possible values are `symbols.instrumented`
* and `symbols.instrumentedErrored`.
*/
function registerHook(nodule, instrumentation, sym = symbols.instrumented) {
if (Array.isArray(nodule[sym]) === true) {
nodule[sym].push(instrumentation.instrumentationId)
} else {
nodule[sym] = [instrumentation.instrumentationId]
}
}
2 changes: 0 additions & 2 deletions lib/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ module.exports = {
databaseName: Symbol('databaseName'),
disableDT: Symbol('Disable distributed tracing'), // description for backwards compatibility
executorContext: Symbol('executorContext'),
instrumented: Symbol('instrumented'),
instrumentedErrored: Symbol('instrumentedErrored'),
name: Symbol('name'),
onceExecuted: Symbol('onceExecuted'),
offTheRecord: Symbol('offTheRecord'),
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
"lint": "eslint ./*.{js,mjs} lib test bin examples",
"lint:fix": "eslint --fix, ./*.{js,mjs} lib test bin examples",
"lint:lockfile": "lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https --validate-integrity",
"public-docs": "jsdoc -c ./jsdoc-conf.json --tutorials examples/shim api.js lib/shim/* lib/shim/specs/params/* lib/transaction/handle.js && cp examples/shim/*.png out/",
"public-docs": "jsdoc -c ./jsdoc-conf.jsonc && cp examples/shim/*.png out/",
"publish-docs": "./bin/publish-docs.sh",
"services": "docker compose up -d --wait",
"smoke": "npm run ssl && time tap test/smoke/**/**/*.tap.js --timeout=180 --no-coverage",
Expand Down Expand Up @@ -217,7 +217,7 @@
"devDependencies": {
"@newrelic/eslint-config": "^0.3.0",
"@newrelic/newrelic-oss-cli": "^0.1.2",
"@newrelic/test-utilities": "^8.1.0",
"@newrelic/test-utilities": "git+https://github.com/jsumners-nr/node-test-utilities#oh-sweet-shimmer",
"@octokit/rest": "^18.0.15",
"@slack/bolt": "^3.7.0",
"ajv": "^6.12.6",
Expand Down
Loading

0 comments on commit 8e7e560

Please sign in to comment.