newrelic
Version:
New Relic agent
458 lines (413 loc) • 14.6 kB
JavaScript
/*
* Copyright 2023 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
const {
assignError,
getTransactionInfo,
isError,
makeGetReq,
MIDDLEWARE_TYPE_NAMES
} = require('./common')
const { assignCLMSymbol } = require('../../util/code-level-metrics')
const { RecorderSpec } = require('../specs')
const makeMiddlewareRecorder = require('../../metrics/recorders/middleware')
const MIDDLEWARE_TYPE_DETAILS = {
APPLICATION: { name: 'Mounted App: ', path: true, record: false },
ERRORWARE: { name: '', path: false, record: true },
MIDDLEWARE: { name: '', path: false, record: true },
PARAMWARE: { name: '', path: false, record: true },
ROUTE: { name: 'Route Path: ', path: true, record: false },
ROUTER: { name: 'Router: ', path: true, record: false }
}
/**
* Retrieves the route from the spec
*
* @private
* @param {object} spec middleware spec
* @param {Shim} shim instance of shim
* @returns {string} route route path
*/
function getRoute(spec, shim) {
let route = spec.route || '/'
if (shim.isFunction(route)) {
route = route()
}
if (route instanceof RegExp) {
route = '/' + route.source + '/'
} else if (shim.isArray(route)) {
route = route.join(',')
} else if (route[0] !== '/') {
route = '/' + route
}
return route
}
/**
* Retrieves the parameters from the spec.params.
* When a `transaction.finalizeNameFromWeb` is called, it takes these params
* and prefixes `request.parameters.route.` to every route param.
* Query params are handled in lib/transaction/index.js#initializeWeb
*
* @private
* @param {object} params object passed to fn
* @param {object} params.spec middleware spec
* @param {Shim} params.shim instance of shim
* @param {Function} params.fn middleware function
* @param {string} params.fnName function name
* @param {Array} params.args arguments passed to middleware function
* @param {object} params.req request object
* @returns {object} parameters object
*/
function copyParams({ spec, shim, fn, fnName, args, req }) {
// Copy over route parameters onto the transaction root.
return shim.agent.config.high_security
? null
: spec.params.call(this, shim, fn, fnName, args, req)
}
/**
* Creates the middleware recorder if the type specifies this flag
*
* @private
* @param {object} params object passed to fn
* @param {object} params.txInfo transaction
* @param {object} params.typeDetails metadata about the middleware type
* @param {string} params.metricName metric name for middleware function
* @returns {Function} recorder for middleware type
*/
function constructRecorder({ txInfo, typeDetails, metricName }) {
let recorder = null
if (typeDetails.record) {
const stackPath = txInfo.transaction.nameState.getPath() || ''
recorder = makeMiddlewareRecorder(metricName + '/' + stackPath)
}
return recorder
}
/**
* Updates nameState and errorHandled property of transaction info
*
* @private
* @param {object} params object passed to fn
* @param {Shim} params.shim instance of shim
* @param {string} params.fnName function name
* @param {string} params.route route path
* @param {object} params.req request object
* @param {boolean} params.isErrorWare indicates if it is error middleware
* @returns {object| null} updated transaction info
*/
function assignTxInfo({ shim, req, route, fnName, isErrorWare }) {
// Fetch the transaction information from that request.
const txInfo = getTransactionInfo(shim, req)
if (!txInfo || !txInfo.transaction) {
shim.logger.debug(
{ txInfo },
'Could not get transaction info in %s (%s)',
route,
fnName
)
return null
}
txInfo.transaction.nameState.setPrefix(shim._metrics.FRAMEWORK)
txInfo.errorHandled |= isErrorWare
return txInfo
}
/**
* Recorder for middleware that is a callback
*
* When called it will update transaction names, create a recorder for the middleware functions,
* assign parameters from request, and name the segment
*
* @private
* @param {object} params fn params
* @param {object} params.spec middleware spec
* @param {object} params.typeDetails metadata about the middleware type
* @param {string} params.metricName metric name for middleware function
* @param {boolean} params.isErrorWare flag indicating if errors are handled by function
* @returns {Function} recorder function
*/
function middlewareWithCallbackRecorder({ spec, typeDetails, metricName, isErrorWare }) {
return function callbackRecorder(shim, fn, fnName, args) {
const route = getRoute(spec, shim)
// Pull out the request object.
const req = getReq(spec, shim).call(this, shim, fn, fnName, args)
const txInfo = assignTxInfo({ shim, req, route, fnName, isErrorWare })
if (!txInfo || !txInfo.transaction) {
return null
}
const params = copyParams.call(this, { spec, shim, fn, fnName, args, req })
const nextWrapper = wrapNextHandler({
shim,
spec,
route,
args,
isErrorWare,
isPromise: false,
txInfo
})
// Append this middleware's mount point if it's not an errorware...
// (to avoid doubling up, a la 'WebTransaction/Expressjs/GET//test/test')
if (!isErrorWare && spec.appendPath) {
txInfo.transaction.nameState.appendPath(route, params)
}
const recorder = constructRecorder({ txInfo, typeDetails, metricName })
const segmentName = getSegmentName(metricName, typeDetails, route)
// Finally, return the segment descriptor.
return new RecorderSpec({
name: segmentName,
callback: nextWrapper,
parent: txInfo.segmentStack[txInfo.segmentStack.length - 1],
recorder,
after: function afterExec({ shim, error }) {
const errIsError = isError(shim, error)
if (errIsError) {
assignError(txInfo, error)
} else if (!nextWrapper && !isErrorWare && spec.appendPath) {
txInfo.transaction.nameState.popPath(route)
}
if (errIsError || !nextWrapper) {
txInfo.segmentStack.pop()
}
}
})
}
}
/**
* Recorder for middleware that is a promise
*
* When called it will update transaction names, create a recorder for the middleware functions,
* assign parameters from request, and name the segment
*
* @private
* @param {object} params fn params
* @param {object} params.spec middleware spec
* @param {object} params.typeDetails metadata about the middleware type
* @param {string} params.metricName metric name for middleware function
* @param {boolean} params.isErrorWare flag indicating if errors are handled by function
* @returns {Function} recorder function
*/
function middlewareWithPromiseRecorder({ spec, typeDetails, metricName, isErrorWare }) {
return function promiseRecorder(shim, fn, fnName, args) {
const route = getRoute(spec, shim)
// Pull out the request object.
const req = getReq(spec, shim).call(this, shim, fn, fnName, args)
const txInfo = assignTxInfo({ shim, req, route, fnName, isErrorWare })
if (!txInfo || !txInfo.transaction) {
return null
}
const params = copyParams.call(this, { spec, shim, fn, fnName, args, req })
// Append this middleware's mount point and possibly construct a recorder.
if (spec.appendPath) {
txInfo.transaction.nameState.appendPath(route, params)
}
const recorder = constructRecorder({ txInfo, typeDetails, metricName })
const nextWrapper = wrapNextHandler({
shim,
spec,
route,
args,
isErrorWare,
isPromise: true,
txInfo
})
const segmentName = getSegmentName(metricName, typeDetails, route)
// Finally, return the segment descriptor.
return new RecorderSpec({
name: segmentName,
parent: txInfo.segmentStack[txInfo.segmentStack.length - 1],
promise: spec.promise,
callback: nextWrapper,
recorder,
after: function afterExec({ shim, error, result }) {
if (shim._responsePredicate(args, result)) {
txInfo.transaction.nameState.freeze()
}
if (isError(shim, error)) {
assignError(txInfo, error)
} else {
txInfo.errorHandled = true
if (spec.appendPath) {
txInfo.transaction.nameState.popPath(route)
}
}
txInfo.segmentStack.pop()
}
})
}
}
/**
* Constructs segment name passed on path/route information
*
* @private
* @param {string} metricName metric name for middleware function
* @param {object} typeDetails metadata about the middleware type
* @param {string} route route path
* @returns {string} name given to segment
*/
function getSegmentName(metricName, typeDetails, route) {
let segmentName = metricName
if (typeDetails.path) {
segmentName += route
} else if (route.length > 1) {
segmentName += '/' + route
}
return segmentName
}
/**
* Retrieves the req function that is called to retrieve the request
*
* @private
* @param {object} spec middleware spec
* @param {Shim} shim instance of shim
* @returns {Function} function to call to obtain request object
*/
function getReq(spec, shim) {
return shim.isFunction(spec.req) ? spec.req : makeGetReq(shim, spec.req)
}
/**
* Wraps the given function in a middleware recorder function.
*
* @private
* @param {Shim} shim instance of shim
* The shim used for this instrumentation.
* @param {Function} middleware
* The middleware function to record.
* @param {object} spec
* The spec describing the middleware.
* @returns {Function} The middleware function wrapped in a recorder.
*/
module.exports._recordMiddleware = function _recordMiddleware(shim, middleware, spec) {
const typeDetails = MIDDLEWARE_TYPE_DETAILS[spec.type]
const isErrorWare = spec.type === MIDDLEWARE_TYPE_NAMES.ERRORWARE
const name = spec.name || shim.getName(shim.getOriginal(middleware))
let metricName = shim._metrics.PREFIX + typeDetails.name
if (typeDetails.record) {
metricName = shim._metrics.MIDDLEWARE + metricName + name
}
assignCLMSymbol(shim, middleware)
return shim.record(
middleware,
spec.promise
? middlewareWithPromiseRecorder({ spec, typeDetails, metricName, isErrorWare })
: middlewareWithCallbackRecorder({ spec, typeDetails, metricName, isErrorWare })
)
}
/**
* Wrap the `next` middleware function and push on our name state if we find it. We only want to
* push the name state if there is a next so that we can safely remove it
* if context leaves this middleware.
*
* @param {object} params params object
* @param {Shim} params.shim shim instance
* @param {object} params.spec middleware spec, should contain a next function
* @param {string} params.route route
* @param {Array} params.args arguments
* @param {boolean} params.isErrorWare is this error ware
* @param {boolean} params.isPromise is this a promise
* @param {object} params.txInfo transaction information
* @private
*/
function wrapNextHandler({ shim, spec, route, args, isErrorWare, isPromise, txInfo }) {
let nextWrapper = null
if (isPromise) {
nextWrapper = function pushSegment(_shim, _fn, _name, segment) {
txInfo.segmentStack.push(segment)
}
}
if (shim.isFunction(spec.next)) {
const nextDetails = {
route,
wrapNext: spec.next,
isErrorWare,
isPromise,
appendPath: spec.appendPath
}
nextWrapper = _makeNextBinder(nextDetails, txInfo)
} else {
const nextIdx = shim.normalizeIndex(args.length, spec.next)
if (nextIdx !== null && args[nextIdx] instanceof Function) {
const nextDetails = {
route,
wrapNext: function wrapNext(s, f, n, _args, wrap) {
wrap(_args, nextIdx)
},
isErrorWare,
isPromise,
appendPath: spec.appendPath
}
nextWrapper = _makeNextBinder(nextDetails, txInfo)
}
}
return nextWrapper
}
/**
* Assigns the active segment to stack and wraps the next function
*
* @private
* @param {object} nextDetails details about the function
* @param {object} txInfo transaction
* @returns {Function} wrapped function
*/
function _makeNextBinder(nextDetails, txInfo) {
return function bindNext(shim, fn, _name, segment, args) {
if (!segment) {
return
}
txInfo.segmentStack.push(segment)
nextDetails.wrapNext(
shim,
fn,
_name,
args,
wrapNextFn.bind(null, { shim, txInfo, nextDetails, segment })
)
}
}
/**
*
* Called from outside to wrap functions that could be called to continue to the next middleware
*
* @private
* @param {object} params params as 1st arg
* @param {object} params.shim instance of shim
* @param {object} params.txInfo transaction
* @param {object} params.nextDetails details about the function
* @param {object} params.segment active segment
* @param {Function} nodule module to wrap
* @param {string} property name of function to wrap
* @param {boolean} isFinal flag to indicate last route segment
*/
function wrapNextFn({ shim, txInfo, nextDetails, segment }, nodule, property, isFinal) {
shim.wrap(nodule, property, function wrapper(shim, original) {
const parentSegment = segment || shim.getSegment()
return shim.bindSegment(function boundNext(err) {
// Only pop the stack if we didn't error. This way the transaction
// name is derived from the failing middleware.
if (isError(shim, err)) {
assignError(txInfo, err)
} else if (!isFinal && !nextDetails.isErrorWare && nextDetails.appendPath) {
txInfo.transaction.nameState.popPath(nextDetails.route)
}
// The next call does not signify the end of the segment
// calling next in the promise case. Keep the segment on the
// stack and wait for its promise to be resolved to end it.
if (!nextDetails.isPromise) {
txInfo.segmentStack.pop()
segment.end()
}
const ret = original.apply(this, arguments)
if (nextDetails.isPromise && shim.isPromise(ret)) {
// After the next call has resolved, we should reinstate the
// segment responsible for calling next in case there is
// more work to do in that scope.
return ret.then(function onNextFinish(v) {
if (nextDetails.appendPath) {
txInfo.transaction.nameState.appendPath(nextDetails.route)
}
txInfo.segmentStack.push(segment)
return v
})
}
return ret
}, parentSegment) // Bind to parent.
})
}