UNPKG

newrelic

Version:
1,185 lines (1,074 loc) 35.6 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' var genericRecorder = require('../metrics/recorders/generic') var logger = require('../logger.js').child({component: 'WebFrameworkShim'}) var metrics = require('../metrics/names') var TransactionShim = require('./transaction-shim') var Shim = require('./shim') var specs = require('./specs') var urltils = require('../util/urltils') var util = require('util') /** * An enumeration of well-known web frameworks so that new instrumentations can * use the same names we already use for first-party instrumentation. * * Each of these values is also exposed directly on the WebFrameworkShim class * as static members. * * @readonly * @memberof WebFrameworkShim * @enum {string} */ var FRAMEWORK_NAMES = { CONNECT: 'Connect', DIRECTOR: 'Director', EXPRESS: 'Expressjs', HAPI: 'Hapi', KOA: 'Koa', RESTIFY: 'Restify', FASTIFY: 'Fastify' } var 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} } var MIDDLEWARE_TYPE_NAMES = { APPLICATION: 'APPLICATION', ERRORWARE: 'ERRORWARE', MIDDLEWARE: 'MIDDLEWARE', PARAMWARE: 'PARAMWARE', ROUTE: 'ROUTE', ROUTER: 'ROUTER' } /** * Name of the key used to store transaction information on `req` and `res`. * * @private */ var TRANSACTION_INFO_KEY = '__NR_transactionInfo' /** * Constructs a shim associated with the given agent instance, specialized for * instrumenting web frameworks. * * @constructor * @extends TransactionShim * @classdesc * A helper class for wrapping web framework modules. * * @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. * * @param {string} [frameworkId] * The name of the web framework being instrumented. If available, use one of * the values from {@link WebFrameworkShim.FRAMEWORK_NAMES}. * * @see TransactionShim * @see WebFrameworkShim.FRAMEWORK_NAMES */ function WebFrameworkShim(agent, moduleName, resolvedName, frameworkId) { TransactionShim.call(this, agent, moduleName, resolvedName) this._logger = logger.child({module: moduleName}) if (frameworkId) { this.setFramework(frameworkId) } this._routeParser = _defaultRouteParser this._errorPredicate = _defaultErrorPredicate this._responsePredicate = _defaultResponsePredicate } module.exports = WebFrameworkShim util.inherits(WebFrameworkShim, TransactionShim) // Add constants on the shim for the well-known frameworks. WebFrameworkShim.FRAMEWORK_NAMES = FRAMEWORK_NAMES Object.keys(FRAMEWORK_NAMES).forEach(function defineWebFrameworkMetricEnum(fwName) { Shim.defineProperty(WebFrameworkShim, fwName, FRAMEWORK_NAMES[fwName]) Shim.defineProperty(WebFrameworkShim.prototype, fwName, FRAMEWORK_NAMES[fwName]) }) WebFrameworkShim.MIDDLEWARE_TYPE_NAMES = MIDDLEWARE_TYPE_NAMES Object.keys(MIDDLEWARE_TYPE_NAMES).forEach(function defineMiddlewareTypeEnum(mtName) { Shim.defineProperty(WebFrameworkShim, mtName, MIDDLEWARE_TYPE_NAMES[mtName]) Shim.defineProperty(WebFrameworkShim.prototype, mtName, MIDDLEWARE_TYPE_NAMES[mtName]) }) WebFrameworkShim.prototype.setRouteParser = setRouteParser WebFrameworkShim.prototype.setFramework = setFramework WebFrameworkShim.prototype.setTransactionUri = setTransactionUri WebFrameworkShim.prototype.wrapMiddlewareMounter = wrapMiddlewareMounter WebFrameworkShim.prototype.recordParamware = recordParamware WebFrameworkShim.prototype.recordMiddleware = recordMiddleware WebFrameworkShim.prototype.recordRender = recordRender WebFrameworkShim.prototype.noticeError = noticeError WebFrameworkShim.prototype.errorHandled = errorHandled WebFrameworkShim.prototype.setErrorPredicate = setErrorPredicate WebFrameworkShim.prototype.setResponsePredicate = setResponsePredicate WebFrameworkShim.prototype.savePossibleTransactionName = savePossibleTransactionName WebFrameworkShim.prototype.captureUrlParams = captureUrlParams // -------------------------------------------------------------------------- // /** * @callback RouteParserFunction * * @summary * Called whenever new middleware are mounted using the instrumented framework, * this method should pull out a representation of the mounted path. * * @param {WebFrameworkShim} shim * The shim in use for this instrumentation. * * @param {function} fn * The function which received this route string/RegExp. * * @param {string} fnName * The name of the function to which this route was given. * * @param {string|RegExp} route * The route that was given to the function. * * @return {string|RegExp} The mount point from the given route. */ /** * @callback RouteRequestFunction * * @summary * Extracts the request object from the arguments to the middleware function. * * @param {WebFrameworkShim} shim - The shim used for instrumentation. * @param {function} fn - The middleware function. * @param {string} fnName - The name of the middleware function. * @param {Array} args - The arguments to the middleware function. * * @return {Object} The request object. */ /** * @callback RouteNextFunction * * @summary * Used to wrap functions that users can call to continue to the next middleware. * * @param {WebFrameworkShim} shim - The shim used for instrumentation. * @param {function} fn - The middleware function. * @param {string} fnName - The name of the middleware function. * @param {Array} args - The arguments to the middleware function. * @param {NextWrapperFunction} wrap - A function to wrap an individual next function. * * @return {Object} The request object. */ /** * @callback RouteParameterFunction * * @summary * Extracts the route parameters from the arguments to the middleware function. * * @param {WebFrameworkShim} shim - The shim used for instrumentation. * @param {function} fn - The middleware function. * @param {string} fnName - The name of the middleware function. * @param {Array} args - The arguments to the middleware function. * * @return {Object} A map of route parameter names to values. */ /** * @callback MiddlewareWrapperFunction * * @summary * Called for each middleware passed to a mounting method. Should perform the * wrapping of the middleware. * * @param {WebFrameworkShim} shim * The shim used for instrumentation. * * @param {function} middleware * The middleware function to wrap. * * @param {string} fnName * The name of the middleware function. * * @param {string} [route=null] * The route the middleware is mounted on if one was found. * * @see WebFrameworkShim#recordMiddleware * @see WebFrameworkShim#recordParamware */ /** * @interface MiddlewareSpec * * @description * Describes the interface for middleware functions with this instrumentation. * * @property {number|RouteRequestFunction} [req=shim.FIRST] * Indicates which argument to the middleware is the request object. It can also be * a function to extract the request object from the middleware arguments. * * @property {number} [res=shim.SECOND] * Indicates which argument to the middleware is the response object. * * @property {number|RouteNextFunction} [next=shim.THIRD] * Indicates which argument to the middleware function is the callback. When it is * a function, it will be called with the arguments of the middleware and a function * for wrapping calls that represent continuation from the current middleware. * * @property {string} [name] * The name to use for this middleware. Defaults to `middleware.name`. * * @property {RouteParameterFunction} [params] * A function to extract the route parameters from the middleware arguments. * Defaults to using `req.params`. * * @property {string} [type='MIDDLEWARE'] * * @property {string|function} [route=null] * Route/path used for naming segments and transaction name candidates. If a function, * will be invoked just before segment creation with middleware invocation. * * @property {boolean} [appendPath=true] * Indicates that the path associated with the middleware should be appended * and popped from the stack of name candidates. */ /** * @interface MiddlewareMounterSpec * * @description * Describes the arguments provided to mounting methods (e.g. `app.post()`). * * @property {number|string} [route=null] * Tells which argument may be the mounting path for the other arguments. If * the indicated argument is a function it is assumed the route was not provided * and the indicated argument is a middleware function. If a string is provided * it will be used as the mounting path. * * @property {MiddlewareWrapperFunction} [wrapper] * A function to call for each middleware function passed to the mounter. */ /** * @interface RenderSpec * @extends RecorderSpec * * @description * Describes the interface for render methods. * * @property {number} [view=shim.FIRST] * Identifies which argument is the name of the view being rendered. Defaults * to {@link Shim#ARG_INDEXES shim.FIRST}. * * @see SegmentSpec * @see RecorderSpec */ // -------------------------------------------------------------------------- // /** * Sets the function used to convert the route handed to middleware-adding * methods into a string. * * - `setRouteParser(parser)` * * @memberof WebFrameworkShim.prototype * * @param {RouteParserFunction} parser - The parser function to use. */ function setRouteParser(parser) { if (!this.isFunction(parser)) { return this.logger.debug('Given route parser is not a function.') } this._routeParser = parser } /** * Sets the name of the web framework in use by the server to the one given. * * - `setFramework(framework)` * * This should be the first thing the instrumentation does. * * @memberof WebFrameworkShim.prototype * * @param {WebFrameworkShim.FRAMEWORK_NAMES|string} framework * The name of the framework. * * @see WebFrameworkShim.FRAMEWORK_NAMES */ function setFramework(framework) { this._metrics = { PREFIX: framework + '/', FRAMEWORK: framework, MIDDLEWARE: metrics.MIDDLEWARE.PREFIX } this.agent.environment.setFramework(framework) this._logger = this._logger.child({framework: framework}) this.logger.trace({metrics: this._metrics}, 'Framework metric names set') } /** * Sets the URI path to be used for naming the transaction currently in scope. * * @memberof WebFrameworkShim.prototype * * @param {string} uri - The URI path to use for the transaction. */ function setTransactionUri(uri) { var tx = this.tracer.getTransaction() if (!tx) { return } tx.nameState.setName( this._metrics.FRAMEWORK, tx.verb, metrics.ACTION_DELIMITER, uri ) } /** * Records calls to methods used for rendering views. * * - `recordRender(nodule, properties [, spec])` * - `recordRender(func [, spec])` * * @memberof WebFrameworkShim.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 {RenderSpec} [spec] * The spec for wrapping the render method. * * @return {Object|Function} The first parameter to this function, after * wrapping it or its properties. */ function recordRender(nodule, properties, spec) { if (this.isObject(properties) && !this.isArray(properties)) { // recordRender(func, spec) spec = properties properties = null } spec = this.setDefaults(spec, { view: this.FIRST, callback: null, promise: null }) return this.record(nodule, properties, function renderRecorder(shim, fn, name, args) { var viewIdx = shim.normalizeIndex(args.length, spec.view) if (viewIdx === null) { shim.logger.debug( 'Invalid spec.view (%d vs %d), not recording.', spec.view, args.length ) return null } return { name: metrics.VIEW.PREFIX + args[viewIdx] + metrics.VIEW.RENDER, callback: spec.callback, promise: spec.promise, recorder: genericRecorder, // Hidden class stuff rowCallback: null, stream: null, internal: false } }) } /** * Wraps a method that is used to add middleware to a server. The middleware * can then be recorded as metrics. * * - `wrapMiddlewareMounter(nodule, properties [, spec])` * - `wrapMiddlewareMounter(func [, spec])` * * @memberof WebFrameworkShim.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 {MiddlewareMounterSpec} [spec] * Spec describing the parameters for this middleware mount point. * * @return {Object|Function} The first parameter to this function, after * wrapping it or its properties. * * @see WebFrameworkShim#recordMiddleware */ function wrapMiddlewareMounter(nodule, properties, spec) { if (properties && !this.isString(properties) && !this.isArray(properties)) { // wrapMiddlewareMounter(func, spec) spec = properties properties = null } if (this.isFunction(spec)) { // wrapMiddlewareMounter(nodule [, properties], wrapper) spec = {wrapper: spec} } spec = this.setDefaults(spec, { route: null, endpoint: null }) var wrapSpec = { wrapper: function wrapMounter(shim, fn, fnName) { if (!shim.isFunction(fn)) { return fn } return function wrappedMounter() { var args = shim.argsToArray.apply(shim, arguments) // Normalize the route index and pull out the route argument if provided. var routeIdx = null var route = null if (shim.isNumber(spec.route)) { routeIdx = shim.normalizeIndex(args.length, spec.route) route = routeIdx === null ? null : args[routeIdx] const isArrayOfFunctions = shim.isArray(route) && shim.isFunction(route[0]) if (shim.isFunction(route) || isArrayOfFunctions) { routeIdx = null route = null } else if (shim.isArray(route)) { route = route.map((routeArg) => { return shim._routeParser.call(this, shim, fn, fnName, routeArg) }) } else { route = shim._routeParser.call(this, shim, fn, fnName, route) } } else if (spec.route !== null) { route = shim._routeParser.call(this, shim, fn, fnName, spec.route) } _wrapMiddlewares.call(this, routeIdx, args) function _wrapMiddlewares(_routeIdx, middlewares) { for (let i = 0; i < middlewares.length; ++i) { // If this argument is the route argument skip it. if (i === _routeIdx) { continue } // Some platforms accept an arbitrarily nested array of middlewares, // so if this argument is an array we must recurse into it. var middleware = middlewares[i] if (middleware instanceof Array) { _wrapMiddlewares(null, middleware) continue } middlewares[i] = spec.wrapper.call( this, shim, middleware, shim.getName(middleware), route ) } } return fn.apply(this, args) } } } _copyExpectedSpecParameters(wrapSpec, spec) return this.wrap(nodule, properties, wrapSpec) } /** * Records the provided function as a middleware. * * - `recordMiddleware(nodule, properties [, spec])` * - `recordMiddleware(func [, spec])` * * @memberof WebFrameworkShim.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 {MiddlewareSpec} [spec] * The spec for wrapping the middleware. * * @return {Object|Function} The first parameter to this function, after * wrapping it or its properties. * * @see WebFrameworkShim#wrapMiddlewareMounter */ function recordMiddleware(nodule, properties, spec) { if (this.isObject(properties) && !this.isArray(properties)) { // recordMiddleware(func, spec) spec = properties properties = null } spec = spec || Object.create(null) var mwSpec = new specs.MiddlewareSpec(spec) var wrapSpec = new specs.WrapSpec(function wrapMiddleware(shim, middleware) { return _recordMiddleware(shim, middleware, mwSpec) }) _copyExpectedSpecParameters(wrapSpec, spec) return this.wrap(nodule, properties, wrapSpec) } /** * Records the provided function as a paramware. * * - `recordParamware(nodule, properties [, spec])` * - `recordParamware(func [, spec])` * * Paramware are specialized middleware that execute when certain route * parameters are encountered. For example, the route `/users/:userId` could * trigger a paramware hooked to `userId`. * * For every new request that comes in, this should be called as early in the * processing as possible. * * @memberof WebFrameworkShim.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 {MiddlewareSpec} [spec] * The spec for wrapping the middleware. * * @return {Object|Function} The first parameter to this function, after * wrapping it or its properties. */ function recordParamware(nodule, properties, spec) { if (this.isObject(properties) && !this.isArray(properties)) { // recordParamware(func, spec) spec = properties properties = null } spec = spec || Object.create(null) var mwSpec = new specs.MiddlewareSpec(spec) if (spec && this.isString(spec.name)) { mwSpec.route = '[param handler :' + spec.name + ']' } else { mwSpec.route = '[param handler]' } mwSpec.type = MIDDLEWARE_TYPE_NAMES.PARAMWARE var wrapSpec = new specs.WrapSpec(function wrapParamware(shim, middleware, name) { mwSpec.name = name return _recordMiddleware(shim, middleware, mwSpec) }) _copyExpectedSpecParameters(wrapSpec, spec) return this.wrap(nodule, properties, wrapSpec) } /** * Tells the shim that the given request has caused an error. * * The given error will be checked for truthiness and if it passes the error * predicate check before being held onto. * * Use {@link WebFrameworkShim#errorHandled} to unnotice an error if it is later * caught by the user. * * @memberof WebFrameworkShim.prototype * * @param {Request} req - The request which caused the error. * @param {*?} err - The error which has occurred. * * @see WebFrameworkShim#errorHandled * @see WebFrameworkShim#setErrorPredicate */ function noticeError(req, err) { var txInfo = _getTransactionInfo(this, req) if (txInfo && _isError(this, err)) { _noticeError(this, txInfo, err) } } /** * Indicates that the given error has been handled for this request. * * @memberof WebFrameworkShim.prototype * * @param {Request} req - The request which caused the error. * @param {*} err - The error which has been handled. * * @see WebFrameworkShim#noticeError * @see WebFrameworkShim#setErrorPredicate */ function errorHandled(req, err) { var txInfo = _getTransactionInfo(this, req) if (txInfo && txInfo.error === err) { txInfo.errorHandled = true } } /** * Sets a function to call when an error is noticed to determine if it is really * an error. * * @memberof WebFrameworkShim.prototype * * @param {function(object): bool} pred * Function which should return true if the object passed to it is considered * an error. * * @see WebFrameworkShim#noticeError * @see WebFrameworkShim#errorHandled */ function setErrorPredicate(pred) { this._errorPredicate = pred } /** * Marks the current path as a potential responder. * * @memberof WebFrameworkShim.prototype * * @param {Request} req - The request which caused the error. */ function savePossibleTransactionName(req) { var txInfo = _getTransactionInfo(this, req) if (txInfo && txInfo.transaction) { txInfo.transaction.nameState.markPath() } } /** * Sets a function to call with the result of a middleware to determine if it has * responded. * * @memberof WebFrameworkShim.prototype * * @param {function(args, object): bool} pred * Function which should return true if the object passed to it is considered * a response. */ function setResponsePredicate(pred) { this._responsePredicate = pred } /** * Capture URL parameters from a request object as attributes of the current segment. * * @memberof WebFrameworkShim.prototype * * @param {Object} params * An object with key-value pairs. */ function captureUrlParams(params) { var segment = this.getSegment() if (segment && !this.agent.config.high_security) { urltils.copyParameters(params, segment.parameters) } } // -------------------------------------------------------------------------- // /** * Default route parser function if one is not provided. * * @private * * @param {WebFrameworkShim} shim * The shim in use for this instrumentation. * * @param {function} fn * The function which received this route string/RegExp. * * @param {string} fnName * The name of the function to which this route was given. * * @param {string|RegExp} route * The route that was given to the function. * * @see RouteParserFunction */ function _defaultRouteParser(shim, fn, fnName, route) { if (route instanceof RegExp) { return '/' + route.source + '/' } else if (typeof route === 'string') { return route } return '<unknown>' } /** * Default error predicate just returns true. * * @private * * @return {bool} True. Always. */ function _defaultErrorPredicate() { return true } /** * Default response predicate just returns false. * * @private * * @return {bool} False. Always. */ function _defaultResponsePredicate() { return false } /** * Wraps the given function in a middleware recorder function. * * @private * * @param {WebFrameworkShim} shim * The shim used for this instrumentation. * * @param {function} middleware * The middleware function to record. * * @param {MiddlewareSpec} spec * The spec describing the middleware. * * @return {function} The middleware function wrapped in a recorder. */ function _recordMiddleware(shim, middleware, spec) { function getRoute() { 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 } const typeDetails = MIDDLEWARE_TYPE_DETAILS[spec.type] 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 } function getSegmentName(route) { let segmentName = metricName if (typeDetails.path) { segmentName += route } else if (route.length > 1) { segmentName += '/' + route } return segmentName } var isErrorWare = spec.type === MIDDLEWARE_TYPE_NAMES.ERRORWARE var getReq = shim.isFunction(spec.req) ? spec.req : _makeGetReq(shim, spec.req) return shim.record( middleware, spec.promise ? middlewareWithPromiseRecorder : middlewareWithCallbackRecorder ) // TODO: let's please break these out function middlewareWithCallbackRecorder(shim, fn, fnName, args) { const route = getRoute() // Pull out the request object. var req = getReq.call(this, shim, fn, fnName, args) // Fetch the transaction information from that request. var txInfo = _getTransactionInfo(shim, req) if (!txInfo || !txInfo.transaction) { shim.logger.debug( {txInfo: txInfo}, 'Could not get transaction info in %s (%s)', route, fnName ) return null } txInfo.transaction.nameState.setPrefix(shim._metrics.FRAMEWORK) txInfo.errorHandled |= isErrorWare // Copy over route parameters onto the transaction root. var params = shim.agent.config.high_security ? null : spec.params.call(this, shim, fn, fnName, args, req) // Wrap up `next` 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. var nextWrapper = null if (shim.isFunction(spec.next)) { const nextDetails = { route, wrapNext: spec.next, isErrorWare, isPromise: false, appendPath: spec.appendPath } nextWrapper = _makeNextBinder(nextDetails, txInfo) } else { var 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: false, appendPath: spec.appendPath } nextWrapper = _makeNextBinder(nextDetails, 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) } // ...and possibly construct a recorder var recorder = null if (typeDetails.record) { var stackPath = txInfo.transaction.nameState.getPath() || '' recorder = _makeMiddlewareRecorder(shim, metricName + '/' + stackPath) } const segmentName = getSegmentName(route) // Finally, return the segment descriptor. return { name: segmentName, callback: nextWrapper, parent: txInfo.segmentStack[txInfo.segmentStack.length - 1], recorder: recorder, parameters: params, after: function afterExec(shim, _fn, _name, err) { var errIsError = _isError(shim, err) if (errIsError) { _noticeError(shim, txInfo, err) } else if (!nextWrapper && !isErrorWare && spec.appendPath) { txInfo.transaction.nameState.popPath(route) } if (errIsError || !nextWrapper) { txInfo.segmentStack.pop() } } } } function middlewareWithPromiseRecorder(shim, fn, fnName, args) { const route = getRoute() // Pull out the request object. var req = getReq.call(this, shim, fn, fnName, args) // Fetch the transaction information from that request. var txInfo = _getTransactionInfo(shim, req) if (!txInfo || !txInfo.transaction) { shim.logger.debug( {txInfo: txInfo}, 'Could not get transaction info in %s (%s)', route, fnName ) return null } txInfo.transaction.nameState.setPrefix(shim._metrics.FRAMEWORK) txInfo.errorHandled |= isErrorWare // Copy over route parameters onto the transaction root. var params = shim.agent.config.high_security ? null : spec.params.call(this, 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) } var recorder = null if (typeDetails.record) { var stackPath = txInfo.transaction.nameState.getPath() || '' recorder = _makeMiddlewareRecorder(shim, metricName + '/' + stackPath) } // The next callback style can still apply to promise based // middleware (e.g. koa). In this case we would like to remove the // path for the current executing middleware, then readd it once the // next callback is done (either asynchronously or after the // returned promise is resolved). var nextWrapper = function pushSegment(shim, _fn, _name, segment) { txInfo.segmentStack.push(segment) } if (shim.isFunction(spec.next)) { const nextDetails = { route, wrapNext: spec.next, isErrorWare, isPromise: true, appendPath: spec.appendPath } nextWrapper = _makeNextBinder(nextDetails, txInfo) } else { var 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: true, appendPath: spec.appendPath } nextWrapper = _makeNextBinder(nextDetails, txInfo) } } const segmentName = getSegmentName(route) // Finally, return the segment descriptor. return { name: segmentName, parent: txInfo.segmentStack[txInfo.segmentStack.length - 1], promise: spec.promise, callback: nextWrapper, recorder: recorder, parameters: params, after: function afterExec(shim, _fn, _name, err, result) { if (shim._responsePredicate(args, result)) { txInfo.transaction.nameState.freeze() } if (_isError(shim, err)) { _noticeError(shim, txInfo, err) } else { txInfo.errorHandled = true if (spec.appendPath) { txInfo.transaction.nameState.popPath(route) } } txInfo.segmentStack.pop() } } } } function _makeGetReq(shim, req) { return function getReqFromArgs(shim, fn, name, args) { var reqIdx = shim.normalizeIndex(args.length, req) if (reqIdx === null || !args[reqIdx]) { shim.logger.debug('Can not find request parameter, not recording.') return null } return args[reqIdx] } } 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, nextWrapper) // Called from outside to wrap functions that could be called to continue // to the next middleware function nextWrapper(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)) { _noticeError(shim, txInfo, err) } else if (!isFinal && !nextDetails.isErrorWare && nextDetails.appendPath) { segment.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() } var 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) { segment.transaction.nameState.appendPath(nextDetails.route) } txInfo.segmentStack.push(segment) return v }) } return ret }, parentSegment) // Bind to parent. }) } } } /** * Retrieves the cached transaction information from the given object if it is * available. * * @private * * @param {WebFrameworkShim} shim - The shim used for this instrumentation. * @param {http.IncomingMessage} req - The incoming request object. * * @return {object?} The transaction information if available, otherwise null. */ function _getTransactionInfo(shim, req) { try { return req[TRANSACTION_INFO_KEY] || null } catch (e) { shim.logger.debug(e, 'Failed to fetch transaction info from req') return null } } /** * Creates a recorder for middleware metrics. * * @private * * * @param {string} path - The mounting path of the middleware. * @param {Segment} segment - The segment generated for this middleware. * @param {string} scope - The scope of the metric to record. */ function _makeMiddlewareRecorder(shim, metricName) { return function middlewareMetricRecorder(segment, scope) { var duration = segment.getDurationInMillis() var exclusive = segment.getExclusiveDurationInMillis() var transaction = segment.transaction if (scope) { transaction.measure(metricName, scope, duration, exclusive) } transaction.measure(metricName, null, duration, exclusive) } } /** * Adds the given error to the transaction information if it is actually an error. * * @private * * @param {WebFrameworkShim} shim * The shim used for this web framework. * * @param {TransactionInfo} txInfo * The transaction context information for the request. * * @param {*} err * The error to notice. */ function _noticeError(shim, txInfo, err) { txInfo.error = err txInfo.errorHandled = false } /** * Determines if the given object is an error according to the shim. * * @private * * @param {WebFrameworkShim} shim * The shim used for this web framework. * * @param {?*} err * The object to check for error-ness. * * @return {bool} True if the given object is an error according to the shim. */ function _isError(shim, err) { return err && shim._errorPredicate(err) } /** * Copy the keys expected from source to destination. * * @private * * @param {Object} destination * The spec object receiving the expected values * * @param {Object} source * The spec object the values are coming from */ function _copyExpectedSpecParameters(destination, source) { var keys = [ 'matchArity' ] for (var i = 0; i < keys.length; ++i) { var key = keys[i] if (source[key] != null) { destination[key] = source[key] } } }