Skip to content

Commit

Permalink
Merge branch 'integration' into mgoin/NODE-2124-HarvestCleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelgoin committed Oct 9, 2019
2 parents 428099d + 4c4f3aa commit f18244a
Show file tree
Hide file tree
Showing 9 changed files with 648 additions and 269 deletions.
7 changes: 7 additions & 0 deletions lib/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ exports.config = () => ({
filter_cache_limit: 1000
},

diagnostics: {
code_injector: {
enabled: false,
internal_file_pattern: /nodejs_agent\/(?:(?!test))|\/node_modules\/(?:@)?newrelic/
}
},

logging: {
/**
* Verbosity of the module's logging. This module uses bunyan
Expand Down
6 changes: 6 additions & 0 deletions lib/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ const ENV_MAPPING = {
include: 'NEW_RELIC_TRANSACTION_EVENTS_ATTRIBUTES_INCLUDE'
}
},
diagnostics: {
code_injector: {
enabled: 'NEW_RELIC_DIAGNOSTICS_CODE_INJECTOR_ENABLED'
}
},
transaction_tracer: {
attributes: {
enabled: 'NEW_RELIC_TRANSACTION_TRACER_ATTRIBUTES_ENABLED',
Expand Down Expand Up @@ -186,6 +191,7 @@ const BOOLEAN_VARS = new Set([
'NEW_RELIC_TRACER_ENABLED',
'NEW_RELIC_TRANSACTION_TRACER_ATTRIBUTES_ENABLED',
'NEW_RELIC_TRANSACTION_EVENTS_ATTRIBUTES_ENABLED',
'NEW_RELIC_DIAGNOSTICS_CODE_INJECTOR_ENABLED',
'NEW_RELIC_TRANSACTION_SEGMENTS_ATTRIBUTES_ENABLED',
'NEW_RELIC_DEBUG_METRICS',
'NEW_RELIC_DEBUG_TRACER',
Expand Down
2 changes: 1 addition & 1 deletion lib/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,7 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
return
}

if (typeof node === 'object' && !Array.isArray(node)) {
if (typeof node === 'object' && !(node instanceof RegExp) && !Array.isArray(node)) {
// is top level and can have arbitrary keys
var allowArbitrary = internal === this || HAS_ARBITRARY_KEYS.has(key)
this._fromPassed(node, internal[key], allowArbitrary)
Expand Down
161 changes: 161 additions & 0 deletions lib/injector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use strict'

// to avoid parsing the esprima code base we require it here
// TODO: excise this with better internal file filtering
const esprima = require('esprima')
const ecg = require('escodegen')
const fileNameToken = {
type: 'Literal',
value: null
}
const lineNumberTokenTemplate = {
type: 'Literal',
value: null
}

class Mutator {
constructor(appliesTo, mutate) {
this.appliesTo = appliesTo
this.mutate = mutate
this.tokens = []
}

add(token) {
this.tokens.push(token)
}

apply() {
this.tokens.forEach(this.mutate)
}
}

const functionTypes = new Set([
'ArrowFunctionExpression',
'FunctionExpression',
'AsyncFunction'
])

const equalityCheckOps = new Set([
'==',
'===',
'!=',
'!==',
'instanceof'
])

const variableTypes = new Set([
'MemberExpression',
'Identifier'
])

const callTypes = new Set([
'CallExpression',
'NewExpression'
])

const wrapTemplate = esprima.parse('__NR_wrap()').body[0].expression
const unwrapTemplate = esprima.parse('__NR_unwrap()').body[0].expression

const mutators = [
new Mutator(function wrapAssignmentPredicate(token) {
return token.type === 'AssignmentExpression' &&
token.operator === '=' &&
functionTypes.has(token.right.type)
}, function injectWrapAssignment(token) {
token.right = wrapToken(token.right)
}),
new Mutator(function wrapArgPredicate(token) {
return callTypes.has(token.type)
}, function injectWrapArg(token) {
token.arguments = token.arguments.map(wrapToken)
}),
new Mutator(function unwrapPredicate(token) {
return token.type === 'BinaryExpression' && equalityCheckOps.has(token.operator)
}, function injectUnwrap(token) {
token.left = unwrapToken(token.left)
token.right = unwrapToken(token.right)
})
]

function wrapToken(argToken) {
const type = argToken.type
if (
!functionTypes.has(type) &&
!callTypes.has(type) &&
!variableTypes.has(type) ||
!argToken.loc
) {
return argToken
}

const wrapped = Object.assign({}, wrapTemplate)
const lineNumberToken = Object.assign({}, lineNumberTokenTemplate)
lineNumberToken.value = argToken.loc.start.line
wrapped.arguments = [argToken, lineNumberToken, Object.assign({}, fileNameToken)]
return wrapped
}

function unwrapToken(argToken) {
if (!variableTypes.has(argToken.type)) {
return argToken
}

const wrapped = Object.assign({}, unwrapTemplate)
wrapped.arguments = [argToken]
return wrapped
}

function inject(sourceCode, file) {
// wrap the incoming file code to make it more palatable for esprima.
// node likewise wraps the contents of the file in a function, so this
// replicates the behavior (e.g. allows for global returns)
sourceCode = 'function main() {' + sourceCode + '}'
const sourceRootBody = esprima.parse(sourceCode, {loc: true}).body[0].body.body

const toRelax = [].concat(sourceRootBody)

while (toRelax.length) {
const currentToken = toRelax.pop()

mutators.forEach(m => {
if (m.appliesTo(currentToken)) {
m.add(currentToken)
}
})

for (let key of Object.keys(currentToken)) {
if (key === 'loc') continue
const value = currentToken[key]
if (value && value instanceof Object) {
if (Array.isArray(value)) {
for (let t of value) {
if (t) {
toRelax.push(t)
}
}
} else {
toRelax.push(value)
}
}
}
}

// TODO: make this less janky
fileNameToken.value = file
mutators.forEach(m => m.apply())
fileNameToken.value = null
// create a new base level token that contains all the statements we
// want to pass back to node
const printed = ecg.generate({
type: 'Program',
body: sourceRootBody,
sourceType: 'script'
}, {
format: {
semicolons: false
}
})
return printed
}

module.exports = { inject }
116 changes: 116 additions & 0 deletions lib/shimmer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var logger = require('./logger').child({component: 'shimmer'})
var INSTRUMENTATIONS = require('./instrumentations')()
var properties = require('./util/properties')
var shims = require('./shim')
const { inject } = require('./injector')

var MODULE_TYPE = shims.constants.MODULE_TYPE

Expand Down Expand Up @@ -342,6 +343,121 @@ var shimmer = module.exports = {
var Module = require('module')
var filepathMap = {}

// TODO: how do we unpatch this?
const diagConf = agent.config.diagnostics.code_injector
if (diagConf.enabled) {
// XXX: this is required to handle unwrapping event listener removal
const EEProto = require('events').EventEmitter.prototype
shimmer.wrapMethod(
EEProto,
'EventEmitter.prototype',
['addListener', 'on', 'once', 'off', 'prependListener', 'prependOnceListener'],
function wrapAddListener(addListener) {
return function wrappedAddListener(ev, listener) {
return addListener.call(this, ev, __NR_unwrap(listener))
}
}
)
shimmer.wrapMethod(
EEProto,
'EventEmitter.prototype',
'removeListener',
function wrapRemoveListener(removeListener) {
return function wrappedRemoveListener(ev, listener) {
return removeListener.call(this, ev, __NR_unwrap(listener))
}
}
)
const internalCodePattern = diagConf.internal_file_pattern
const proto = Module.prototype
shimmer.wrapMethod(
proto,
'Module.prototype',
'_compile',
function wrapCompile(_compile) {
return function wrappedCompile(code, file) {
let injected
try {
injected = internalCodePattern.test(file) ? code : inject(code, file)
return _compile.call(this, injected, file)
} catch (e) {
logger.debug('Unable to parse file:', file, e)
return _compile.call(this, code, file)
}
}
}
)

// TODO: use shimmer methods
global.tracer = agent.tracer
global.__NR_wrap = __NR_wrap
function __NR_wrap(f, lineNum, fileName) {
const scheduledSegment = agent.tracer.getSegment()
if (
typeof f !== 'function' ||
!scheduledSegment ||
!scheduledSegment.transaction.isActive()
) {
return f
}


const scheduledTransaction = scheduledSegment.transaction
return new Proxy(f, {
get: function getTrap(target, prop) {
// Allow for look up of the target
if (prop === '__NR_new_original') {
return target
}
return target[prop]
},
construct: function constructTrap(Target, proxyArgs) {
const currentSegment = agent.tracer.getSegment()
if (
scheduledTransaction.isActive() &&
(
!currentSegment ||
currentSegment.transaction.id !== scheduledTransaction.id
)
) {
logger.info(
`lost state in ${fileName} (line ${lineNum});`,
`expected to be in transaction ${scheduledTransaction.id},`,
`instead landed in ${currentSegment && currentSegment.transaction.id}`
)
}
return new Target(...proxyArgs)
},
apply: function wrappedApply(target, thisArg, args) {
const currentSegment = agent.tracer.getSegment()
if (
scheduledTransaction.isActive() &&
(
!currentSegment ||
currentSegment.transaction.id !== scheduledTransaction.id
)
) {
logger.info(
`lost state in ${fileName} (line ${lineNum});`,
`expected to be in transaction ${scheduledTransaction.id},`,
`instead landed in ${currentSegment && currentSegment.transaction.id}`
)
}
return target.apply(thisArg, args)
}
})
}

global.__NR_unwrap = __NR_unwrap
function __NR_unwrap(f) {
if (typeof f !== 'function' || !f || !f.__NR_new_original) {
return f
}

return f.__NR_new_original
}
}

shimmer.wrapMethod(Module, 'Module', '_resolveFilename', function wrapRes(resolve) {
return function wrappedResolveFilename(file) {
// This is triggered by the load call, so record the path that has been seen so
Expand Down
Loading

0 comments on commit f18244a

Please sign in to comment.