newrelic
Version:
New Relic agent
1,722 lines (1,597 loc) • 67.3 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const arity = require('../util/arity')
const constants = require('./constants')
const hasOwnProperty = require('../util/properties').hasOwn
const logger = require('../logger').child({component: 'Shim'})
const path = require('path')
const specs = require('./specs')
const util = require('util')
// Some modules do terrible things, like change the prototype of functions. To
// avoid crashing things we'll use a cached copy of apply everywhere.
const fnApply = Function.prototype.apply
/**
* Constructs a shim associated with the given agent instance.
*
* @constructor
* @classdesc
* A helper class for wrapping modules with segments.
*
* @param {Agent} agent - The agent this shim will use.
* @param {string} moduleName - The name of the module being instrumented.
* @param {string} resolvedName - The full path to the loaded module.
*/
function Shim(agent, moduleName, resolvedName) {
if (!agent || !moduleName) {
throw new Error('Shim must be initialized with an agent and module name.')
}
this._logger = logger.child({module: moduleName})
this._agent = agent
this._toExport = null
this._debug = false
this.defineProperty(this, 'moduleName', moduleName)
// Determine the root directory of the module.
var moduleRoot = null
var next = resolvedName || '/'
do {
moduleRoot = next
next = path.dirname(moduleRoot)
} while (moduleRoot.length > 1 && !/node_modules(?:\/@[^/]+)?$/.test(next))
this._moduleRoot = moduleRoot
}
module.exports = Shim
Shim.defineProperty = defineProperty
Shim.defineProperties = defineProperties
// Copy the argument index enumeration onto the shim.
Shim.prototype.ARG_INDEXES = specs.ARG_INDEXES
defineProperties(Shim.prototype, specs.ARG_INDEXES)
// Copy symbols to the shim as well.
defineProperties(Shim, constants.SYMBOLS)
defineProperties(Shim.prototype, constants.SYMBOLS)
// Define other miscellaneous properties of the shim.
defineProperties(Shim.prototype, {
/**
* The agent associated with this shim.
*
* @readonly
* @member {Agent} Shim.prototype.agent
*/
agent: function getAgent() {
return this._agent
},
/**
* The tracer in use by the agent for the shim.
*
* @readonly
* @member {Tracer} Shim.prototype.tracer
*/
tracer: function getTracer() {
return this._agent.tracer
},
/**
* The logger for this shim.
*
* @readonly
* @member {Logger} Shim.prototype.logger
*/
logger: function getLogger() {
return this._logger
}
})
Shim.prototype.wrap = wrap
Shim.prototype.bindSegment = bindSegment
Shim.prototype.bindPromise = bindPromise
Shim.prototype.execute = execute
Shim.prototype.wrapReturn = wrapReturn
Shim.prototype.wrapClass = wrapClass
Shim.prototype.wrapExport = wrapExport
Shim.prototype.record = record
Shim.prototype.isWrapped = isWrapped
Shim.prototype.unwrap = unwrap
Shim.prototype.unwrapOnce = unwrapOnce
Shim.prototype.getOriginal = getOriginal
Shim.prototype.getSegment = getSegment
Shim.prototype.getActiveSegment = getActiveSegment
Shim.prototype.setActiveSegment = setActiveSegment
Shim.prototype.storeSegment = storeSegment
Shim.prototype.bindCallbackSegment = bindCallbackSegment
Shim.prototype.applySegment = applySegment
Shim.prototype.createSegment = createSegment
Shim.prototype.getName = getName
Shim.prototype.isObject = isObject
Shim.prototype.isFunction = isFunction
Shim.prototype.isPromise = isPromise
Shim.prototype.isString = isString
Shim.prototype.isNumber = isNumber
Shim.prototype.isBoolean = isBoolean
Shim.prototype.isArray = isArray
Shim.prototype.isNull = isNull
Shim.prototype.toArray = toArray
Shim.prototype.argsToArray = argsToArray
Shim.prototype.normalizeIndex = normalizeIndex
Shim.prototype.once = once
Shim.prototype.setInternalProperty = setInternalProperty
Shim.prototype.defineProperty = defineProperty
Shim.prototype.defineProperties = defineProperties
Shim.prototype.setDefaults = setDefaults
Shim.prototype.proxy = proxy
Shim.prototype.require = shimRequire
Shim.prototype.copySegmentParameters = copySegmentParameters
Shim.prototype.interceptPromise = interceptPromise
Shim.prototype.fixArity = arity.fixArity
// Internal methods.
Shim.prototype.getExport = getExport
Shim.prototype.enableDebug = enableDebug
Shim.prototype.__NR_unwrap = unwrapAll
// -------------------------------------------------------------------------- //
/**
* @callback WrapFunction
*
* @summary
* A function which performs the actual wrapping logic.
*
* @description
* If the return value of this function is not `original` then the return value
* will be marked as a wrapper.
*
* @param {Shim} shim
* The shim this function was passed to.
*
* @param {Object|Function} original
* The item which needs wrapping. Most of the time this will be a function.
*
* @param {string} name
* The name of `original` if it can be determined, otherwise `'<anonymous>'`.
*
* @return {*} The wrapper for the original, or the original value itself.
*/
/**
* @private
* @callback ArrayWrapFunction
*
* @description
* A wrap function used on elements of an array. In addition to the parameters
* of `WrapFunction`, these also receive an `index` and `total` as described
* below.
*
* @see WrapFunction
*
* @param {number} index - The index of the current element in the array.
* @param {number} total - The total number of items in the array.
*/
/**
* @private
* @callback ArgumentsFunction
*
* @param {Shim} shim
* The shim this function was passed to.
*
* @param {Function} func
* The function these arguments were passed to.
*
* @param {*} context
* The context the function is executing under (i.e. `this`).
*
* @param {Array.<*>} args
* The arguments being passed into the function.
*/
/**
* @callback SegmentFunction
*
* @summary
* A function which is called to compose a segment.
*
* @param {Shim} shim
* The shim this function was passed to.
*
* @param {Function} func
* The function the segment is created for.
*
* @param {string} name
* The name of the function.
*
* @param {Array.<*>} args
* The arguments being passed into the function.
*
* @return {string|SegmentSpec} The desired properties for the new segment.
*/
/**
* @callback RecorderFunction
*
* @summary
* A function which is called to compose a segment for recording.
*
* @param {Shim} shim
* The shim this function was passed to.
*
* @param {Function} func
* The function being recorded.
*
* @param {string} name
* The name of the function.
*
* @param {Array.<*>} args
* The arguments being passed into the function.
*
* @return {string|RecorderSpec} The desired properties for the new segment.
*/
/**
* @callback CallbackBindFunction
*
* @summary
* Performs segment binding on a callback function. Useful when identifying a
* callback is more complex than a simple argument offset.
*
* @param {Shim} shim
* The shim this function was passed to.
*
* @param {Function} func
* The function being recorded.
*
* @param {string} name
* The name of the function.
*
* @param {TraceSegment} segment
* The segment that the callback should be bound to.
*
* @param {Array.<*>} args
* The arguments being passed into the function.
*/
/**
* @private
* @callback MetricFunction
*
* @summary
* Measures all the necessary metrics for the given segment. This functionality
* is meant to be used by Shim subclasses, instrumentations should never create
* their own recorders.
*
* @param {TraceSegment} segment - The segment to record.
* @param {string} [scope] - The scope of the recording.
*/
/**
* @callback ConstructorHookFunction
*
* @summary
* Pre/post constructor execution hook for wrapping classes. Used by
* {@link ClassWrapSpec}.
*
* @param {Shim} shim
* The shim performing the wrapping/binding.
*
* @param {Function} Base
* The class that was wrapped.
*
* @param {string} name
* The name of the `Base` class.
*
* @param {Array.<*>} args
* The arguments to the class constructor.
*
* @see ClassWrapSpec
*/
/**
* @private
* @interface Spec
*
* @description
* The syntax for declarative instrumentation. It can be used interlaced with
* custom, hand-written instrumentation for one-off or hard to simplify
* instrumentation logic.
*
* @property {Spec|WrapFunction} $return
* Changes the context to the return value of the current context. This means
* the sub spec will not be executed up front, but instead upon every execution
* of the current context.
*
* ```js
* var ret = func.apply(this, args);
* return shim.wrap(ret, spec.$return)
* ```
*
* @property {Spec|WrapFunction} $proto
* Changes the context to the prototype of the current context. The prototype
* is found using `Object.getPrototypeOf`.
*
* ```js
* shim.wrap(Object.getPrototypeOf(context), spec.$proto)
* ```
*
* @property {bool} $once
* Ensures that the parent spec will only be executed one time if the value is
* `true`. Good for preventing double wrapping of prototype methods.
*
* ```js
* if (spec.$once && spec.__NR_onceExecuted) {
* return context
* }
* spec.__NR_onceExecuted = true
* ```
*
* @property {ArgumentsFunction} $arguments
* Executes the function with all of the arguments passed in. The arguments can
* be modified in place. This will execute before `$eachArgument`.
*
* ```js
* spec.$arguments(args)
* ```
*
* @property {Spec|ArrayWrapFunction} $eachArgument
* Executes `shim.wrap` on each argument passed to the current context. The
* returned arguments will then be used to actually execute the function.
*
* ```js
* var argLength = arguments.length
* var extraArgs = extras.concat([0, argLength])
* var iIdx = extraArgs.length - 2
* var args = new Array(argLength)
* for (var i = 0; i < argLength; ++i) {
* extraArgs[iIdx] = i
* args[i] = shim.wrap(arguments[i], spec.$eachArgument, extraArgs)
* }
* func.apply(this, args)
* ```
*
* @property {Array.<{$properties: Array.<string>, $spec: Spec}>} $wrappings
* Executes `shim.wrap` with the current context as the `nodule` for each
* element in the array. The `$properties` sub-key must list one or more
* properties to be wrapped. The `$spec` sub-key must be a {@link Spec} or
* {@link WrapFunction} for wrapping the properties.
*
* ```js
* spec.$wrappings.forEach(function($wrap) {
* shim.wrap(context, $wrap.$properties, $wrap.$spec)
* })
* ```
*
* @property {bool|string|SegmentFunction} $segment
* Controls segment creation. If a falsey value (i.e. `undefined`, `false`,
* `null`, etc) then no segment will be created. If the value is `true`, then
* the name of the current context is used to name the segment. If the value is
* a string then that string will be the name of the segment. Lastly, if the
* value is a function, that function will be called with the current context
* and arguments.
*
* ```js
* var segment = null
* if (spec.$segment) {
* var seg = {name: spec.$segment}
* if (shim.isFunction(seg.name)) {
* seg = seg.name(func, this, arguments)
* }
* else if (seg.name === true) {
* seg.name = func.name
* }
* segment = shim.createSegment(seg.name, seg.recorder, seg.parent)
* }
* ```
*
* @property {Object.<string, *>} $cache
* Adds the value as an extra parameter to all specs in the same context as the
* cache. If the current context is a function, the cache will be recreated on
* each invocation of the function. This value can be useful for passing a
* value at runtime from one spec into another.
*
* ```js
* var args = extras || []
* if (spec.$cache) {
* args.push({})
* }
* ```
*
* @property {number} $callback
* Indicates that one of the parameters is a callback which should be wrapped.
*
* ```js
* if (shim.isNumber(spec.$callback)) {
* var idx = spec.$callback
* if (idx < 0) {
* idx = args.length + idx
* }
* args[idx] = shim.bindSegment(args[idx], segment)
* }
* ```
*
* @property {Spec|WrapFunction} property
* Any field which does not start with a `$` is assumed to name a property on
* the current context which should be wrapped. This is simply shorthand for a
* `$wrappings` with only one `$properties` value.
*/
/**
* @interface SegmentSpec
*
* @description
* The return value from a {@link SegmentFunction}, used to set the parameters
* of segment creation.
*
* @property {string} name
* The name for the segment to-be.
*
* @property {MetricFunction} [recorder]
* A metric recorder for the segment. This is purely for internal use by shim
* classes. Instrumentations should never implement their own metric functions.
*
* @property {TraceSegment} [parent]
* The parent segment. Defaults to the currently active segment.
*
* @see RecorderSpec
* @see SegmentFunction
*/
/**
* @interface RecorderSpec
* @extends SegmentSpec
*
* @description
* The return value from a {@link RecorderFunction}, used to set the parameters
* of segment creation and lifetime. Extends the {@link SegmentSpec}.
*
* @property {bool|string} [stream]
* Indicates if the return value from the wrapped function is a stream. If the
* value is truthy then the recording will extend to the `end` event of the
* stream. If the value is a string it is assumed to be the name of an event to
* measure. A segment will be created to record emissions of the event.
*
* @property {bool} [promise]
* Indicates if the return value from the wrapped function is a Promise. If the
* value is truthy then the recording will extend to the completion of the
* Promise.
*
* @property {number|CallbackBindFunction} [callback]
* If this is a number, it identifies which argument is the callback and the
* segment will also be bound to the callback. Otherwise, the passed function
* should perform the segment binding itself.
*
* @property {number|CallbackBindFunction} [rowCallback]
* Like `callback`, this identifies a callback function in the arguments. The
* difference is that the default behavior for row callbacks is to only create
* one segment for all calls to the callback. This is mostly useful for
* functions which will be called repeatedly, such as once for each item in a
* result set.
*
* @property {bool} [internal=false]
* Marks this as the boundary point into the instrumented library. If `true`
* and the current segment is _also_ marked as `internal` by the same shim,
* then we will not record this inner activity.
*
* This is useful when instrumenting a library which implements high-order
* methods which simply call other public methods and you only want to record
* the method directly called by the user while still instrumenting all
* endpoints.
*
* @property {function} [after=null]
* A function to call after the synchronous execution of the recorded method.
* If the function synchronously threw an error, that error will be handed to
* this function.
*
* @property {bool} [callbackRequired]
* When `true`, a recorded method must be called with a callback for a segment
* to be created. Does not apply if a custom callback method has been assigned
* via {@link callback}.
*
* @see SegmentSpec
* @see RecorderFunction
*/
/**
* @interface ClassWrapSpec
*
* @description
* Specifies the style of wrapping and construction hooks for wrapping classes.
*
* @property {bool} [es6=false]
* @property {ConstructorHookFunction} [pre=null]
* A function called with the constructor's arguments before the base class'
* constructor is executed. The `this` value will be `null`.
*
* @property {ConstructorHookFunction} [post=null]
* A function called with the constructor's arguments after the base class'
* constructor is executed. The `this` value will be the just-constructed object.
*
*/
// -------------------------------------------------------------------------- //
/**
* Entry point for executing a spec.
*
* @memberof Shim.prototype
*/
function execute(nodule, spec) {
if (this.isFunction(spec)) {
spec(this, nodule)
} else {
_specToFunction(spec)(this, nodule)
}
}
/**
* Executes the provided spec on one or more objects.
*
* - `wrap(nodule, properties, spec [, args])`
* - `wrap(func, spec [, args])`
*
* When called with a `nodule` and one or more properties, the spec will be
* executed on each property listed and the return value put back on the
* `nodule`.
*
* When called with just a function, the spec will be executed on the function
* and the return value of the spec simply passed back.
*
* The wrapped version will have the same prototype as the original
* method.
*
* @memberof Shim.prototype
*
* @param {Object|Function} nodule
* The source for the properties to wrap, or a single function to wrap.
*
* @param {string|Array.<string>} [properties]
* One or more properties to wrap. If omitted, the `nodule` parameter is
* assumed to be the function to wrap.
*
* @param {Spec|WrapFunction} spec
* The spec for wrapping these items.
*
* @param {Array.<*>} [args=[]]
* Optional extra arguments to be sent to the spec when executing it.
*
* @return {Object|Function} The first parameter to this function, after
* wrapping it or its properties.
*
* @see WrapFunction
*/
function wrap(nodule, properties, spec, args) {
if (!nodule) {
this.logger.debug('Not wrapping non-existent nodule.')
return nodule
}
// Sort out the parameters.
if (this.isObject(properties) && !this.isArray(properties)) {
// wrap(nodule, spec [, args])
args = spec
spec = properties
properties = null
}
if (this.isFunction(spec)) {
// wrap(nodule [, properties], wrapper [, args])
spec = {
wrapper: spec
}
}
// TODO: Add option for omitting __NR_original; unwrappable: false
spec = this.setDefaults(spec, {matchArity: false})
// If we're just wrapping one thing, just wrap it and return.
if (properties == null) {
const name = this.getName(nodule)
this.logger.trace('Wrapping nodule itself (%s).', name)
return _wrap(this, nodule, name, spec, args)
}
// Coerce properties into an array.
if (!this.isArray(properties)) {
properties = [properties]
}
// Wrap each property and return the nodule.
this.logger.trace('Wrapping %d properties on nodule.', properties.length)
properties.forEach(function wrapEachProperty(prop) {
// Skip nonexistent properties.
var original = nodule[prop]
if (!original) {
this.logger.debug('Not wrapping missing property "%s"', prop)
return
}
// Wrap up the property and add a special unwrapper.
var wrapped = _wrap(this, original, prop, spec, args)
if (wrapped && wrapped !== original) {
this.logger.trace('Replacing "%s" with wrapped version', prop)
nodule[prop] = wrapped
this.setInternalProperty(wrapped, '__NR_unwrap', function unwrapWrap() {
nodule[prop] = original
return original
})
}
}, this)
return nodule
}
/**
* Executes the provided spec with the return value of the given properties.
*
* - `wrapReturn(nodule, properties, spec [, args])`
* - `wrapReturn(func, spec [, args])`
*
* If the wrapper is executed with `new` then the wrapped function will also be
* called with `new`. This feature should only be used with factory methods
* disguised as classes. Normally {@link Shim#wrapClass} should be used to wrap
* constructors instead.
*
* @memberof Shim.prototype
*
* @param {Object|Function} nodule
* The source for the properties to wrap, or a single function to wrap.
*
* @param {string|Array.<string>} [properties]
* One or more properties to wrap. If omitted, the `nodule` parameter is
* assumed to be the function to wrap.
*
* @param {Spec|WrapReturnFunction} spec
* The spec for wrapping the returned value from the properties.
*
* @param {Array.<*>} [args=[]]
* Optional extra arguments to be sent to the spec when executing it.
*
* @return {Object|Function} The first parameter to this function, after
* wrapping it or its properties.
*
* @see Shim#wrap
* @see WrapReturnFunction
*/
function wrapReturn(nodule, properties, spec, args) {
// Munge our parameters as needed.
if (this.isObject(properties) && !this.isArray(properties)) {
// wrapReturn(nodule, spec [, args])
args = spec
spec = properties
properties = null
}
if (!this.isFunction(spec)) {
spec = _specToFunction(spec)
}
if (!this.isArray(args)) {
args = []
}
// Perform the wrapping!
return this.wrap(nodule, properties, function returnWrapper(shim, fn, fnName) {
// Only functions can have return values for us to wrap.
if (!shim.isFunction(fn)) {
return fn
}
let unwrapReference = null
const handler = {
get: function getTrap(target, prop) {
// Allow for look up of the target
if (prop === '__NR_original') {
return target
}
if (prop === '__NR_unwrap') {
return unwrapReference
}
return target[prop]
},
defineProperty: function definePropertyTrap(target, key, descriptor) {
if (key === '__NR_unwrap') {
unwrapReference = descriptor.value
} else {
Object.defineProperty(target, key, descriptor)
}
return true
},
set: function setTrap(target, key, val) {
if (key === '__NR_unwrap') {
unwrapReference = val
} else {
target[key] = val
}
return true
},
construct: function constructTrap(Target, proxyArgs) {
// Call the underlying function. If this was called as a constructor, call
// the wrapped function as a constructor too.
let ret = new Target(...proxyArgs)
// Assemble the arguments to hand to the spec.
const _args = [shim, fn, fnName, ret]
if (args.length > 0) {
_args.push.apply(_args, args)
}
// Call the spec and see if it handed back a different return value.
const newRet = spec.apply(ret, _args)
if (newRet) {
ret = newRet
}
return ret
},
apply: function applyTrap(target, thisArg, proxyArgs) {
// Call the underlying function. If this was called as a constructor, call
// the wrapped function as a constructor too.
let ret = target.apply(thisArg, proxyArgs)
// Assemble the arguments to hand to the spec.
const _args = [shim, fn, fnName, ret]
if (args.length > 0) {
_args.push.apply(_args, args)
}
// Call the spec and see if it handed back a different return value.
const newRet = spec.apply(thisArg, _args)
if (newRet) {
ret = newRet
}
return ret
}
}
return new Proxy(fn, handler)
})
}
/**
* Wraps a class constructor using a subclass with pre- and post-construction
* hooks.
*
* - `wrapClass(nodule, properties, spec [, args])`
* - `wrapClass(func, spec [, args])`
*
* @memberof Shim.prototype
*
* @param {Object|Function} nodule
* The source for the properties to wrap, or a single function to wrap.
*
* @param {string|Array.<string>} [properties]
* One or more properties to wrap. If omitted, the `nodule` parameter is
* assumed to be the constructor to wrap.
*
* @param {ClassWrapSpec|ConstructorHookFunction} spec
* The spec for wrapping the returned value from the properties or a post hook.
*
* @param {Array.<*>} [args=[]]
* Optional extra arguments to be sent to the spec when executing it.
*
* @return {Object|Function} The first parameter to this function, after
* wrapping it or its properties.
*
* @see Shim#wrap
*/
function wrapClass(nodule, properties, spec, args) {
// Munge our parameters as needed.
if (this.isObject(properties) && !this.isArray(properties)) {
// wrapReturn(nodule, spec [, args])
args = spec
spec = properties
properties = null
}
if (this.isFunction(spec)) {
spec = {pre: null, post: spec}
} else {
spec.pre = spec.pre || null
spec.post = spec.post || null
}
if (!this.isArray(args)) {
args = []
}
// Perform the wrapping!
return this.wrap(nodule, properties, function classWrapper(shim, Base, fnName) {
// Only functions can have return values for us to wrap.
if (!shim.isFunction(Base) || shim.isWrapped(Base)) {
return Base
}
// When es6 classes are being wrapped, we need to use an es6 class due to
// the fact our es5 wrapper depends on calling the constructor without `new`.
var wrapper = spec.es6 || /^class /.test(Base.toString())
? _es6WrapClass
: _es5WrapClass
return wrapper(shim, Base, fnName, spec, args)
})
}
/**
* Wraps the actual module being instrumented to change what `require` returns.
*
* - `wrapExport(nodule, spec)`
*
* @memberof Shim.prototype
*
* @param {*} nodule
* The original export to replace with our new one.
*
* @param {WrapFunction} spec
* A wrapper function. The return value from this spec is what will replace
* the export.
*
* @return {*} The return value from `spec`.
*/
function wrapExport(nodule, spec) {
return this._toExport = this.wrap(nodule, null, spec)
}
/**
* If the export was wrapped, that wrapper is returned, otherwise `defaultExport`.
*
* @private
* @memberof Shim.prototype
*
* @param {*} defaultExport - The original export in case it was never wrapped.
*
* @return {*} The result from calling {@link Shim#wrapExport} or `defaultExport`
* if it was never used.
*
* @see Shim.wrapExport
*/
function getExport(defaultExport) {
return this._toExport || defaultExport
}
/**
* Determines if the specified function or property exists and is wrapped.
*
* - `isWrapped(nodule, property)`
* - `isWrapped(func)`
*
* @memberof Shim.prototype
*
* @param {Object|Function} nodule
* The source for the property or a single function to check.
*
* @param {string} [property]
* The property to check. If omitted, the `nodule` parameter is assumed to be
* the function to check.
*
* @return {bool} True if the item exists and has been wrapped.
*
* @see Shim#wrap
* @see Shim#bindSegment
*/
function isWrapped(nodule, property) {
if (property) {
return !!(nodule && nodule[property] && nodule[property].__NR_original)
}
return !!(nodule && nodule.__NR_original)
}
/**
* Wraps a function with segment creation and binding.
*
* - `record(nodule, properties, recordNamer)`
* - `record(func, recordNamer)`
*
* This is shorthand for calling {@link Shim#wrap} and manually creating a segment.
*
* @memberof Shim.prototype
*
* @param {Object|Function} nodule
* The source for the properties to record, or a single function to record.
*
* @param {string|Array.<string>} [properties]
* One or more properties to record. If omitted, the `nodule` parameter is
* assumed to be the function to record.
*
* @param {RecorderFunction} recordNamer
* A function which returns a record descriptor that gives the name and type of
* record we'll make.
*
* @return {Object|Function} The first parameter, possibly wrapped.
*
* @see RecorderFunction
* @see RecorderSpec
* @see Shim#wrap
*/
function record(nodule, properties, recordNamer) {
if (this.isFunction(properties)) {
recordNamer = properties
properties = null
}
return this.wrap(nodule, properties, function makeWrapper(shim, fn, name) {
// Can't record things that aren't functions.
if (!shim.isFunction(fn)) {
shim.logger.debug('Not recording non-function "%s".', name)
return fn
}
shim.logger.trace('Wrapping "%s" with metric recording.', name)
return function wrapper() {
// Create the segment that will be recorded.
var args = argsToArray.apply(shim, arguments)
var segDesc = recordNamer.call(this, shim, fn, name, args)
if (!segDesc) {
shim.logger.trace('No segment descriptor for "%s", not recording.', name)
return fnApply.call(fn, this, args)
}
segDesc = new specs.RecorderSpec(segDesc)
// See if we're in an active transaction.
var parent
if (segDesc.parent) {
// We only want to continue recording in a transaction if the
// transaction is active.
parent = segDesc.parent.transaction.isActive() ? segDesc.parent : null
} else {
parent = shim.getActiveSegment()
}
if (!parent) {
shim.logger.debug('Not recording function %s, not in a transaction.', name)
return fnApply.call(fn, this, arguments)
}
if (
segDesc.callbackRequired &&
!_hasValidCallbackArg(shim, args, segDesc.callback)
) {
return fnApply.call(fn, this, arguments)
}
// Only create a segment if:
// - We are _not_ making an internal segment.
// - OR the parent segment is either not internal or not from this shim.
var shouldCreateSegment = !(
parent.opaque || (segDesc.internal && parent.internal && shim === parent.shim)
)
var segment = shouldCreateSegment ? _rawCreateSegment(shim, segDesc) : parent
return _doRecord.call(this, segment, args, segDesc, shouldCreateSegment)
}
function _hasValidCallbackArg(shim, args, specCallback) {
if (shim.isNumber(specCallback)) {
const cbIdx = normalizeIndex(args.length, specCallback)
if (cbIdx === null) {
return false
}
const callback = args[cbIdx]
return shim.isFunction(callback)
}
return true
}
function _doRecord(segment, args, segDesc, shouldCreateSegment) {
// Now bind any callbacks specified in the segment descriptor.
_bindAllCallbacks.call(this, shim, fn, name, args, {
spec: segDesc,
segment: segment,
shouldCreateSegment: shouldCreateSegment
})
// Apply the function, and (if it returned a stream) bind that too.
// The reason there is no check for `segment` is because it should
// be guaranteed by the parent and active transaction check
// at the beginning of this function.
var ret = _applyRecorderSegment(segment, this, args, segDesc)
if (ret) {
if (segDesc.stream) {
shim.logger.trace('Binding return value as stream.')
_bindStream(shim, ret, segment, {
event: shim.isString(segDesc.stream) ? segDesc.stream : null,
shouldCreateSegment: shouldCreateSegment
})
} else if (segDesc.promise && shim.isPromise(ret)) {
shim.logger.trace('Binding return value as Promise.')
ret = shim.bindPromise(ret, segment)
}
}
return ret
}
function _applyRecorderSegment(segment, ctx, args, segDesc) {
var error = null
var promised = false
var ret
try {
ret = shim.applySegment(fn, segment, true, ctx, args, segDesc.inContext)
if (segDesc.after && segDesc.promise && shim.isPromise(ret)) {
promised = true
return ret.then(function onThen(val) {
segment.touch()
segDesc.after(shim, fn, name, null, val)
return val
}, function onCatch(err) {
segment.touch()
segDesc.after(shim, fn, name, err, null)
throw err // NOTE: This is not an error from our instrumentation.
})
}
return ret
} catch (err) {
error = err
throw err // Just rethrowing this error, not our error!
} finally {
if (segDesc.after && (error || !promised)) {
segDesc.after(shim, fn, name, error, ret)
}
}
}
})
}
/**
* Unwraps one or more items, revealing the original value.
*
* - `unwrap(nodule, property)`
* - `unwrap(func)`
*
* If called with a `nodule` and properties, the unwrapped values will be put
* back on the nodule. Otherwise, the unwrapped function is just returned.
*
* @memberof Shim.prototype
*
* @param {Object|Function} nodule
* The source for the properties to unwrap, or a single function to unwrap.
*
* @param {string|Array.<string>} [properties]
* One or more properties to unwrap. If omitted, the `nodule` parameter is
* assumed to be the function to unwrap.
*
* @return {Object|Function} The first parameter after unwrapping.
*/
function unwrap(nodule, properties) {
// Don't try to unwrap potentially `null` or `undefined` things.
if (!nodule) {
return nodule
}
// If we're unwrapping multiple things
if (this.isArray(properties)) {
properties.forEach(unwrap.bind(this, nodule))
return nodule
}
this.logger.trace('Unwrapping %s', properties || '<nodule>')
var original = properties ? nodule[properties] : nodule
while (original && original.__NR_original) {
original = this.isFunction(original.__NR_unwrap)
? original.__NR_unwrap()
: original.__NR_original
}
return original
}
/**
* Unwraps one item, revealing the underlying value.
*
* - `unwrapOnce(nodule, property)`
* - `unwrapOnce(func)`
*
* If called with a `nodule` and properties, the unwrapped value will be put
* back on the nodule. Otherwise, the unwrapped function is just returned.
*
* @memberof Shim.prototype
*
* @param {Object|Function} nodule
* The source for the properties to unwrap, or a single function to unwrap.
*
* @param {string|Array.<string>} [properties]
* One or more properties to unwrap. If omitted, the `nodule` parameter is
* assumed to be the function to unwrap.
*
* @return {Object|Function} The first parameter after unwrapping.
*/
function unwrapOnce(nodule, properties) {
// Don't try to unwrap potentially `null` or `undefined` things.
if (!nodule) {
return nodule
}
// If we're unwrapping multiple things
if (this.isArray(properties)) {
properties.forEach(unwrapOnce.bind(this, nodule))
return nodule
}
this.logger.trace('Unwrapping %s', properties || '<nodule>')
var original = properties ? nodule[properties] : nodule
if (original && original.__NR_original) {
original = this.isFunction(original.__NR_unwrap)
? original.__NR_unwrap()
: original.__NR_original
}
return original
}
/**
* Retrieves the original method for a wrapped function.
*
* - `getOriginal(nodule, property)`
* - `getOriginal(func)`
*
* @memberof Shim.prototype
*
* @param {Object|Function} nodule
* The source of the property to get the original of, or a function to unwrap.
*
* @param {string} [property]
* A property on `nodule` to get the original value of.
*
* @return {Object|Function} The original value for the given item.
*/
function getOriginal(nodule, property) {
if (!nodule) {
return nodule
}
var original = property ? nodule[property] : nodule
while (original && original.__NR_original) {
original = original.__NR_original
}
return original
}
/**
* Binds the execution of a function to a single segment.
*
* - `bindSegment(nodule , property [, segment [, full]])`
* - `bindSegment(func [, segment [, full]])`
*
* If called with a `nodule` and a property, the wrapped property will be put
* back on the nodule. Otherwise, the wrapped function is just returned.
*
* @memberof Shim.prototype
*
* @param {Object|Function} nodule
* The source for the property or a single function to bind to a segment.
*
* @param {string} [property]
* The property to bind. If omitted, the `nodule` parameter is assumed
* to be the function to bind the segment to.
*
* @param {?TraceSegment} [segment=null]
* The segment to bind the execution of the function to. If omitted or `null`
* the currently active segment will be bound instead.
*
* @param {bool} [full=false]
* Indicates if the full lifetime of the segment is bound to this function.
*
* @return {Object|Function} The first parameter after wrapping.
*/
function bindSegment(nodule, property, segment, full) {
// Don't bind to null arguments.
if (!nodule) {
return nodule
}
// Determine our arguments.
if (this.isObject(property) && !this.isArray(property)) {
// bindSegment(func, segment [, full])
full = segment
segment = property
property = null
}
// This protects against the `bindSegment(func, null, true)` case, where the
// segment is `null`, and thus `true` (the full param) is detected as the
// segment.
if (segment != null && !this.isObject(segment)) {
this.logger.debug({segment: segment}, 'Segment is not a segment, not binding.')
return nodule
}
var wrapped = this.wrap(nodule, property, function wrapFunc(shim, func) {
if (!shim.isFunction(func)) {
return func
}
// Wrap up the function with this segment.
segment = segment || shim.getSegment()
if (!segment) {
return func
}
var binder = _makeBindWrapper(shim, func, segment, full || false)
shim.storeSegment(binder, segment)
return binder
})
return wrapped
}
/**
* Replaces the callback in an arguments array with one that has been bound to
* the given segment.
*
* - `bindCallbackSegment(args, cbIdx [, segment])`
* - `bindCallbackSegment(obj, property [, segment])`
*
* @memberof Shim.prototype
*
* @param {Array|Object} args
* The arguments array to pull the cb from.
*
* @param {number|string} cbIdx
* The index of the callback.
*
* @param {TraceSegment} [parentSegment]
* The segment to use as the callback segment's parent. Defaults to the
* currently active segment.
*
* @see Shim#bindSegment
*/
function bindCallbackSegment(args, cbIdx, parentSegment) {
if (!args) {
return
}
if (this.isNumber(cbIdx)) {
var normalizedCBIdx = normalizeIndex(args.length, cbIdx)
if (normalizedCBIdx === null) {
// Bad index.
this.logger.debug(
'Invalid index %d for args of length %d, not binding callback segment',
cbIdx, args.length
)
return
}
cbIdx = normalizedCBIdx
}
// Pull out the callback and make sure it is a function.
var cb = args[cbIdx]
if (this.isFunction(cb)) {
var shim = this
var realParent = parentSegment || shim.getSegment()
args[cbIdx] = shim.wrap(cb, null, function callbackWrapper(shim, fn, name) {
return function wrappedCallback() {
if (realParent) {
realParent.opaque = false
}
var segment = _rawCreateSegment(shim, new specs.SegmentSpec({
name: 'Callback: ' + name,
parent: realParent
}))
if (segment) {
segment.async = false
}
// CB may end the transaction so update the parent's time preemptively.
realParent && realParent.touch()
return shim.applySegment(cb, segment, true, this, arguments)
}
})
shim.storeSegment(args[cbIdx], realParent)
}
}
/**
* Retrieves the segment associated with the given object, or the current
* segment if no object is given.
*
* - `getSegment([obj])`
*
* @memberof Shim.prototype
*
* @param {*} [obj] - The object to retrieve a segment from.
*
* @return {?TraceSegment} The trace segment associated with the given object or
* the current segment if no object is provided or no segment is associated
* with the object.
*/
function getSegment(obj) {
if (obj && obj.__NR_segment) {
return obj.__NR_segment
}
return this.tracer.getSegment()
}
/**
* Retrieves the segment associated with the given object, or the currently
* active segment if no object is given.
*
* - `getActiveSegment([obj])`
*
* An active segment is one whose transaction is still active (e.g. has not
* ended yet).
*
* @memberof Shim.prototype
*
* @param {*} [obj] - The object to retrieve a segment from.
*
* @return {?TraceSegment} The trace segment associated with the given object or
* the currently active segment if no object is provided or no segment is
* associated with the object.
*/
function getActiveSegment(obj) {
var segment = this.getSegment(obj)
if (segment && segment.transaction && segment.transaction.isActive()) {
return segment
}
return null
}
/**
* Explicitly sets the active segment to the one passed in. This method
* should only be used if there is no function to tie a segment's timing
* to.
*
* - `setActiveSegment(segment)`
*
* @memberof Shim.prototype
*
* @param {TraceSegment} segment - The segment to set as the active segment.
*
*/
function setActiveSegment(segment) {
return this.tracer.segment = segment
}
/**
* Associates a segment with the given object.
*
* - `storeSegment(obj [, segment])`
*
* If no segment is provided, the currently active segment is used.
*
* @memberof Shim.prototype
*
* @param {!*} obj - The object to retrieve a segment from.
* @param {TraceSegment} [segment] - The segment to link the object to.
*/
function storeSegment(obj, segment) {
this.setInternalProperty(obj, '__NR_segment', segment || this.tracer.getSegment())
}
/**
* Sets the given segment as the active one for the duration of the function's
* execution.
*
* - `applySegment(func, segment, full, context, args[, inContextCB])`
*
* @memberof Shim.prototype
*
* @param {Function} func
* The function to execute in the context of the given segment.
*
* @param {TraceSegment} segment
* The segment to make active for the duration of the function.
*
* @param {bool} full
* Indicates if the full lifetime of the segment is bound to this function.
*
* @param {*} context
* The `this` argument for the function.
*
* @param {Array.<*>} args
* The arguments to be passed into the function.
*
* @param {Function} [inContextCB]
* The function used to do more instrumentation work. This function is
* guaranteed to be executed with the segment associated with.
*
*
* @return {*} Whatever value `func` returned.
*/
/* eslint-disable max-params */
function applySegment(func, segment, full, context, args, inContextCB) {
// Exist fast for bad arguments.
if (!this.isFunction(func)) {
return
}
if (!segment) {
this.logger.trace('No segment to apply to function.')
return fnApply.call(func, context, args)
}
this.logger.trace('Applying segment %s', segment.name)
// Set this segment as the current one on the tracer.
var tracer = this.tracer
var prevSegment = tracer.segment
tracer.segment = segment
if (full) {
segment.start()
}
if (typeof inContextCB === 'function') {
inContextCB()
}
// Execute the function and then return the tracer segment to the old one.
try {
return fnApply.call(func, context, args)
} catch (error) {
if (prevSegment === null && process.domain != null) {
process.domain.__NR_segment = tracer.segment
}
throw error // Rethrowing application error, this is not an agent error.
} finally {
if (full) {
segment.touch()
}
tracer.segment = prevSegment
}
}
/* eslint-enable max-params */
/**
* Creates a new segment.
*
* - `createSegment(opts)`
* - `createSegment(name [, recorder] [, parent])`
*
* @memberof Shim.prototype
*
* @param {string} name
* The name to give the new segment.
*
* @param {?Function} [recorder=null]
* Optional. A function which will record the segment as a metric. Default is
* to not record the segment.
*
* @param {TraceSegment} [parent]
* Optional. The segment to use as the parent. Default is to use the currently
* active segment.
*
* @return {?TraceSegment} A new trace segment if a transaction is active, else
* `null` is returned.
*/
function createSegment(name, recorder, parent) {
var opts = null
if (this.isString(name)) {
// createSegment(name [, recorder] [, parent])
opts = new specs.SegmentSpec({name})
// if the recorder arg is not used, it can either be omitted or null
if (this.isFunction(recorder) || this.isNull(recorder)) {
// createSegment(name, recorder [, parent])
opts.recorder = recorder
opts.parent = parent
} else {
// createSegment(name [, parent])
opts.parent = recorder
}
} else {
// createSegment(opts)
opts = name
}
return _rawCreateSegment(this, opts)
}
function _rawCreateSegment(shim, opts) {
// Grab parent segment when none in opts so we can check opaqueness
opts.parent = opts.parent || shim.getActiveSegment()
// When parent exists and is opaque, no new segment will be created
// by tracer.createSegment and the parent will be returned. We bail
// out early so we do not risk modifying the parent segment.
if (opts.parent && opts.parent.opaque) {
shim.logger.trace(opts, 'Did not create segment because parent is opaque')
return opts.parent
}
var segment = shim.tracer.createSegment(opts.name, opts.recorder, opts.parent)
if (segment) {
segment.internal = opts.internal
segment.opaque = opts.opaque
segment.shim = shim
if (hasOwnProperty(opts, 'parameters')) {
shim.copySegmentParameters(segment, opts.parameters)
}
shim.logger.trace(opts, 'Created segment')
} else {
shim.logger.debug(opts,'Failed to create segment')
}
return segment
}
/**
* Determine the name of an object.
*
* @memberof Shim.prototype
*
* @param {*} obj - The object to get a name for.
*
* @return {string} The name of the object if it has one, else `<anonymous>`.
*/
function getName(obj) {
return String((!obj || obj === true) ? obj : (obj.name || '<anonymous>'))
}
/**
* Determines if the given object is an Object.
*
* @memberof Shim.prototype
*
* @param {*} obj - The object to check.
*
* @return {bool} True if the object is an Object, else false.
*/
function isObject(obj) {
return obj instanceof Object
}
/**
* Determines if the given object exists and is a function.
*
* @memberof Shim.prototype
*
* @param {*} obj - The object to check.
*
* @return {bool} True if the object is a function, else false.
*/
function isFunction(obj) {
return typeof obj === 'function'
}
/**
* Determines if the given object exists and is a string.
*
* @memberof Shim.prototype
*
* @param {*} obj - The object to check.
*
* @return {bool} True if the object is a string, else false.
*/
function isString(obj) {
return typeof obj === 'string'
}
/**
* Determines if the given object is a number literal.
*
* @memberof Shim.prototype
*
* @param {*} obj - The object to check.
*
* @return {bool} True if the object is a number literal, else false.
*/
function isNumber(obj) {
return typeof obj === 'number'
}
/**
* Determines if the given object is a boolean literal.
*
* @memberof Shim.prototype
*
* @param {*} obj - The object to check.
*
* @return {bool} True if the object is a boolean literal, else false.
*/
function isBoolean(obj) {
return typeof obj === 'boolean'
}
/**
* Determines if the given object exists and is an array.
*
* @memberof Shim.prototype
*
* @param {*} obj - The object to check.
*
* @return {bool} True if the object is an array, else false.
*/
function isArray(obj) {
return obj instanceof Array
}
/**
* Determines if the given object is a promise instance.
*
* @memberof Shim.prototype
*
* @param {*} obj - The object to check.
*
* @return {bool} True if the object is a promise, else false.
*/
function isPromise(obj) {
return obj && typeof obj.then === 'function'
}
/**
* Determines if the given value is null.
*
* @memberof Shim.prototype
*
* @param {*} val - The value to check.
*
* @return {bool} True if the value is null, else false.
*/
function isNull(val) {
return val === null
}
/**
* Converts an array-like object into an array.
*
* @memberof Shim.prototype
*
* @param {*} obj - The array-like object (i.e. `arguments`).
*
* @return {Array.<*>} An instance of `Array` containing the elements of the
* array-like.
*/
function toArray(obj) {
var len = obj.length
var arr = new Array(len)
for (var i = 0; i < len; ++i) {
arr[i] = obj[i]
}
return arr
}
/**
* Like {@link Shim#toArray}, but converts `arguments` to an array.
*
* This is the preferred function, when used with `.apply`, for converting the
* `arguments` object into an actual `Array` as it will not cause deopts.
*
* @memberof Shim.prototype
*
* @return {Array} An array containing the elements of `arguments`.
*
* @see Shim#toArray
* @see https://github.com/petkaantonov/bluebird/wiki/Optimization-killers
*/
function argsToArray() {
var len = arguments.length
var arr = new Array(len)
for (var i = 0; i < len; ++i) {
arr[i] = arguments[i]
}
return arr
}
/**
* Ensures the given index is a valid index inside the array.
*
* A negative index value is converted to a positive one by adding it to the
* array length before checking it.
*
* @memberof Shim.prototype
*
* @param {number} arrayLength - The length of the array this index is for.
* @param {number} idx - The index to normalize.
*
* @return {?number} The adjusted index value if it is valid, else `null`.
*/
function normalizeIndex(arrayLength, idx) {
if (idx < 0) {
idx = arrayLength + idx
}
return (idx < 0 || idx >= arrayLength) ? null : idx
}
/**
* Wraps a function such that it will only be executed once.
*
* @memberof Shim.prototype
*
* @param {function} fn - The function to wrap in an execution guard.
*
* @return {function} A function which will execute `fn` at most once.
*/
function once(fn) {
var called = false
return function onceCaller() {
if (!called) {
called = true
return fn.apply(this, arguments)
}
}
}
/**
* Sets a property to the given value. If the property doesn't exist yet it will
* be made writable and non-enumerable.
*
* @memberof Shim.prototype
*
* @param {!object} obj - The object to add the property to.
*