UNPKG

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