ca-apm-probe
Version:
CA APM Node.js Agent monitors real-time health and performance of Node.js applications
382 lines (335 loc) • 11.5 kB
JavaScript
/**
* Copyright (c) 2015 CA. All rights reserved.
*
* This software and all information contained therein is confidential and proprietary and
* shall not be duplicated, used, disclosed or disseminated in any way except as authorized
* by the applicable license agreement, without the express written permission of CA. All
* authorized reproductions must be marked with this language.
*
* EXCEPT AS SET FORTH IN THE APPLICABLE LICENSE AGREEMENT, TO THE EXTENT
* PERMITTED BY APPLICABLE LAW, CA PROVIDES THIS SOFTWARE WITHOUT WARRANTY
* OF ANY KIND, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT WILL CA BE
* LIABLE TO THE END USER OR ANY THIRD PARTY FOR ANY LOSS OR DAMAGE, DIRECT OR
* INDIRECT, FROM THE USE OF THIS SOFTWARE, INCLUDING WITHOUT LIMITATION, LOST
* PROFITS, BUSINESS INTERRUPTION, GOODWILL, OR LOST DATA, EVEN IF CA IS
* EXPRESSLY ADVISED OF SUCH LOSS OR DAMAGE.
*/
'use strict';
//CA Code start
var createNamespace = require('cls-hooked').createNamespace;
var getNamespace = require('cls-hooked').getNamespace;
var caCtxStorage = createNamespace('__ca_ctx');
var logger = require("./logger.js");
// CA Code end
var kContextPropertyName = '__STRONGOPS_CONTEXT__';
var agent = null;
function before(object, method, hook) { around(object, method, hook); }
function after(object, method, hook) { around(object, method, null, hook); }
function around(object, method, before, after) {
if (method instanceof Array) {
method.forEach(function(method) { around(object, method, before, after); });
return;
}
var all = descriptors(object, function(_, des) {
return des.configurable === true && 'function' === typeof(des.value);
});
var des = all[method];
if (des == null) {
return; // No such property.
}
var target = des.value;
if (target == null) {
return; // Getter or setter, don't touch.
}
if (isproxy(target)) {
var proxy = target; // Already patched.
} else {
// Reuse a proxy if one already exists, else create a new one.
des = first(all, function(_, des) {
return des && des.value && des.value[kContextPropertyName] &&
des.value[kContextPropertyName].target === target;
});
proxy = des ? des.value : wrap(target);
}
var context = proxy[kContextPropertyName];
if (before && context.before.indexOf(before) === -1) {
context.before.push(before);
}
if (after && context.after.indexOf(after) === -1) {
context.after.push(after);
}
context.forward = recompile(context);
each(all, function(method, des) {
// This check filters out getters and that's intentional: a getter may have
// observable side effects and its return value need not be constant over
// time. It's essentially a black box that can't be inspected.
if (des && des.value === target) install(object, method, des, proxy);
});
}
function callback(args, index, before, after) {
if (index === -1) {
index = args.length;
while (--index >= 0 && typeof(args[index]) !== 'function')
;
if (index === -1) return;
}
var target = args[index];
var proxy = wrap(target, function(recv, args, context) {
if (before) {
try {
before(recv, args, context.storage);
} catch (e) {
logProbeErrorMessage(e, recv, context.target, true);
}
}
var rval = target.apply(recv, args);
if (after) {
try {
after(recv, args, rval, context.storage);
} catch (e) {
logProbeErrorMessage(e, recv, context.target, null, true);
}
}
return rval;
}, true);
args[index] = proxy;
}
function getter(object, method, hook) {
if (method instanceof Array) {
method.forEach(function(method) { getter(object, method, hook) });
return;
}
var des = descriptor(object, method);
if (des == null) {
return;
}
if (des.get == null) {
return; // Not a getter.
}
if (des.configurable === false) {
return; // Immutable property.
}
if (isproxy(des.get)) {
var proxy = des.get;
} else {
var target = des.get;
var proxy = wrap(target);
var getters = descriptors(object, function(_, des) {
return des && des.get && des.get === target && des.configurable === true;
});
each(getters, function(method, des) {
Object.defineProperty(object, method, {
configurable: true,
enumerable: des.enumerable,
get: proxy,
});
});
}
var context = proxy[kContextPropertyName];
if (context.after.indexOf(hook) === -1) {
context.after.push(hook);
}
context.forward = recompile(context);
}
function init(agent_) { agent = agent_; }
function each(dict, cb) {
Object.getOwnPropertyNames(dict)
.forEach(function(key) { cb(key, dict[key]); });
}
function first(dict, pred) {
try {
each(dict, function(key, value) {
if (pred(key, value)) throw value;
});
} catch (value) {
return value;
}
}
function descriptors(object, filter) {
function collect(object, collected) {
if (object == null || object === Array.prototype ||
object === Date.prototype || object === Function.prototype ||
object === Object.prototype) {
return collected;
}
collect(Object.getPrototypeOf(object), collected);
Object.getOwnPropertyNames(object).forEach(function(key) {
try {
var des = Object.getOwnPropertyDescriptor(object, key);
if (filter(key, des)) collected[key] = des;
} catch(err) {
}
});
return collected;
}
return collect(object, Object.create(null));
}
function descriptor(object, method) {
do {
var des = Object.getOwnPropertyDescriptor(object, method);
object = Object.getPrototypeOf(object);
} while (des == null && object != null);
return des;
}
function isproxy(fun) {
return fun && fun.hasOwnProperty(kContextPropertyName);
}
function wrap(target, forward, isCallback) {
if (isproxy(target)) {
return target;
}
sustainabilityData.incrementWrappedFunctionsCount();
var context = {
forward: forward,
target: target,
storage: caCtxStorage,
before: [],
after: [],
};
var recomped = false;
if (context.forward == null) {
recomped = true;
context.forward = recompile(context);
}
var proxy = {};
// Generate a function with the same function name as the target.
if (target.name === "" || typeof target.name === 'undefined') {
var source = ' (function(context) { \n return function ' + target.name + '() { \n var rval = null; \n var obj = this; \n var args = arguments; \n context.storage.run(function(){ \n rval = context.forward(obj, args, context); \n }); \n return rval; \n }; \n }); \n';
proxy = eval(source)(context);
}
else {
proxy = function () {
var rval = null;
var obj = this;
var args = arguments;
context.storage.run(function () {
rval = context.forward(obj, args, context);
});
return rval;
};
}
if (!isCallback) {
Object.defineProperty(proxy, kContextPropertyName, {value: context});
// Make stringification yield the source of the target function.
Object.defineProperty(proxy, 'toString', {
configurable: true,
enumerable: false,
writable: true,
value: function () {
return '' + proxy[kContextPropertyName].target
},
});
//console.log((new Error()).stack);
//console.log(target.name + ":" + recomped + ":" + proxy.toString());
// Copy method annotations from the target (http, shared etc.)
Object.getOwnPropertyNames(target).forEach(function caDefineProperty(k) {
var des = Object.getOwnPropertyDescriptor(target, k);
if (des.configurable) {
Object.defineProperty(proxy, k, des);
}
});
}
return proxy;
}
function recompile(context) {
var before = {values: context.before};
before.argnames = before.values.map(function caArgNames(_, i) { return 'before' + i });
before.funcalls = before.values.map(function caFunCalls(f, i) {
var args = (f.length === 1 ? '(recv)' : '(recv, args, context.storage)');
return before.argnames[i] + args;
});
var after = {values: context.after};
after.argnames = after.values.map(function(_, i) { return 'after' + i });
after.funcalls = after.values.map(function(f, i) {
var args = (f.length === 2 ? '(recv, rval)' : '(recv, args, rval, context.storage)');
return after.argnames[i] + args;
});
var source = require('util').format(
' (function(%s) { \n return function(recv, args) { \n try{%s;}catch(e){logProbeErrorMessage(e, recv, context.target, true);} \n var rval = context.target.apply(recv, args); \n try{%s;}catch(e){logProbeErrorMessage(e, recv, context.target, null, true);} \n return rval; \n }; \n }) \n' ,
['context'].concat(before.argnames).concat(after.argnames).join(','),
before.funcalls.join(';\n'), after.funcalls.join(';\n'));
var args = [context].concat(before.values).concat(after.values);
return eval(source).apply(null, args);
}
function install(object, method, des, proxy) {
if (des.configurable === false) {
return;
}
var newdes = {
configurable: true,
enumerable: des.enumerable,
writable: des.writable,
value: proxy,
};
Object.defineProperty(object, method, newdes);
}
//handler for error in probe execution logic
function logProbeErrorMessage(ex, object, fun, before, after) {
var message = ex.toString() || ex.message || '';
var funName = findFunctionName(fun, 'unknown');
var className = findClassName(object, 'unknown');
var hookType = before ? 'before' : 'after';
logger.error("got exception: '%s' while executing '%s' trace logic in function: '%s#%s'",
message, hookType, className, funName);
if (ex instanceof Error) {
var stackTrace = ex.stack;
if (stackTrace) {
logger.debug(ex.stack);
}
}
}
function findFunctionName(fun, defaultName) {
var name = '';
if (fun && typeof fun === 'function') {
name = fun.name;
if (!name) {
// figure out function name from src
name = fun.toString();
if (name) {
name = name.substr('function'.length + 1);
name = name.substr(0, name.indexOf('('));
}
}
}
return name || defaultName;
}
function findClassName(object, defaultName) {
var name = '';
if (object) {
if (typeof object === 'function') {
name = findFunctionName(object, defaultName);
} else if (typeof object.constructor === 'function') {
name = findFunctionName(object.constructor, defaultName);
}
}
return name || defaultName;
}
function SustainabilityData() {
//wrapped functions count per interval
this.wrapFunctionsCount = 0;
// total wrapped functions
this.wrapFunctionsTotal = 0;
this.collectMetrics = function() {
//update total
this.wrapFunctionsTotal += this.wrapFunctionsCount;
// fetch current metrics
var metrics = [this.wrapFunctionsCount, this.wrapFunctionsTotal];
//reset per interval variable
this.wrapFunctionsCount = 0;
return metrics;
};
this.incrementWrappedFunctionsCount = function() {
this.wrapFunctionsCount++;
};
}
var sustainabilityData = new SustainabilityData();
module.exports = {
after: after,
around: around,
before: before,
callback: callback,
getter: getter,
init: init,
sustainabilityData: sustainabilityData
};