UNPKG

appdynamics

Version:

Performance Profiler and Monitor

612 lines (514 loc) 15.9 kB
/* Copyright (c) AppDynamics, Inc., and its affiliates 2015 All Rights Reserved */ 'use strict'; // uncomment to generate proxy-funcs.js file /* var proxyFuncsContent = 'module.exports = {\n'; for(var i = 0; i < 250; i++) { proxyFuncsContent += ' ' + i + ': function(func) { return function appd_proxy_' + i + '() { return func.apply(this, arguments); }},\n'; } proxyFuncsContent += '};\n'; var fs = require('fs'); require('fs').writeFile("proxy-funcs.js", proxyFuncsContent, function(err) { if(err) return console.error(err); console.log('done.'); }); return; */ var EventEmitter = require('events').EventEmitter; var proxyFuncs = require('./proxy-funcs'); function AppDProxy(agent) { this.agent = agent; this.threadProxyMap = undefined; this.threadProxyIndex = undefined; this.callContextMap = undefined; this.callContextEnabled = undefined; } exports.AppDProxy = AppDProxy; AppDProxy.prototype.init = function() { var self = this; // removeListener compairs objects, so the original callback // should be passed instead of the proxy self.before(EventEmitter.prototype, 'removeListener', function(obj, args) { if(args.length > 1 && args[1] && args[1].__appdynamicsProxy__) { args[1] = args[1].__appdynamicsProxy__; } else { // Try to match the callback function intended to be removed for // the event with all the probed callbacks attached for that event // on the object var eventCbList = obj._events[args[0]]; if ((typeof eventCbList === 'function') && eventCbList.__appdynamicsProxy__ && eventCbList.__appdynamicsProxy__ === args[1]) { eventCbList = eventCbList.__appdynamicsProxy__; obj._events[args[0]] = eventCbList; } if (eventCbList && eventCbList.length) { for(var i = 0; i < eventCbList.length; i++) { if (eventCbList[i] && eventCbList[i].__appdynamicsProxy__ && (eventCbList[i].__appdynamicsProxy__ === args[1])) eventCbList[i] = eventCbList[i].__appdynamicsProxy__; } } } }); self.agent.on('btDetails', function(btDetails, transaction) { if(!self.callContextEnabled) return; var proxyId = self.threadProxyMap[transaction.threadId]; if (proxyId >= 0) { if(btDetails.btInfoRequest && btDetails.btInfoRequest.btIdentifier && btDetails.btInfoRequest.btIdentifier.btID) { var callContext = {'btId': btDetails.btInfoRequest.btIdentifier.btID}; if(btDetails.snapshotInfo && btDetails.snapshotInfo.snapshot.snapshotGUID) { callContext.snapshotGuid = btDetails.snapshotInfo.snapshot.snapshotGUID; } self.callContextMap['appd_proxy_' + proxyId] = callContext; } } }); self.agent.on('updateCallContextMap', function(transaction, btId) { if (!self.callContextEnabled || btId <= 0) return; var proxyId = self.threadProxyMap[transaction.threadId]; if (proxyId >= 0) { var callContext = { 'btId': btId, 'snapshotGuid': transaction.guid }; self.callContextMap['appd_proxy_' + proxyId] = callContext; } }); self.disableCallContext(); }; AppDProxy.prototype.enableCallContext = function() { var self = this; self.disableCallContext(); self.callContextEnabled = true; }; AppDProxy.prototype.disableCallContext = function() { var self = this; self.callContextEnabled = false; self.callContextMap = {}; self.threadProxyMap = {}; self.threadProxyIndex = -1; }; AppDProxy.prototype.getCallContextMap = function() { var self = this; return self.callContextMap; }; /* istanbul ignore next */ AppDProxy.prototype.generateThreadProxy = function(func, index) { var proxyFuncGen = proxyFuncs[index]; if(proxyFuncGen) { return proxyFuncGen(func); } return undefined; }; AppDProxy.prototype.getThreadProxy = function(func) { var self = this; var threadId = self.agent.thread.current(); if (threadId !== undefined) { // check if already mapped var threadProxyId = self.threadProxyMap[threadId]; if (threadProxyId >= 0 ) { return self.generateThreadProxy(func, threadProxyId); } else { // try to get a free wrapper if (self.threadProxyIndex++ < 250) { // map wrapper id to thread id self.threadProxyMap[threadId] = self.threadProxyIndex; return self.generateThreadProxy(func, self.threadProxyIndex); } } } return undefined; }; AppDProxy.prototype.wrapWithThreadProxyIfEnabled = function (realCallback) { if (!this.callContextEnabled) { return realCallback; } var result = this.getThreadProxy(realCallback); if (!result) { return realCallback; } return result; }; var Locals = function() { this.time = undefined; this.stackTrace = undefined; this.params = undefined; this.opts = undefined; this.group = undefined; this.req = undefined; this.res = undefined; this.error = undefined; this.transaction = undefined; this.exitCall = undefined; }; AppDProxy.prototype.release = function(proxied) { if(!proxied) return; var info = proxied.__appdynamicsProxyInfo__; if (!info) return; info.obj[info.meth] = info.orig; }; AppDProxy.prototype.getSymbolProperty = function(obj, symbolDesc) { for (const s of Object.getOwnPropertySymbols(obj)) { const desc = s.toString().replace(/Symbol\((.*)\)$/, '$1'); if (desc === symbolDesc) { return obj[s]; } } }; AppDProxy.prototype.before = function(obj, meths, hook, isCallbackHook, copyAllProps, methsInvocationCtxt) { var self = this; if(!obj) return false; if(!Array.isArray(meths)) meths = [meths]; meths.forEach(function(meth) { var orig = obj[meth]; if(!orig) return; var beforeExecLogic = function() { var currentCtxt = self.agent.thread.current(); try { var args = [...arguments]; var methsInvocationCtxtLocal = methsInvocationCtxt; if (methsInvocationCtxtLocal) self.agent.thread.resume(methsInvocationCtxtLocal); if(isCallbackHook) { var selfProxy = this; // the hook code should contain try/catch args = hook(this, args, function() { return orig.apply(selfProxy, args); }) || args; } else { try { args = hook(this, args) || args; } catch (e) { self.logError(e); } var retValue = orig.apply(this, args); return retValue; } } finally { self.agent.thread.resume(currentCtxt); } }; defineProperty(obj, meth, self.getArityFunction(orig.length, beforeExecLogic)); if(copyAllProps) copyObjectProps(orig, obj[meth]); obj[meth].__appdynamicsProxyInfo__ = { obj: obj, meth: meth, orig: orig }; }); }; AppDProxy.prototype.after = function(obj, meths, hook, copyAllProps, methsInvocationCtxt) { var self = this; if(!obj) return false; if(!Array.isArray(meths)) meths = [meths]; meths.forEach(function(meth) { var orig = obj[meth]; if(!orig) return; var afterExecLogic = function() { var currentCtxt = self.agent.thread.current(); try { var methsInvocationCtxtLocal = methsInvocationCtxt; if (methsInvocationCtxtLocal) self.agent.thread.resume(methsInvocationCtxtLocal); var ret = orig.apply(this, arguments); var hookRet; try { hookRet = hook(this, [...arguments], ret); } catch (e) { self.logError(e); } return hookRet || ret; } finally { self.agent.thread.resume(currentCtxt); } }; defineProperty(obj, meth, self.getArityFunction(orig.length, afterExecLogic)); if(copyAllProps) copyObjectProps(orig, obj[meth]); obj[meth].__appdynamicsProxyInfo__ = { obj: obj, meth: meth, orig: orig }; }); }; AppDProxy.prototype.isPromiseSupported = function() { // Promises are supported in Node v0.12 and above. if (parseInt(process.versions.node.split('.')[0], 10) === 0 && parseInt(process.versions.node.split('.')[1], 10) < 12) return false; return true; }; AppDProxy.prototype.promise = function(returnVal, promiseHook, obj, methodArgs, locals) { // For unsupported node versions return. if (!this.isPromiseSupported()) return; // A Promise is always 'thenable'. // "thenable" is an object or function that defines a then method. // Here is thread, with a discussion on how to determine if an Object is a Promise // https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise if (!returnVal || typeof (returnVal.then) !== 'function' || returnVal instanceof EventEmitter) return; // For Promises return returnVal.then(function resolve(data) { promiseHook(obj, methodArgs, { __appdynamicsIsPromiseResult__: true, error: null, data: data }, locals); return data; }).catch(function(err) { promiseHook(obj, methodArgs, { __appdynamicsIsPromiseResult__: true, error: err, data: null }, locals); return Promise.reject(err); }); }; AppDProxy.prototype.around = function(obj, meths, hookBefore, hookAfter, copyAllProps, methsInvocationCtxt) { var self = this; if(!obj) return false; if(!Array.isArray(meths)) meths = [meths]; meths.forEach(function(meth) { var orig = obj[meth]; if(!orig) return; defineProperty(obj, meth, function() { var currentCtxt = self.agent.thread.current(); try { var args = [...arguments]; var methsInvocationCtxtLocal = methsInvocationCtxt; if (methsInvocationCtxtLocal) self.agent.thread.resume(methsInvocationCtxtLocal); var locals = new Locals(); try { args = hookBefore(this, args, locals) || args; } catch (e) { self.logError(e); } finally { // If methsInvocationCtxtLocal is undefined, preserve the thread context in which the // interceptor function is called. // Make sure the original function and after function is executed in the same context. if (!methsInvocationCtxtLocal && locals.time && locals.time.threadId) { methsInvocationCtxtLocal = locals.time.threadId; self.agent.thread.resume(methsInvocationCtxtLocal); } } var ret = orig.apply(this, args); var promiseRet = self.promise(ret, hookAfter, this, args, locals, methsInvocationCtxtLocal); if (promiseRet) { self.agent.thread.resume(currentCtxt); return promiseRet; } var hookRet; try { hookRet = hookAfter(this, args, ret, locals); } catch (e) { self.logError(e); } return hookRet || ret; } finally { self.agent.thread.resume(currentCtxt); } }); if(copyAllProps) copyObjectProps(orig, obj[meth]); obj[meth].__appdynamicsProxyInfo__ = { obj: obj, meth: meth, orig: orig }; }); }; AppDProxy.prototype.callback = function(args, pos, hookBefore, hookAfter, methsInvocationCtxt) { var self = this; if(!args) return false; if(args.length <= pos) return false; if(pos === -1) pos = args.length - 1; var orig = (typeof args[pos] === 'function') ? args[pos] : undefined; if(!orig) return false; args[pos] = function appd_proxy() { var currentCtxt = self.agent.thread.current(); try { if (methsInvocationCtxt) self.agent.thread.resume(methsInvocationCtxt); if(hookBefore) { try { hookBefore(this, arguments); } catch(e) { self.logError(e); } } var ret = orig.apply(this, arguments); if(hookAfter) { try { hookAfter(this, arguments, ret); } catch(e) { self.logError(e); } } return ret; } finally { self.agent.thread.resume(currentCtxt); } }; if(self.callContextEnabled) { var threadProxy = self.getThreadProxy(args[pos]); if(threadProxy) { args[pos] = threadProxy; } } // this is needed for removeListener args[pos].__appdynamicsProxy__ = orig; return true; }; AppDProxy.prototype.extendFunctionConstructor = function(base, hook, copyAllProps) { var self = this; class wrapper extends base { constructor(...args) { super(...args); try { hook(this, args); } catch(e) { self.logError(e); } } } if (copyAllProps) { copyObjectProps(base.prototype, wrapper.prototype); } return wrapper; }; AppDProxy.prototype.getter = function(obj, props, hook) { var self = this; if(!Array.isArray(props)) props = [props]; props.forEach(function(prop) { var orig = obj.__lookupGetter__(prop); if(!orig) return; obj.__defineGetter__(prop, function() { var ret = orig.apply(this, arguments); try { hook(this, ret); } catch(e) { self.logError(e); } return ret; }); }); }; AppDProxy.prototype.getErrorObject = function(args) { if(args && args.length > 0 && args[0]) { if(typeof(args[0]) === 'object' || typeof(args[0]) === 'string') { return args[0]; } else { return 'unspecified'; } } return undefined; }; AppDProxy.prototype.logError = function(err) { this.agent.logger.error(err); }; /*eslint-disable */ AppDProxy.prototype.getArityFunction = function(arity, functionLogic) { var returnFunc; switch(arity) { case 0: returnFunc = function () { return functionLogic.apply(this, arguments); }; break; case 1: returnFunc = function (a) { return functionLogic.apply(this, arguments); }; break; case 2: returnFunc = function (a, b) { return functionLogic.apply(this, arguments); }; break; case 3: returnFunc = function (a, b, c) { return functionLogic.apply(this, arguments); }; break; case 4: returnFunc = function (a, b, c, d) { return functionLogic.apply(this, arguments); }; break; case 5: returnFunc = function (a, b, c, d, e) { return functionLogic.apply(this, arguments); }; break; case 6: returnFunc = function (a, b, c, d, e, f) { return functionLogic.apply(this, arguments); }; break; case 7: returnFunc = function (a, b, c, d, e, f, g) { return functionLogic.apply(this, arguments); }; break; case 8: returnFunc = function (a, b, c, d, e, f, g, h) { return functionLogic.apply(this, arguments); }; break; case 9: returnFunc = function (a, b, c, d, e, f, g, h, i) { return functionLogic.apply(this, arguments); }; break; case 10: returnFunc = function (a, b, c, d, e, f, g, h, i, j) { return functionLogic.apply(this, arguments); }; break; default: this.agent.logger.warn('Experienced a high arity function with arity of ', arity); returnFunc = function () { return functionLogic.apply(this, arguments); }; break; } return returnFunc; }; /*eslint-enable */ function defineProperty(obj, name, value) { var enumerable = !!obj[name] && obj.propertyIsEnumerable(name); Object.defineProperty(obj, name, { configurable: true, enumerable: enumerable, writable: true, value: value }); } function copyObjectProps(source, destination) { var methodProps = Object.getOwnPropertyNames(source); for(var i = 0; i < methodProps.length; i++) { if(Object.getOwnPropertyDescriptor(source, methodProps[i]).writable) { destination[methodProps[i]] = source[methodProps[i]]; } } }