/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const genericRecorder = require('../../metrics/recorders/generic')
const logger = require('../../logger.js').child({ component: 'WebFrameworkShim' })
const metrics = require('../../metrics/names')
const TransactionShim = require('../transaction-shim')
const Shim = require('../shim')
const specs = require('../specs')
const util = require('util')
const { assignError, getTransactionInfo, isError, MIDDLEWARE_TYPE_NAMES } = require('./common')
const wrapMiddlewareMounter = require('./middleware-mounter')
const { _recordMiddleware } = require('./middleware')
/**
* 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}
*/
const FRAMEWORK_NAMES = {
CONNECT: 'Connect',
DIRECTOR: 'Director',
EXPRESS: 'Expressjs',
FASTIFY: 'Fastify',
HAPI: 'Hapi',
KOA: 'Koa',
NEXT: 'Nextjs',
NEST: 'Nestjs',
RESTIFY: 'Restify'
}
/**
* Constructs a shim associated with the given agent instance, specialized for
* instrumenting web frameworks.
*
* @class
* @augments 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} shimName
* Used to persist shim ids across different shim instances.
* @param {string} pkgVersion
* version of module
* @see TransactionShim
* @see WebFrameworkShim.FRAMEWORK_NAMES
*/
function WebFrameworkShim(agent, moduleName, resolvedName, shimName, pkgVersion) {
TransactionShim.call(this, agent, moduleName, resolvedName, shimName, pkgVersion)
this._logger = logger.child({ module: moduleName })
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
/**
* 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.
* @returns {undefined}
*/
function setRouteParser(parser) {
if (!this.isFunction(parser)) {
this.logger.debug('Given route parser is not a function.')
return
}
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) {
const 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.
* @returns {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
}
return this.record(nodule, properties, function renderRecorder(shim, fn, name, args) {
const 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
}
spec.recorder = genericRecorder
spec.name = metrics.VIEW.PREFIX + args[viewIdx] + metrics.VIEW.RENDER
return spec
})
}
/**
* 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.
* @returns {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
}
const wrapSpec = new specs.WrapSpec({
matchArity: spec.matchArity,
wrapper: function wrapMiddleware(shim, middleware) {
return _recordMiddleware(shim, middleware, 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.
* @returns {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
}
if (spec && this.isString(spec.name)) {
spec.route = '[param handler :' + spec.name + ']'
} else {
spec.route = '[param handler]'
}
spec.type = MIDDLEWARE_TYPE_NAMES.PARAMWARE
const wrapSpec = new specs.WrapSpec({
matchArity: spec.matchArity,
wrapper: function wrapParamware(shim, middleware, name) {
spec.name = name
return _recordMiddleware(shim, middleware, 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) {
const txInfo = getTransactionInfo(this, req)
if (txInfo && isError(this, err)) {
assignError(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) {
const 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): boolean} 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) {
const 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): boolean} pred
* Function which should return true if the object passed to it is considered
* a response.
*/
function setResponsePredicate(pred) {
this._responsePredicate = pred
}
// -------------------------------------------------------------------------- //
/**
* 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.
* @returns {string} route name
* @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
* @returns {boolean} True. Always.
*/
function _defaultErrorPredicate() {
return true
}
/**
* Default response predicate just returns false.
*
* @private
* @returns {boolean} False. Always.
*/
function _defaultResponsePredicate() {
return false
}