UNPKG

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
/** * 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 };