ca-apm-probe
Version:
CA APM Node.js Agent monitors real-time health and performance of Node.js applications
511 lines (431 loc) • 17 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.
*/
if ('CAAPMPROBE' in global) {
module.exports = global.CAAPMPROBE;
return;
}
var assert = require('assert');
var fs = require('fs');
var util = require('util');
var events = require('events');
var semver = require('semver');
var path = require('path');
var metrics = require('./metrics');
var metricsReporter = metrics.getReporter();
var metricTypes = require('./metric-types');
var json = require('./json');
var proxy = require('./proxy');
var sender = require('./sender');
var info = require('./info');
var moduleDetector = require('./module-detector');
var logger = require("./logger.js");
var commonUtil = require('./utils/common-utils');
// metric path prefixes
const SUSTAIN_METRIC_PREFIX = 'Agent Stats|Sustainability:'
function Agent() {
events.EventEmitter.call(this);
this.started = false;
// Setup default config for apps that just call .use()
this.cpuinfo = require('./cpuinfo');
this.internal = new events.EventEmitter;
this.internal.send = this.internal.emit.bind(this.internal, 'send');
this.probeMap = new Object;
this.moduleIndex =0;
this.probeName = "NodeApplication";
}
util.inherits(Agent, events.EventEmitter);
//----------
//PBDCONFIG
//Config - collector agent communication
//
Agent.prototype.setAsynchConfigEventHandlers = function(send, receive, repost) {
this.asynchEventConfigSendHandler = send;
this.asynchEventConfigReceiveHandler = receive;
this.asynEventConfigRepostRequireHandler = repost;
};
Agent.prototype.asynchEventSendRequire= function(moduleName, args) {
if (this.asynchEventConfigSendHandler != undefined && this.asynchEventConfigSendHandler != null) {
return this.asynchEventConfigSendHandler(moduleName, args);
}
};
Agent.prototype.asynchEventReceiveRequire= function(moduleName) {
if (this.asynchEventConfigReceiveHandler != undefined && this.asynchEventConfigReceiveHandler != null) {
return this.asynchEventConfigReceiveHandler(moduleName, args);
}
};
Agent.prototype.asynchEventRepostRequire= function(eventData) {
if (this.asynEventConfigRepostRequireHandler != undefined && this.asynEventConfigRepostRequireHandler != null) {
return this.asynEventConfigRepostRequireHandler(eventData);
}
};
Agent.prototype.addToProbeMap= function(moduleNameIndex, probe) {
this.probeMap[moduleNameIndex] = probe;
};
Agent.prototype.getFromProbeMap= function(moduleNameIndex) {
return this.probeMap[moduleNameIndex];
};
Agent.prototype.getNextModuleIndex= function() {
return this.moduleIndex++;
};
//------------
/**
* @deprecated since version 1.10.51
*/
Agent.prototype.setMetricEventHandlers = function(send) {
this.metricSendHandler = send;
};
/**
* @deprecated since version 1.10.51
*/
Agent.prototype.metricEventSend= function(metric) {
if (this.metricSendHandler) {
return this.metricSendHandler(metric);
}
};
Agent.prototype.setAsynchEventModelHandlers = function(start, done, finish) {
this.asynchEventStartHandler = start;
this.asynchEventDoneHandler = done;
this.asynchEventFinishHandler = finish;
};
Agent.prototype.asynchEventStart = function(ctx, name, args) {
if (this.asynchEventStartHandler != undefined && this.asynchEventStartHandler != null) {
return this.asynchEventStartHandler(ctx, name, args);
}
};
Agent.prototype.asynchEventDone = function(ctx, name, args, errorObj) {
if (this.asynchEventDoneHandler != undefined && this.asynchEventDoneHandler != null) {
return this.asynchEventDoneHandler(ctx, name, args, errorObj);
}
};
Agent.prototype.asynchEventFinish = function(ctx) {
if (this.asynchEventFinishHandler != undefined && this.asynchEventFinishHandler != null) {
return this.asynchEventFinishHandler(ctx);
}
};
Agent.prototype.checkAndSetErrorObject = function(args,errorProbe) {
var errorObject = null;
var message = "";
if(args[0] != null){
for(var key in args[0])
{
if(typeof args[0][key] != "object"){
message += key + ": " + args[0][key] + " , ";
}
}
errorObject = {
class : errorProbe,
msg: message.substr(0, message.length-3)
};
}
return errorObject;
};
Agent.prototype.licensed = function(feature) {
return true;
};
Agent.prototype.configure = function(options) {
this.options = options;
};
Agent.prototype.start = function() {
if (this.started) return;
proxy.init(this);
sender.init(this);
info.init(this);
this.prepareProbes();
this.preparePoll(this.options.interval);
this.started = true;
};
Agent.prototype.stop = function() {
this.started = false;
};
Agent.prototype.prepareProbes = function() {
var probes = {}, wrapping_probes = {};
var probe_files = fs.readdirSync(__dirname + '/probes');
//var wrapper_files = fs.readdirSync(__dirname + '/wrapping-probes');
probe_files.forEach(function(file) {
var m = file.match(/^(.*)+\.js$/);
if (m && m.length == 2) probes[m[1]] = true;
});
if (this.options.hasOwnProperty("appVersions")) {
var appSupportedVersions = {};
var appUnsupportedVersions = {};
var appVersionsObj = JSON.parse(this.options.appVersions);
Object.keys(appVersionsObj).forEach(function (key) {
if (probes[key]) {
appSupportedVersions[key] = appVersionsObj[key];
} else {
appUnsupportedVersions[key] = appVersionsObj[key];
}
});
this.options.appSupportedVersions = JSON.stringify(appSupportedVersions);
this.options.appUnsupportedVersions = JSON.stringify(appUnsupportedVersions);
if (appSupportedVersions) {
this.options.appSupportedVersions = this.options.appSupportedVersions.replace(/['"]+/g, '');
logger.info("[CA APM PROBE] Application Dependency Supported Versions: " + this.options.appSupportedVersions);
}
if (appUnsupportedVersions) {
this.options.appUnsupportedVersions = this.options.appUnsupportedVersions.replace(/['"]+/g, '');
logger.info("[CA APM PROBE] Application Dependency Unsupported Versions: " + this.options.appUnsupportedVersions);
}
}
//wrapper_files.forEach(function(file) {
// var m = file.match(/^(.*)+\.js$/);
// if (m && m.length == 2) wrapping_probes[m[1]] = true;
//});
var original_require = module.__proto__.require;
var agent = this;
module.__proto__.require = function(name) {
// var modName = path.basename(name);
// var dirName = path.dirname(name);
// if (path.basename(dirName) == 'probes') {
// logger.info("Ooops.")
// modName = name;
// }
// var sqlflag = false;
// if (name.indexOf('mysql') !== -1) {
// logger.info('require: ' + name + ' => ' + dirName +'+'+ modName+'.');
// console.trace();
// sqlflag = true;
// }
// if (name === './http') {
// logger.info('fixing http');
// modName = name;
// }
var modName = name;
var args = Array.prototype.slice.call(arguments);
if (name.indexOf('cls-hooked') !== -1) {
logger.debug('require: %s', name);
//console.trace();
}
//probes['loopback-datasource-juggler'] = null;
var target_module = original_require.apply(this, args);
// skip instrumenting ourselves
// TODO - may be we need to be more specific on detecting probe modules
if (name.includes('./probes')){
return target_module;
}
try{
if (args.length == 1 && target_module && (!Object.prototype.hasOwnProperty.call(target_module, '__required__') || !target_module.__required__)) {
if (wrapping_probes[modName]) {
target_module.__required__ = true;
target_module = require('./wrapping-probes/' + modName)(
target_module);
} else {
if (probes[modName]) {
var message = util.format('Noticed module: %s', modName);
if (target_module.version) {
message += util.format(', version: %s',
target_module.version);
}
logger.info(message);
var probe = require('./probes/' + modName);
var target_module_bkp = probe(target_module);
if(target_module_bkp && target_module_bkp !== target_module){
target_module = target_module_bkp;
}
target_module.__required__ = true;
target_module.__ca_apm_probe_mod_id__ = modName;
updatePbdConfig(probe, modName, target_module);
}
// Disabled custom probe support
/*
var matches = false; // do we have a matching probe
var simpleName = getSimpleName(modName);
if (probes[simpleName]) {
var probe = require('./probes/' + simpleName);
var isSpec2Probe = (typeof probe === "function" && probe.length === 2);
// we pass all matching modules to 2.0 spec probe
if (isSpec2Probe) {
// pass any additional information to probe for making instrumentation decision
probe(target_module, modName);
matches = true;
} else {
// apply 1.0 spec probes only if module loaded by id.
// so it instruments
// var x = require('http') // loaded from core
// var y = require('mongodb') // loaded from node_modules
// but skips var z = require('./util/http.js') // loaded from relative or absolute file/folder
if (isLoadedById(modName)) {
// apply probe for module loaded by id
// examples- require('http'), require('mongodb')
probe(target_module);
matches = true;
}
}
if (matches) {
target_module.__required__ = true;
target_module.__ca_apm_probe_mod_id__ = simpleName;
var message = util
.format('Noticed module: %s', modName);
if (target_module.version) {
message += util.format(', version: %s',
target_module.version);
}
logger.info(message);
updatePbdConfig(probe, modName, target_module);
}
}*/
}
}
} catch(e){
// console.log("Module " + modName + " is not instrumented");
}
return target_module;
};
};
function getSimpleName(modName) {
if (modName.includes('/')) {
var lastName = modName.substring(modName.lastIndexOf('/') + 1);
var m = lastName.match(/^(.*)+\.js$/);
if (m && m.length == 2) {
lastName = m[1];
}
return lastName;
}
return modName;
}
function isLoadedById(modName) {
return !(modName.includes('/'));
}
function updatePbdConfig(probe, name, target_module) {
probe.targetModule = target_module;
if (probe.getMethodsWithProbes != undefined
&& probe.getMethodsWithProbes != null) {
logger.info('Probe %s can be controlled by PBD', name);
// get methods in probe
var methodArray = probe.getMethodsWithProbes();
if (methodArray != undefined && methodArray != null) {
// push probe into probe map
var uniqueName = name + CAAPMPROBE.getNextModuleIndex();
CAAPMPROBE.addToProbeMap(uniqueName, probe);
// call collector
CAAPMPROBE.asynchEventSendRequire(uniqueName, methodArray);
}
}
}
Agent.prototype.poll = function() {
var data;
this.emit('poll::start');
info.poll(); // Returns nothing, recorded as CPU/heap/connection metrics.
if (data = metrics.poll()) {
for (var key in data) {
this.internal.emit('metric', data[key]);
}
}
this.emit('poll::stop');
};
Agent.prototype.preparePoll = function(baseInterval) {
setInterval(this.poll.bind(this), baseInterval).unref();
};
// Returns `this` on success or undefined on error for parity with .profile().
Agent.prototype.use = function(callback) {
this.start();
this.internal.on('stats', function(stat, value, type) {
switch (type) {
case 'timer':
return callback(stat + '.timer', value / 1e6 /*ns->ms*/);
case 'count':
return callback(stat + '.count', value);
default:
return callback(stat, value);
}
});
this.internal.on('send', function(name, value) {
if (value == null) return; // Should never happen.
var resMetricPrefix = commonUtil.getResMetricPrefix();
if (name === 'update' && value.name === 'CPU util') {
callback('cpu.total', commonUtil.fix(value.value));
var cpuTot = value.value;
metricsReporter.reportFluctuatingCounterMetric(resMetricPrefix + 'CPU Total (%)', cpuTot);
return;
}
if (name === 'update' && value.name === 'CPU util stime') {
callback('cpu.system', commonUtil.fix(value.value));
var cpuSys = value.value;
metricsReporter.reportFluctuatingCounterMetric(resMetricPrefix + 'CPU System (%)', cpuSys);
return;
}
if (name === 'update' && value.name === 'CPU util utime') {
callback('cpu.user', commonUtil.fix(value.value));
var cpuUser = value.value;
metricsReporter.reportFluctuatingCounterMetric(resMetricPrefix + 'CPU User (%)', cpuUser);
return;
}
if (name === 'update' && value.name === 'Heap Data') {
var mem = value.value;
var rss = mem.rss || 0;
var heapTotal = mem.heapTotal || 0;
var heapUsed = mem.heapUsed || 0;
var heapUsedPercent = 0;
if (heapTotal !== 0) {
heapUsedPercent = heapUsed / heapTotal * 100;
}
callback('rss', rss);
metricsReporter.reportFluctuatingCounterMetric(resMetricPrefix + 'Resident Set Size', rss);
callback('heap.total', heapTotal);
metricsReporter.reportFluctuatingCounterMetric(resMetricPrefix + 'Heap Total', heapTotal);
callback('heap.used', heapUsed);
metricsReporter.reportFluctuatingCounterMetric(resMetricPrefix + 'Heap Used', heapUsed);
callback('heap.usedpercent', heapUsedPercent);
metricsReporter.reportFluctuatingCounterMetric(resMetricPrefix + 'Heap Used (%)', heapUsedPercent);
return;
}
if (name === 'update' && value.name === 'Connections') {
// Index 0 is the number of open connections.
// Index 1 is the interval in sec.
// Index 2 is the number of new connections in the last interval.
// Index 3 is the number of new connections in the interval before that.
var curr = value.value[2] | 0;
var prev = value.value[3] | 0;
var interval = value.value[1] | 0;
var count = prev > curr ? 0 : (curr - prev);
var tps = interval == 0 ? 0 : count/interval;
var connsec = roundToInt(tps);
callback('http.connection.count', count);
metricsReporter.reportIntervalCounterMetric(resMetricPrefix + 'HTTP Connection Count', count);
metricsReporter.reportIntervalCounterMetric(resMetricPrefix + 'HTTP Connections Per Second', connsec);
return;
}
if (name === 'update' && value.name === 'Sustainability') {
// Index 0 is the number of function redefinitions/instrumentations per interval.
// Index 1 is total number of redefinitions
var countPerInterval = value.value[0] | 0;
metricsReporter.reportIntervalCounterMetric(SUSTAIN_METRIC_PREFIX + 'Instrumented Functions Per Interval', countPerInterval);
if (countPerInterval > 0) {
var countTotal = value.value[1] | 0;
metricsReporter.reportFluctuatingCounterMetric(SUSTAIN_METRIC_PREFIX + 'Instrumented Functions Total', countTotal);
}
return;
}
}.bind(this));
return this;
};
Agent.prototype.metric = function(scope, name, value, unit, op, persist) {
if (!this.started) return;
metrics.add(scope, name, value, unit, op, persist);
};
Agent.prototype.setProbeName = function(nameStr) {
this.probeName = nameStr;
};
Agent.prototype.getProbeName = function() {
return this.probeName;
};
function roundToInt(n){ return Math.round(Number(n)); };
module.exports = new Agent;
module.exports.Agent = Agent;
module.exports.require = require;
Object.defineProperty(global, 'CAAPMPROBE', {value: module.exports});