documentation/module-instrumentations.md
An instrumentation allows us to run some code immediately after a Node.js program loads a specific module via the require function. This code can then "wrap" (i.e redefine, "monkey-patch", etc.) module methods in order to create metrics, events, segments, whatever-else, etc. as needed. An instrumentation also uses helper objects called "shims". The methods on these shims can perform tasks that are shared across different sorts of instrumentations, (naming a transaction, creating a segment, etc.).
There are three broad and overlapping categories of instrumentations.
Core instrumentations instrument modules provided by the core Node.js system. (ex. 'http')
First party instrumentations instrument modules that don't ship with core Node.js (ex. 'express'), and usually come from npm packages. An internal first party instrumentation's code lives inside the agent. An external first party instrumentation lives as a stand alone npm package.
This document will describe the mechanics of all three.
We'll start with configuring an instrumentation. When we configure an instrumentation, we need to tell the agent
Configuration details vary slightly across the three broad categories of instrumentations.
A core instrumentation is configured via the CORE_INSTRUMENTATION object in the the shimmer module.
var CORE_INSTRUMENTATION = {
child_process: {
type: MODULE_TYPE.GENERIC,
file: 'child_process.js'
},
[module_to_instrument]: {
type: MODULE_TYPE.[type],
file: 'instrumentation-module-file'
}, etc...
Each key in the CORE_INSTRUMENTATION object names the internal Node.js module we want to instrument. This configuration
child_process: {
/* ... */
},
will instrument the child_process module. The inner object contains the instrumentation's configuration.
{
type: MODULE_TYPE.GENERIC,
file: 'child_process.js'
},
The 'file' key tells the agent which file contains our instrumentation module, and assumes a base folder of lib/instrumentation/core. This means our instrumentation module for the child_process module is located at lib/instrumentation/core/child_process.js.
The 'type' key tells the agent the instrumentation's type. An instrumentation's type controls the sort of shim object our instrumentation module will receive. We'll cover that momentarily.
First party instrumentations are configured via the function exported by the agent-local instrumentations module.
module.exports = function instrumentations() {
return {
/* internal instrumentation */
'express': {type: MODULE_TYPE.WEB_FRAMEWORK},
'module-name': {type: MODULE_TYPE.[type]},
/* external instrumentation */
'koa': {module: '@newrelic/koa'},
'module-name': {module: 'npm-module-name'},
}
}
As mentioned, there are internal and external first party instrumentations. For both type of instrumentation, the key of the returned object is the name of the module to instrument. The above instrumentations instrument express and koa, respectively.
However, the value object will differ depending on whether this is an internal or external instrumentation.
For internal instrumentations, you must provide a module 'type'.
'express': {type: MODULE_TYPE.WEB_FRAMEWORK},
As with core instrumentations, this will determine the shim available to you in the instrumentation itself. You'll notice that, unlike core instrumentations, there's no file 'key'. For internal first party instrumentations the agent will automatically load a file based on the name of the module being instrumented. For example, the 'express' instrumentation will be loaded from the lib/instrumentation/express.js file.
For external instrumentations, there's no type to configure.
'koa': {module: '@newrelic/koa'},
Instead, all you need to provide is an npm package identifier. These external first party instrumentations are structured slightly differently from the internal ones.
We'll discuss the specifics of that in the next section.
Once you've configured your instrumentation, you'll need to create your instrumentation module.
Core instrumentations and internal first party instrumentations share the same format. First party external/npm-package based instrumentations use a different format. We'll start with the core and internal instrumentation format.
To create either a core instrumentations or internal first party instrumentations you'll need to
Core instrumentations live in the lib/instrumentation/core/ folder, and their file name is the one you configured in the CORE_INSTRUMENTATION object. For example, the configuration for the http module
http: {
type: MODULE_TYPE.TRANSACTION,
file: 'http.js'
},
is configured with the filename http.js. This means the location of this instrumentation file will be lib/instrumentation/core/http.js.
First party instrumentations live in the lib/instrumentation folder, and will be automatically named based on the module being instrumented. For example, the express instrumentation
module.exports = function instrumentations() {
return {
/* ... */
'express': {type: MODULE_TYPE.WEB_FRAMEWORK},
/* ... */
}
}
can be found at lib/instrumentation/express.js.
In either case, an instrumentation module exports a single function, (named initialize by convention).
module.exports = function initialize(agent, moduleToInstrument, moduleName, shim) {
//your instrumentation code here
}
agent
This is a reference to the agent object
moduleToInstrument
This is a reference to the module that we're instrumenting, and that NodeJS just loaded.
moduleName
This is the name of the module that we're instrumenting, as a string.
shim
The type of the object in the shim variable depends on your instrumentation's type, and can be found in the SHIM_TYPE_MAP object.
var SHIM_TYPE_MAP = Object.create(null)
SHIM_TYPE_MAP[MODULE_TYPE.GENERIC] = shims.Shim
SHIM_TYPE_MAP[MODULE_TYPE.CONGLOMERATE] = shims.ConglomerateShim
SHIM_TYPE_MAP[MODULE_TYPE.DATASTORE] = shims.DatastoreShim
SHIM_TYPE_MAP[MODULE_TYPE.MESSAGE] = shims.MessageShim
SHIM_TYPE_MAP[MODULE_TYPE.PROMISE] = shims.PromiseShim
SHIM_TYPE_MAP[MODULE_TYPE.TRANSACTION] = shims.TransactionShim
SHIM_TYPE_MAP[MODULE_TYPE.WEB_FRAMEWORK] = shims.WebFrameworkShim
When you configure an external first party instrumentation, you provide an npm package identifier -- @newrelic/superagent below.
'superagent': {module: '@newrelic/superagent'},
These packages must have a top level Node.js module named nr-hooks. This nr-hooks module must be an array of objects.
module.exports = [{
type: 'generic',
moduleName: 'superagent',
onRequire: require('./lib/instrumentation')
}]
Each object in this array configures a single instrumentation. The type is the same type as in core and internal first party instrumentations (configured via a raw string above, since these stand-alone instrumentations don't have access to the MODULE_NAME constant).
The 'moduleName' is the name of the module to be instrumented, and 'onRequire' loads an external instrumentation module.
This external instrumentation module is similar, but not identical to, the first party and core instrumentation modules. This module still needs to export a single function which will setup your instrumentation. However, this function only has two parameters
module.exports = function instrument(shim, moduleToInstrument) {
//wrap your methods and do your newrelic-y things here
}
The first, 'shim', is the same helper object used with core and first party internal modules. These are the same SHIM_TYPE_MAP shim objects, discussed above.
The second, 'moduleToInstrument', is a reference to the module object you want to instrument.
As of this writing (April 15, 2019), there's one exception to everything in this document, and that's the amqp instrumentation.
There's a special case for this module in the instrumentation bootstrapping code.