-
Notifications
You must be signed in to change notification settings - Fork 401
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: Migrated @newrelic/koa into mainline agent repo (#2148)
- Loading branch information
Showing
29 changed files
with
3,123 additions
and
244 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/* | ||
* Copyright 2021 New Relic Corporation. All rights reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
'use strict' | ||
const http = require('http') | ||
const methodsLower = http.METHODS.map((method) => method.toLowerCase()) | ||
module.exports.METHODS = methodsLower |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
/* | ||
* Copyright 2020 New Relic Corporation. All rights reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
'use strict' | ||
const symbols = require('../../symbols') | ||
const { MiddlewareSpec, MiddlewareMounterSpec } = require('../../shim/specs') | ||
|
||
module.exports = function initialize(shim, Koa) { | ||
// Koa's exports are different depending on using CJS or MJS - https://github.com/koajs/koa/issues/1513 | ||
const proto = Koa.prototype || Koa.default?.prototype | ||
|
||
if (!shim || !Koa || !proto || Object.keys(proto).length > 1) { | ||
shim.logger.debug( | ||
'Koa instrumentation function called with incorrect arguments, not instrumenting.' | ||
) | ||
return | ||
} | ||
|
||
shim.setFramework(shim.KOA) | ||
|
||
shim.wrapMiddlewareMounter( | ||
proto, | ||
'use', | ||
new MiddlewareMounterSpec({ | ||
wrapper: wrapMiddleware | ||
}) | ||
) | ||
shim.wrapReturn(proto, 'createContext', wrapCreateContext) | ||
|
||
// The application is used to handle unhandled errors in the application. We | ||
// want to notice those. | ||
shim.wrap(proto, 'emit', function wrapper(shim, original) { | ||
return function wrappedEmit(evt, err, ctx) { | ||
if (evt === 'error' && ctx) { | ||
shim.noticeError(ctx.req, err) | ||
} | ||
return original.apply(this, arguments) | ||
} | ||
}) | ||
} | ||
|
||
function wrapMiddleware(shim, middleware) { | ||
// Skip middleware that are already wrapped. | ||
if (shim.isWrapped(middleware)) { | ||
return middleware | ||
} | ||
|
||
if (middleware.router) { | ||
shim.logger.info( | ||
[ | ||
'Found uninstrumented router property on Koa middleware.', | ||
'This may indicate either an unsupported routing library is being used,', | ||
'or a particular version of a supported library is not fully instrumented.' | ||
].join(' ') | ||
) | ||
} | ||
|
||
return shim.recordMiddleware( | ||
middleware, | ||
new MiddlewareSpec({ | ||
type: shim.MIDDLEWARE, | ||
promise: true, | ||
appendPath: true, | ||
next: shim.LAST, | ||
req: function getReq(shim, fn, fnName, args) { | ||
return args[0] && args[0].req | ||
} | ||
}) | ||
) | ||
} | ||
|
||
/** | ||
* Many of the properties on the `context` object are just aliases for the same | ||
* property on the `request` or `response` objects. We take advantage of this | ||
* by just intercepting the `request` or `response` property and don't touch | ||
* the `context` property. | ||
* See: https://github.com/koajs/koa/blob/master/lib/context.js#L186-L241 | ||
* | ||
* @param {Shim} shim instance of shim | ||
* @param {function} _fn createContext function | ||
* @param {string} _fnName name of function | ||
* @param {object} context koa ctx object | ||
*/ | ||
function wrapCreateContext(shim, _fn, _fnName, context) { | ||
wrapResponseBody(shim, context) | ||
wrapMatchedRoute(shim, context) | ||
wrapResponseStatus(shim, context) | ||
} | ||
|
||
function wrapResponseBody(shim, context) { | ||
// The `context.body` and `context.response.body` properties are how users set | ||
// the response contents. It is roughly equivalent to `res.send()` in Express. | ||
// Under the hood, these set the `_body` property on the `context.response`. | ||
context[symbols.koaBody] = context.response.body | ||
context[symbols.koaBodySet] = false | ||
|
||
Object.defineProperty(context.response, '_body', { | ||
get: () => context[symbols.koaBody], | ||
set: function setBody(val) { | ||
if (!context[symbols.koaRouter]) { | ||
shim.savePossibleTransactionName(context.req) | ||
} | ||
context[symbols.koaBody] = val | ||
context[symbols.koaBodySet] = true | ||
} | ||
}) | ||
} | ||
|
||
function wrapMatchedRoute(shim, context) { | ||
context[symbols.koaMatchedRoute] = null | ||
context[symbols.koaRouter] = false | ||
|
||
Object.defineProperty(context, '_matchedRoute', { | ||
get: () => context[symbols.koaMatchedRoute], | ||
set: (val) => { | ||
const match = getLayerForTransactionName(context) | ||
|
||
// match should never be undefined given _matchedRoute was set | ||
if (match) { | ||
const currentSegment = shim.getActiveSegment() | ||
|
||
// Segment/Transaction may be null, see: | ||
// - https://github.com/newrelic/node-newrelic-koa/issues/32 | ||
// - https://github.com/newrelic/node-newrelic-koa/issues/33 | ||
if (currentSegment) { | ||
const transaction = currentSegment.transaction | ||
|
||
if (context[symbols.koaMatchedRoute]) { | ||
transaction.nameState.popPath() | ||
} | ||
|
||
transaction.nameState.appendPath(match.path) | ||
transaction.nameState.markPath() | ||
} | ||
} | ||
|
||
context[symbols.koaMatchedRoute] = val | ||
// still true if somehow match is undefined because we are | ||
// using koa-router naming and don't want to allow default naming | ||
context[symbols.koaRouter] = true | ||
} | ||
}) | ||
} | ||
|
||
function wrapResponseStatus(shim, context) { | ||
// Sometimes people just set `context.status` or `context.response.status` | ||
// without setting a body. When this happens we'll want to use that as the | ||
// response point to name the transaction. `context.status` is just an alias | ||
// for `context.response.status` so we only wrap the latter. | ||
const statusDescriptor = getInheritedPropertyDescriptor(context.response, 'status') | ||
if (!statusDescriptor) { | ||
shim.logger.debug('Failed to find status descriptor on context.response') | ||
return | ||
} else if (!statusDescriptor.get || !statusDescriptor.set) { | ||
shim.logger.debug(statusDescriptor, 'Status descriptor missing getter/setter pair') | ||
return | ||
} | ||
|
||
Object.defineProperty(context.response, 'status', { | ||
get: () => statusDescriptor.get.call(context.response), | ||
set: function setStatus(val) { | ||
if (!context[symbols.koaBodySet] && !context[symbols.koaRouter]) { | ||
shim.savePossibleTransactionName(context.req) | ||
} | ||
return statusDescriptor.set.call(this, val) | ||
} | ||
}) | ||
} | ||
|
||
function getLayerForTransactionName(context) { | ||
// Context.matched might be null | ||
// See https://github.com/newrelic/node-newrelic-koa/pull/29 | ||
if (!context.matched) { | ||
return null | ||
} | ||
for (let i = context.matched.length - 1; i >= 0; i--) { | ||
const layer = context.matched[i] | ||
if (layer.opts.end) { | ||
return layer | ||
} | ||
} | ||
|
||
return null | ||
} | ||
|
||
function getInheritedPropertyDescriptor(obj, property) { | ||
let proto = obj | ||
let descriptor = null | ||
do { | ||
descriptor = Object.getOwnPropertyDescriptor(proto, property) | ||
proto = Object.getPrototypeOf(proto) | ||
} while (!descriptor && proto) | ||
|
||
return descriptor | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright 2020 New Relic Corporation. All rights reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
'use strict' | ||
|
||
const InstrumentationDescriptor = require('../../instrumentation-descriptor') | ||
|
||
module.exports = [ | ||
{ | ||
type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK, | ||
moduleName: 'koa', | ||
shimName: 'koa', | ||
onRequire: require('./instrumentation') | ||
}, | ||
{ | ||
type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK, | ||
moduleName: 'koa-router', | ||
shimName: 'koa', | ||
onRequire: require('./router-instrumentation') | ||
}, | ||
{ | ||
type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK, | ||
moduleName: '@koa/router', | ||
shimName: 'koa', | ||
onRequire: require('./router-instrumentation') | ||
}, | ||
{ | ||
type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK, | ||
moduleName: 'koa-route', | ||
shimName: 'koa', | ||
onRequire: require('./route-instrumentation') | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* Copyright 2020 New Relic Corporation. All rights reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
'use strict' | ||
|
||
const { METHODS } = require('../http-methods') | ||
const { MiddlewareSpec } = require('../../shim/specs') | ||
|
||
module.exports = function instrumentRoute(shim, route) { | ||
shim.setFramework(shim.KOA) | ||
|
||
METHODS.forEach(function wrap(method) { | ||
shim.wrap(route, method, function wrapMethod(shim, methodFn) { | ||
return function wrappedMethod() { | ||
const middleware = methodFn.apply(route, arguments) | ||
return shim.recordMiddleware( | ||
middleware, | ||
new MiddlewareSpec({ | ||
route: arguments[0], | ||
next: shim.LAST, | ||
name: shim.getName(arguments[1]), | ||
promise: true, | ||
req: function getReq(shim, fn, fnName, args) { | ||
return args[0] && args[0].req | ||
} | ||
}) | ||
) | ||
} | ||
}) | ||
}) | ||
} |
Oops, something went wrong.