UNPKG

newrelic

Version:
545 lines (459 loc) 16.1 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const Transaction = require('../index.js') const logger = require('../../logger').child({ component: 'tracer' }) const symbols = require('../../symbols') const INACTIVE_TRANSACTION_MESSAGE = 'Not creating segment "%s" because no transaction was active' const SKIP_WRAPPING_FUNCTION_MESSAGE = 'Not wrapping "%s" because it was not a function' const CREATE_SEGMENT_MESSAGE = 'Creating "%s" segment for transaction %s.' const { addCLMAttributes: maybeAddCLMAttributes } = require('../../util/code-level-metrics') const AsyncLocalContextManager = require('../../context-manager/async-local-context-manager') const TraceSegment = require('../trace/segment') module.exports = Tracer function Tracer(agent) { if (!agent) { throw new Error('Must be initialized with an agent.') } this.agent = agent this._contextManager = new AsyncLocalContextManager(agent.config.opentelemetry_bridge.enabled) } Tracer.prototype.getContext = getContext Tracer.prototype.getTransaction = getTransaction Tracer.prototype.getSegment = getSegment Tracer.prototype.setSegment = setSegment Tracer.prototype.getSpanContext = getSpanContext 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.slice = argSlice Tracer.prototype.wrapFunctionFirstNoSegment = wrapFunctionFirstNoSegment Tracer.prototype.wrapFunction = wrapFunction Tracer.prototype.wrapFunctionLast = wrapFunctionLast Tracer.prototype.wrapFunctionFirst = wrapFunctionFirst Tracer.prototype.wrapSyncFunction = wrapSyncFunction Tracer.prototype.wrapCallback = wrapCallback function getContext() { return this._contextManager.getContext() } function getTransaction() { const context = this.getContext() if (context?.transaction && context?.transaction?.isActive()) { return context.transaction } return null } // TODO: Remove/replace external uses to tracer.getSegment() function getSegment() { const context = this.getContext() return context?.segment || null } // TODO: update to setNewContext or something like that function setSegment({ transaction, segment } = {}) { const context = this.getContext() const newContext = context.enterSegment({ transaction: transaction !== undefined ? transaction : context.transaction, segment: segment !== undefined ? segment : context.segment }) this._contextManager.setContext(newContext) } // TODO: Remove/replace external uses to tracer.getSpanContext() function getSpanContext() { const currentSegment = this.getSegment() return currentSegment && currentSegment.getSpanContext() } /** * Create segment and assign recorder to transaction. This also increments counters of * segments. * Does not create segment if there is no parent, transaction is active or parent * is opaque. * * @param {object} params to fn * @param {string} params.id if present, it will use id as segment.id. only used in otel bridge mode. * @param {string} params.name name of segment * @param {function} params.recorder time slice metrics recorder for segment * @param {TraceSegment} params.parent parent segment of segment being created * @param {Transaction} params.transaction active transaction * @returns {TraceSegment|null} returns new segment, existing parent if opaque or null(no parent or transaction inactive) */ function createSegment({ id, name, recorder, parent, transaction }) { if (!parent || !transaction?.isActive()) { logger.trace( { hasParent: !!parent, transactionActive: transaction?.isActive() }, 'Not creating segment %s, no parent or active transaction available.', name ) return null } if (parent.opaque) { logger.trace('Skipping child addition on opaque segment') return parent } logger.trace('Adding segment %s to %s in %s', name, parent.name, transaction.id) let collect = true if (transaction.numSegments >= this.agent.config.max_trace_segments) { collect = false } transaction.incrementCounters() const segment = new TraceSegment({ id, config: this.agent.config, name, collect, root: transaction.trace.root, parentId: parent.id }) if (recorder) { transaction.addRecorder(recorder.bind(null, segment)) } if (collect) { transaction.trace.segments.add(segment) } return segment } function addSegment(name, recorder, parent, full, task) { if (typeof task !== 'function') { throw new Error('task must be a function') } const context = this.getContext() const segment = this.createSegment({ name, recorder, parent, transaction: context.transaction }) let newContext = context if (segment) { newContext = context.enterSegment({ segment }) } maybeAddCLMAttributes(task, segment) return this.bindFunction(task, newContext, full)(segment, context?.transaction) } function transactionProxy(handler) { // if there's no handler, there's nothing to proxy. if (typeof handler !== 'function') { return handler } const tracer = this const wrapped = function wrapTransactionInvocation() { if (!tracer.agent.canCollectData()) { return handler.apply(this, arguments) } // don't nest transactions, reuse existing ones const context = tracer.getContext() const segment = context?.segment const currentTx = context?.transaction if (segment) { logger.warn( { transaction: { id: currentTx.id, name: currentTx.getName() }, segment: segment.name }, 'Active transaction when creating non-nested transaction' ) tracer.agent.recordSupportability('Nodejs/Transactions/Nested') return handler.apply(this, arguments) } const transaction = new Transaction(tracer.agent) const newContext = context.enterTransaction(transaction) return tracer.bindFunction(handler, newContext, true).apply(this, arguments) } wrapped[symbols.original] = handler return wrapped } /** * 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 {string} type - Type of transaction to create. 'web' or 'bg'. * @param {Function} handler - Generator to proxy. * @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 } const tracer = this const wrapped = function wrapTransactionInvocation() { if (!tracer.agent.canCollectData()) { return handler.apply(this, arguments) } // don't nest transactions, reuse existing ones let context = tracer.getContext() const transaction = tracer.getTransaction() let createNew = false if (!transaction || transaction.type !== type) { createNew = true } if (createNew) { const transaction = new Transaction(tracer.agent) transaction.type = type context = context.enterTransaction(transaction) } return tracer.bindFunction(handler, context).apply(this, arguments) } wrapped[symbols.original] = handler return wrapped } function bindFunction(handler, context, full) { if (typeof handler !== 'function') { return handler } return _makeWrapped({ tracer: this, handler, context, full: !!full }) } function _makeWrapped({ tracer, handler, context, full }) { const { segment } = context wrapped[symbols.original] = getOriginal(handler) wrapped[symbols.segment] = segment return wrapped function wrapped() { if (segment && full) { segment.start() } try { return tracer._contextManager.runInContext(context, handler, this, arguments) } catch (err) { logger.trace(err, 'Error from wrapped function:') throw err // Re-throwing application error, this is not an agent error. } finally { if (segment && full) { segment.touch() } } } } function getOriginal(fn) { const original = fn[symbols.original] if (original) { return original } return fn } function bindEmitter(emitter, segment) { if (!emitter || !emitter.emit) { return emitter } const emit = getOriginal(emitter.emit) const context = this.getContext() const newContext = context.enterSegment({ segment }) emitter.emit = this.bindFunction(emit, newContext) 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. * */ const length = args.length const array = new Array(length) for (let i = 0; i < length; i++) { array[i] = args[i] } return array } function wrapFunctionFirstNoSegment(original, name) { if (typeof original !== 'function') { return original } logger.trace('Wrapping function %s (no segment)', name || original.name || 'anonymous') const tracer = this return wrappedFunction function wrappedFunction() { if (!tracer.getTransaction()) { return original.apply(this, arguments) } const context = tracer.getContext() const args = tracer.slice(arguments) const cb = args[0] if (typeof cb === 'function') { args[0] = tracer.bindFunction(cb, context) } return original.apply(this, args) } } function wrapFunctionLast(name, recorder, original) { if (typeof original !== 'function') { logger.trace(SKIP_WRAPPING_FUNCTION_MESSAGE, name) return original } logger.trace('Wrapping %s as a callback-last function', name) const tracer = this return wrappedFunction function wrappedFunction() { const context = tracer.getContext() const transaction = tracer.getTransaction() if (!transaction) { logger.trace(INACTIVE_TRANSACTION_MESSAGE, name) return original.apply(this, arguments) } logger.trace(CREATE_SEGMENT_MESSAGE, name, transaction.id) const args = tracer.slice(arguments) const last = args.length - 1 const cb = args[last] if (typeof cb !== 'function') { return original.apply(this, arguments) } const child = tracer.createSegment({ name, recorder, parent: context.segment, transaction: context.transaction }) args[last] = tracer.wrapCallback(cb, child, function wrappedCallback() { logger.trace('Ending "%s" segment for transaction %s.', name, transaction.id) child.touch() return cb.apply(this, arguments) }) child.start() const newContext = context.enterSegment({ segment: child }) return tracer.bindFunction(original, newContext).apply(this, args) } } function wrapFunctionFirst(name, recorder, original) { if (typeof original !== 'function') { logger.trace(SKIP_WRAPPING_FUNCTION_MESSAGE, name) return original } logger.trace('Wrapping %s as a callback-first function', name) const tracer = this return wrappedFunction function wrappedFunction() { const context = tracer.getContext() const transaction = tracer.getTransaction() if (!transaction) { logger.trace(INACTIVE_TRANSACTION_MESSAGE, name) return original.apply(this, arguments) } logger.trace(CREATE_SEGMENT_MESSAGE, name, transaction.id) const args = tracer.slice(arguments) const cb = args[0] if (typeof cb !== 'function') { return original.apply(this, arguments) } const child = tracer.createSegment({ name, recorder, parent: context.segment, transaction: context.transaction }) args[0] = tracer.wrapCallback(cb, child, function wrappedCallback() { logger.trace('Ending "%s" segment for transaction %s.', name, transaction.id) child.touch() return cb.apply(this, arguments) }) child.start() const newContext = context.enterSegment({ segment: child }) return tracer.bindFunction(original, newContext).apply(this, args) } } function wrapFunction(name, recorder, original, wrapper, resp) { if (typeof original !== 'function' || !wrapper) { logger.trace(SKIP_WRAPPING_FUNCTION_MESSAGE, name) return original } logger.trace('Wrapping %s using a custom wrapper', name) const tracer = this return wrappedFunction function wrappedFunction() { const context = tracer.getContext() const transaction = tracer.getTransaction() if (!transaction) { logger.trace(INACTIVE_TRANSACTION_MESSAGE, name) return original.apply(this, arguments) } logger.trace(CREATE_SEGMENT_MESSAGE, name, transaction.id) const child = tracer.createSegment({ name, recorder, parent: context.segment, transaction }) const args = wrapper.call(this, child, tracer.slice(arguments), bind) child.start() const newContext = context.enterSegment({ segment: child }) let result = tracer.bindFunction(original, newContext).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(SKIP_WRAPPING_FUNCTION_MESSAGE, name) return original } logger.trace('Wrapping "%s" as a synchronous function', name) const tracer = this return wrappedFunction function wrappedFunction() { const context = tracer.getContext() const transaction = tracer.getTransaction() if (!transaction) { logger.trace(INACTIVE_TRANSACTION_MESSAGE, name) return original.apply(this, arguments) } logger.trace('Creating "%s" sync segment for transaction %s.', name, transaction.id) const child = tracer.createSegment({ name, recorder, parent: context.segment, transaction }) if (child) { child.async = false } const newContext = context.enterSegment({ segment: child }) return tracer.bindFunction(original, newContext, true).apply(this, arguments) } } function wrapCallback(original, segment, wrapped) { const tracer = this const context = this.getContext() 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[symbols.original] = original } const child = tracer.createSegment({ name: 'Callback: ' + (original.name || 'anonymous'), parent: segment, transaction: context.transaction }) if (child) { child.async = false } const newContext = context.enterSegment({ segment: child }) return tracer.bindFunction(wrapped || original, newContext, true).apply(this, arguments) }, context, false ) }