newrelic
Version:
New Relic agent
446 lines (361 loc) • 13.1 kB
JavaScript
'use strict'
var Transaction = require('../index.js')
var logger = require('../../logger').child({component: 'tracer'})
/*
* CONSTANTS
*/
var ORIGINAL = '__NR_original'
var SEGMENT = '__NR_segment'
module.exports = Tracer
function Tracer(agent) {
if (!agent) throw new Error("Must be initialized with an agent.")
this.agent = agent
this._segment = null
}
Tracer.prototype.getTransaction = getTransaction
Tracer.prototype.getSegment = getSegment
Tracer.prototype.createSegment = createSegment
Tracer.prototype.addSegment = addSegment
Tracer.prototype.transactionProxy = transactionProxy
Tracer.prototype.transactionNestProxy = transactionNestProxy
Tracer.prototype.bindFunction = bindFunction
Tracer.prototype.bindEmitter = bindEmitter
Tracer.prototype.getOriginal = getOriginal
Tracer.prototype.getSegmentFromWrapped = getSegmentFromWrapped
Tracer.prototype.slice = argSlice
Tracer.prototype.wrapFunctionNoSegment = wrapFunctionNoSegment
Tracer.prototype.wrapFunctionFirstNoSegment = wrapFunctionFirstNoSegment
Tracer.prototype.wrapFunction = wrapFunction
Tracer.prototype.wrapFunctionLast = wrapFunctionLast
Tracer.prototype.wrapFunctionFirst = wrapFunctionFirst
Tracer.prototype.wrapSyncFunction = wrapSyncFunction
Tracer.prototype.wrapCallback = wrapCallback
Object.defineProperty(Tracer.prototype, 'segment', {
get: function segmentGetter() {
return this._segment
},
set: function segmentSetter(segment) {
this._segment && this._segment.probe('Segment removed from tracer')
segment && segment.probe('Set tracer.segment')
return this._segment = segment
}
})
function getTransaction() {
if (this.segment && this.segment.transaction && this.segment.transaction.isActive()) {
return this.segment.transaction
}
return null
}
function getSegment() {
return this.segment
}
function createSegment(name, recorder, _parent) {
var parent = _parent || this.segment
if (!parent || !parent.transaction.isActive()) {
logger.trace({
hasParent: !!parent,
transactionActive: (parent && parent.transaction.isActive())
}, 'Not creating segment %s, no parent or active transaction available.', name)
return null
}
return parent.add(name, recorder)
}
function addSegment(name, recorder, parent, full, task) {
if (typeof task !== 'function') {
throw new Error('task must be a function')
}
var segment = this.createSegment(name, recorder, parent)
return this.bindFunction(task, segment, full)(segment)
}
function transactionProxy(handler) {
// if there's no handler, there's nothing to proxy.
if (typeof handler !== 'function') return handler
var tracer = this
var wrapped = function wrapTransactionInvocation() {
// don't nest transactions, reuse existing ones
var segment = tracer.segment
if (segment) {
if (segment.transaction.traceStacks) {
segment.probe('!!! Nested transaction creation !!!')
segment.transaction.traceFlag = true // Will log the stacks when it ends.
}
logger.warn({
transaction: {id: segment.transaction.id, name: segment.transaction.getName()},
segment: segment.name
}, 'Active transaction when creating non-nested transaction')
tracer.agent.recordSupportability('Nodejs/Transactions/Nested')
return handler.apply(this, arguments)
}
var transaction = new Transaction(tracer.agent)
return tracer.bindFunction(handler, transaction.trace.root, true)
.apply(this, arguments)
}
wrapped[ORIGINAL] = handler
return wrapped
}
// FLAG: custom_instrumentation
/**
* Use transactionNestProxy to wrap a closure that is a top-level handler that
* is meant to start transactions. This wraps the first half of asynchronous
* handlers. Use bindFunction to wrap handler callbacks. This detects to see
* if there is an in play segment and uses that as the root instead of
* transaction.trace.root.
*
* @param {Function} handler Generator to be proxied.
* @returns {Function} Proxy.
*/
function transactionNestProxy(type, handler) {
if (handler === undefined && typeof type === 'function') {
handler = type
type = undefined
}
// if there's no handler, there's nothing to proxy.
if (typeof handler !== 'function') return handler
var tracer = this
var wrapped = function wrapTransactionInvocation() {
// don't nest transactions, reuse existing ones
var transaction = tracer.getTransaction()
var segment = tracer.segment
var createNew = false
if (!transaction || transaction.type !== type) {
createNew = true
}
if (createNew) {
transaction = new Transaction(tracer.agent)
transaction.type = type
segment = transaction.trace.root
}
return tracer.bindFunction(handler, segment).apply(this, arguments)
}
wrapped[ORIGINAL] = handler
return wrapped
}
function bindFunction(handler, segment, full) {
if (typeof handler !== 'function') {
return handler
}
return _makeWrapped(this, handler, segment || this.segment, !!full)
}
function _makeWrapped(tracer, handler, active, full) {
wrapped[ORIGINAL] = getOriginal(handler)
wrapped[SEGMENT] = active
return wrapped
function wrapped() {
var prev = tracer.segment
tracer.segment = active
if (active && full) active.start()
try {
return handler.apply(this, arguments)
} catch (err) {
logger.trace(err, "Error from wrapped function:")
if (prev === null && process.domain != null) {
process.domain.__NR_transactionSegment = tracer.segment
}
throw err // Re-throwing application error, this is not an agent error.
} finally {
if (active && full) active.touch()
tracer.segment = prev
}
}
}
function getOriginal(fn) {
return fn && fn[ORIGINAL] ? fn[ORIGINAL] : fn
}
function getSegmentFromWrapped(fn) {
return fn && fn[SEGMENT] ? fn[SEGMENT] : null
}
function bindEmitter(emitter, segment) {
if (!emitter || !emitter.emit) {
return emitter
}
var emit = getOriginal(emitter.emit)
emitter.emit = this.bindFunction(emit, segment)
return emitter
}
function argSlice(args) {
/**
* Usefully nerfed version of slice for use in instrumentation. Way faster
* than using [].slice.call, and maybe putting it in here (instead of the
* same module context where it will be used) will make it faster by
* defeating inlining.
*
* http://jsperf.com/array-slice-call-arguments-2
*
* for untrustworthy benchmark numbers. Only useful for copying whole
* arrays, and really only meant to be used with the arguments array like.
*
* Also putting this comment inside the function in an effort to defeat
* inlining.
*
*/
var length = args.length
var array = new Array(length)
for (var i = 0; i < length; i++) {
array[i] = args[i]
}
return array
}
function wrapFunctionNoSegment(original, name, wrapper) {
if (typeof original !== 'function') return original
logger.trace('Wrapping function %s (no segment)', name || original.name || 'anonymous')
var tracer = this
return wrappedFunction
function wrappedFunction() {
if (!tracer.getTransaction()) return original.apply(this, arguments)
var args = tracer.slice(arguments)
if (wrapper === undefined) {
var last = args.length - 1
var cb = args[last]
if (typeof cb === 'function') {
args[last] = tracer.bindFunction(cb)
}
} else {
args = wrapper(args)
}
return original.apply(this, args)
}
}
function wrapFunctionFirstNoSegment(original, name) {
if (typeof original !== 'function') return original
logger.trace('Wrapping function %s (no segment)', name || original.name || 'anonymous')
var tracer = this
return wrappedFunction
function wrappedFunction() {
if (!tracer.getTransaction()) return original.apply(this, arguments)
var args = tracer.slice(arguments)
var cb = args[0]
if (typeof cb === 'function') {
args[0] = tracer.bindFunction(cb)
}
return original.apply(this, args)
}
}
function wrapFunctionLast(name, recorder, original) {
if (typeof original !== 'function') {
logger.trace('Not wrapping "%s" because it was not a function', name)
return original
}
logger.trace('Wrapping %s as a callback-last function', name)
var tracer = this
return wrappedFunction
function wrappedFunction() {
var transaction = tracer.getTransaction()
if (!transaction) {
logger.trace('Not creating segment "%s" because no transaction was active', name)
return original.apply(this, arguments)
}
logger.trace('Creating "%s" segment for transaction %s.', name, transaction.id)
var args = tracer.slice(arguments)
var last = args.length - 1
var cb = args[last]
if (typeof cb !== 'function') return original.apply(this, arguments)
var child = tracer.createSegment(name, recorder)
args[last] = tracer.wrapCallback(cb, child, wrappedCallback)
child.start()
return tracer.bindFunction(original, child).apply(this, args)
function wrappedCallback() {
logger.trace('Ending "%s" segment for transaction %s.', name, transaction.id)
child.touch()
return cb.apply(this, arguments)
}
}
}
function wrapFunctionFirst(name, recorder, original) {
if (typeof original !== 'function') {
logger.trace('Not wrapping "%s" because it was not a function', name)
return original
}
logger.trace('Wrapping %s as a callback-first function', name)
var tracer = this
return wrappedFunction
function wrappedFunction() {
var transaction = tracer.getTransaction()
if (!transaction) {
logger.trace('Not creating segment "%s" because no transaction was active', name)
return original.apply(this, arguments)
}
logger.trace('Creating "%s" segment for transaction %s.', name, transaction.id)
var args = tracer.slice(arguments)
var cb = args[0]
if (typeof cb !== 'function') return original.apply(this, arguments)
var child = tracer.createSegment(name, recorder)
args[0] = tracer.wrapCallback(cb, child, wrappedCallback)
child.start()
return tracer.bindFunction(original, child).apply(this, args)
function wrappedCallback() {
logger.trace('Ending "%s" segment for transaction %s.', name, transaction.id)
child.touch()
var result = cb.apply(this, arguments)
return result
}
}
}
function wrapFunction(name, recorder, original, wrapper, resp) {
if (typeof original !== 'function' || !wrapper) {
logger.trace('Not wrapping "%s" because it was not a function', name)
return original
}
logger.trace('Wrapping %s using a custom wrapper', name)
var tracer = this
return wrappedFunction
function wrappedFunction() {
var transaction = tracer.getTransaction()
if (!transaction) {
logger.trace('Not creating segment "%s" because no transaction was active', name)
return original.apply(this, arguments)
}
logger.trace('Creating "%s" segment for transaction %s.', name, transaction.id)
var child = tracer.createSegment(name, recorder)
var args = wrapper.call(this, child, tracer.slice(arguments), bind)
child.start()
var result = tracer.bindFunction(original, child).apply(this, args)
if (resp) result = resp.call(this, child, result, bind)
return result
function bind(fn) {
if (!fn) return fn
return tracer.wrapCallback(fn, child, function nrWrappedHandler() {
logger.trace('Touching "%s" segment for transaction %s.', name, transaction.id)
child.touch()
return fn.apply(this, arguments)
})
}
}
}
function wrapSyncFunction(name, recorder, original) {
if (typeof original !== 'function') {
logger.trace('Not wrapping "%s" because it was not a function', name)
return original
}
logger.trace('Wrapping "%s" as a synchronous function', name)
var tracer = this
return wrappedFunction
function wrappedFunction() {
var transaction = tracer.getTransaction()
if (!transaction) {
logger.trace('Not creating segment "%s" because no transaction was active', name)
return original.apply(this, arguments)
}
logger.trace('Creating "%s" sync segment for transaction %s.', name, transaction.id)
var child = tracer.createSegment(name, recorder)
if (child) child.async = false
return tracer.bindFunction(original, child, true).apply(this, arguments)
}
}
function wrapCallback(original, segment, wrapped) {
var tracer = this
if (typeof original !== 'function') return original
logger.trace(
'Wrapping callback for "%s" segment',
segment ? segment.name : 'unknown'
)
return tracer.bindFunction(function wrappedCallback() {
if (wrapped) wrapped[ORIGINAL] = original
var child = tracer.createSegment(
'Callback: ' + (original.name || 'anonymous'),
null,
segment
)
if (child) child.async = false
return tracer.bindFunction(wrapped || original, child, true).apply(this, arguments)
}, segment, false)
}