ca-apm-probe
Version:
CA APM Node.js Agent monitors real-time health and performance of Node.js applications
664 lines (537 loc) • 18.7 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.
*/
var http2 = require('http2');
var os = require('os');
var path = require('path');
var fs = require('fs');
var util = require('util');
var semver = require('semver');
var logger = require("./logger.js");
var config = require('../config.json');
var probeUtil = require('./utils/common-utils');
var virtualstack = require('./virtualstack');
var collectorMsg = require("./collectormsg");
var pid = process.pid;
var instanceId = util.format('%s-%s', os.hostname(), pid);
var maxPingDelay = process.env.MAX_PING_DELAY || config.maxPingDelay || 15000;
var lastPingResponseTime = new Date().getTime();
var reconnectAttempt = 1;
var tryReconnect = config.tryReconnect || false;
var collectorHostDefault = 'localhost';
var collectorPortDefault = 5005;
var buff = [];
var currBuffSize = 0;
var maxAllowedBuffSize = 50000;
var buffFull = false;
var probeNameDefault = 'NodeApplication';
var currentHost = collectorHostDefault;
var currentPort = collectorPortDefault;
var currentProbeName = probeNameDefault;
var http2Client = null;
var isCommandConnected = false;
var isDataConnected = false;
var speakPingProbeTimer = null;
var ArfMessageParser = require('./arf-parser').ArfMessageParser;
var arfMessageParser = null;
var logTraceEventStartFlag = true;
var logTraceEventEndFlag = true;
var logMetricsSendFlag = true;
// latest arf protocol version this probe complies to
var ARFP_VERSION = '1.1.0';
// minimum compatible collector version for metric reporting purpose
var METRICS_COMPATIBILITY = { minCollectorVersion: '10.5.2', minArfpVersion: '1.1.0' };
var metricsEnableDelay = 5000;
var metricsReportingEnabled = false;
var checkMetricsCompat = config.metrics.enabled && config.metrics.checkCompatibility;
var configPath = path.resolve(__dirname,'../config.json');
var reportedAttributes = {};
var dataConnMsg = function () {
var object = {
op: 'dataConn',
probe: 'nodejs',
ver: ARFP_VERSION,
pid: pid,
instid: instanceId
};
return object;
};
var commandConnMsg = function () {
var object = {
op: 'commandConn',
probe: 'nodejs',
ver: ARFP_VERSION,
instid: instanceId,
pgm: currentProbeName,
prms: { appName: resolveAppName() }
};
var hostName = resolveHostName();
if (hostName)
object.prms.hostName = hostName;
var containerId = require('./find-container-id')();
if (containerId)
object.prms.containerId = containerId;
if(!containerId){
require('./pod-details')(object);
}
return object;
};
var arfMsg = function () {
var object = {
op: 'arf'
};
return object;
};
var resolveHostName = function () {
var hostNames = [];
// host name from env variable
hostNames[0] = process.env.CA_APM_HOSTNAME;
// from config file
hostNames[1] = config.hostName ? probeUtil.resolveEnv(config.hostName) : null;
for (var i = 0; i < hostNames.length; i++) {
if (hostNames[i]) {
logger.debug("Host Name Detection choosing the name: " + hostNames[i]);
return hostNames[i];
}
}
return null;
};
var resolveAppName = function () {
var appNames = [];
// app name from env variable
appNames[0] = process.env.CA_APM_APPNAME;
// from config file
appNames[1] = config.appName ? probeUtil.resolveEnv(config.appName) : null;
// app name from application
appNames[2] = probeUtil.findAppName();
// default app name
appNames[3] = 'NodeApplication';
for (var i = 0; i < appNames.length; i++) {
if (appNames[i]) {
logger.debug("App Name Detection choosing the name: " + appNames[i]);
return appNames[i];
}
}
return null;
};
module.exports.start = start;
function dataConnWithHeaders(message) {
return {
[http2.constants.HTTP2_HEADER_SCHEME]: "https",
[http2.constants.HTTP2_HEADER_METHOD]: http2.constants.HTTP2_METHOD_POST,
[http2.constants.HTTP2_HEADER_PATH]: '/apmia/datacollector/dataConn',
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
[http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: message.length,
"probe": 'nodejs',
"ver": ARFP_VERSION,
"instid": instanceId,
"pid": pid,
"pgm": currentProbeName,
"appName": resolveAppName()
};
}
function speakWithHeaders() {
return {
[http2.constants.HTTP2_HEADER_SCHEME]: "https",
[http2.constants.HTTP2_HEADER_METHOD]: http2.constants.HTTP2_METHOD_GET,
[http2.constants.HTTP2_HEADER_PATH]: '/apmia/datacollector/speak',
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
"probe": 'nodejs',
"ver": ARFP_VERSION,
"instid": instanceId,
"pid": pid,
"pgm": currentProbeName,
"appName": resolveAppName()
};
}
function dataConnection() {
var dataReq = sendDataConnHttp2Request( generateMsg(dataConnMsg()));
dataReq.on('response', (headers) => {
logger.info('Response for Data connection message received from collector agent');
isDataConnected = true;
console.log("Data Connection ----> isDataConnected : " + isDataConnected);
// report platform metrics once data connection is up
// and collector agent compatibility has be verified
setTimeout(reportPlatformMetrics, (metricsEnableDelay + 2000));
});
dataReq.on('data', (data) => {
console.log("Data Connection Response Received ----> data : " + data);
});
dataReq.on('end', () => {
console.log("Data Connection Ends ");
});
}
function commandConnection() {
var commReq = sendDataConnHttp2Request(generateMsg(commandConnMsg()));
commReq.on('response', (headers, flags) => {
logger.info('Response for Command connection message received from collector agent');
isCommandConnected = true;
logTraceEventStartFlag = true;
logTraceEventEndFlag = true;
logMetricsSendFlag = true;
// we need to wait for agent configuration message
setTimeout(checkMetricReportingStatus, metricsEnableDelay);
});
speakPingProbeTimer = setInterval(speakPingProbe, 2000);
commReq.on('data', (data) => {
console.log("Command Connection Response Received ----> data : " + data);
dataConnection();
});
commReq.on('end', () => {
logger.info("Command Connection Ends");
});
}
function speakPingProbe() {
var currentTime = new Date().getTime();
var speakReq = sendSpeakConnHttp2Request();
speakReq.on('data', (data) => {
console.log("Speak Response ---> data : " + data);
});
}
function sendDataConnHttp2Request(msg) {
console.log("Hello -----> " + msg);
var request = http2Client.request(dataConnWithHeaders(msg));
request.write(msg);
return request;
}
function sendSpeakConnHttp2Request() {
var request = http2Client.request(speakWithHeaders());
//request.write("");
return request;
}
function connectClient(host, port) {
var url = 'https://' + host + ':' + '8085';
var client = http2.connect( url, { rejectUnauthorized: false, ca: fs.readFileSync('ca.pem'), });
client.on('connect', (session, socket) => {
logger.info('Connection established with collector agent listening on host: %s, port: %d, protocol: %e', host, port, socket.alpnProtocol);
commandConnection();
});
client.on('socketError', (err) => {
logger.error(">>>>>>>>>>>>>>>>>>>>" + err);
});
return client;
}
function start(opts) {
currentHost = opts.collectorAgentHost || collectorHostDefault;
currentPort = opts.collectorAgentPort || collectorPortDefault;
currentProbeName = opts.probeName || probeNameDefault;
logger.debug('Trying to connect with collector agent listening on host: %s, port: %d', currentHost, currentPort);
http2Client = connectClient(currentHost, currentPort);
// on connection announce custom attributes
setTimeout(reportConfigAttributes, metricsEnableDelay+2000);
// register reporting of custom attributes at reporting interval
setInterval(reportConfigAttributes, reportingInterval());
fs.watchFile(configPath, configListener);
}
function reportingInterval(){
return config["attribute.decoration"]["reporting.interval"] || 900000 ;
}
function configListener(curr,prev){
logger.debug("config file modified, triggering reporting of custom attributes");
reportConfigAttributes();
}
function reportConfigAttributes() {
try {
delete require.cache[require.resolve(configPath)];
config = require(configPath);
commMsg = commandConnMsg();
var customAttributes = {};
// check attribute decoration is enabled
if (!config['attribute.decoration']) {
return;
}
if (config['attribute.decoration']['enable.extension'] != true) {
logger.debug("custom attribute decoration is not enabled");
return;
}
// load env attributes
var envParams = config['attribute.decoration']['env'];
if (envParams) {
for (var i = 0; i < envParams.length; i++) {
var envname = envParams[i]
customAttributes["attribute\.decoration\.env\." + envname] = process.env[envname] || "undefined"
}
}
var staticParams = config['attribute.decoration']['static'];
if (staticParams) {
Object.keys(staticParams).forEach(function (key) {
customAttributes["attribute\.decoration\.user\." + key] = staticParams[key];
});
}
var extParams = config['attribute.decoration']['ext'];
if (extParams && extParams.filepath) {
var extAttributes = readExternalAttributes(extParams.filepath);
if (extAttributes) {
Object.keys(extAttributes).forEach(function (key) {
customAttributes[key] = extAttributes[key];
});
}
}
var deletedAttr = findDeletedAttributes(reportedAttributes, customAttributes);
commMsg.prms = probeUtil.merge(commMsg.prms,customAttributes);
commMsg.prms = probeUtil.merge(commMsg.prms,deletedAttr);
// report custom attributes
if (http2Client != null) {
var dataReq = sendDataConnHttp2Request(generateMsg(commMsg));
reportedAttributes = customAttributes;
} else {
logger.warn("Unable to report custom attributes , Client unavailable");
}
} catch (e) {
logger.debug("got exception: %s , unable to process custom attributes", e.message);
}
}
// processes configuration message received from collector agent
function processConfigMessage(message) {
var obj = JSON.parse(message);
if (obj != null && obj.op == 'config') {
if (obj.cmd == 'require') {
logger.debug("received pbd configuration message for module: %s", obj.mod);
var mod = obj.mod;
var methodMap = obj.prms;
logger.debug("module method map: %s", methodMap[0]);
probeMod = CAAPMPROBE.getFromProbeMap(obj.mod);
if (probeMod != null && probeMod.instrument != undefined && probeMod.instrument != null) {
probeMod.instrument(methodMap);
}
} else if (obj.msg) {
if (typeof obj.msg === 'string') {
var m = JSON.parse(obj.msg);
// collector version 10.5.2
if (m.collector)
processCollectorVersion(m.collector.version);
}
} else if (obj.cmd == 'version') {
// collector version 10.7 and later
var params = obj.prms;
if (params.collector)
processCollectorVersion(params.collector);
if (params.arfp)
processArfpVersion(params.arfp);
}
}
}
function processCollectorVersion(version) {
logger.info("Probe connected to collector agent version: %s", version);
try {
if (checkMetricsCompat && semver.gte(version, METRICS_COMPATIBILITY.minCollectorVersion)) {
metricsReportingEnabled = true;
}
}
catch (e) {
// ignore
}
}
function processArfpVersion(version) {
logger.debug("Collector agent implements ARF protocol version: %s", version);
if (checkMetricsCompat && semver.gte(version, METRICS_COMPATIBILITY.minArfpVersion)) {
metricsReportingEnabled = true;
}
}
var generateMsg = function (object) {
var origJson = JSON.stringify(object);
return origJson + os.EOL;
};
function checkBufferSize(msg) {
if (currBuffSize + msg.length > maxAllowedBuffSize)
return 0;
else return 1;
}
// Write the buffer to the socket every 500 msecs
setInterval(function () {
if (buff.length > 0 && !buffFull) {
console.log(" Periodically --> " + isDataConnected);
if (!isDataConnected) {
logger.warn(' writeToSocketByAnonymous - Cannot send message as data connection is down.' + buff.join(os.EOL));
return;
}
var dataReq = sendDataConnHttp2Request(buff.join(os.EOL) + os.EOL);
buff = [];
currBuffSize = 0;
}
buffFull = false;
}, 500); // 500 msecs
function writeToSocket(msg) {
console.log(" --> " + msg);
var res = checkBufferSize(msg);
if (res == 1) { // space still available
buff.push(msg);
}
else {
if (!isDataConnected) {
logger.warn(' writeToSocket - Cannot send message as data connection is down.' + buff.join(os.EOL));
return;
}
buffFull = true;
var dataReq = sendDataConnHttp2Request(buff.join(os.EOL) + os.EOL);
buff = [];
currBuffSize = 0;
buff.push(msg); // clear the buff is it exceeds capacity and push the new incoming message
}
currBuffSize += msg.length;
}
function startTrace(name, time, txid, evtid, params) {
/*This method should convert the incoming invocationData to relevant object*/
//Check to find if the command connection is up: Only then proceed with the further execution of this function
if (http2Client == null) {
if (logTraceEventStartFlag) {
logger.info('Start Trace cannot be initiated since the command connection is down.');
logTraceEventStartFlag = false;
}
return;
}
var object = {
op: 'fnC',
fn: name,
ts: time,
tid: txid,
seq: evtid
};
if (params && Object.keys(params).length > 0) {
object.prms = params;
}
var msg = JSON.stringify(object);
writeToSocket(msg);
}
function endTrace(name, time, txid, evtid, params, errormsg) {
//Check to find if the command connection is up: Only then proceed with the further execution of this function
if (http2Client == null) {
if (logTraceEventEndFlag) {
logger.info('End Trace cannot be initiated since the command connection is down.');
logTraceEventEndFlag = false;
}
return;
}
var object = {
op: 'fnR',
fn: name,
ts: time,
tid: txid,
cseq: evtid
};
if (params && Object.keys(params).length > 0) {
object.prms = params;
}
if (errormsg && Object.keys(errormsg).length > 0) {
object.exc = errormsg;
}
var msg = JSON.stringify(object);
writeToSocket(msg);
}
virtualstack.on('tracer-start', function (event) {
startTrace(event.name, event.time, event.txid, event.evtid, event.params);
});
virtualstack.on('tracer-finish', function (event) {
endTrace(event.name, event.time, event.txid, event.evtid, event.params, event.errormsg);
});
module.exports.startTrace = startTrace;
module.exports.endTrace = endTrace;
function sendRequire(event) {
if (!isCommandConnected) {
logger.debug('Cannot send require event as command connection is down.');
defer(sendRequire, event);
return;
}
if (!isDataConnected) {
logger.debug('Cannot send require event as data connection is down.');
defer(sendRequire, event);
return;
}
var object = {
op: 'require',
mod: event.module
};
if (event.params) {
if (Object.keys(event.params).length > 0) {
object.prms = event.params;
}
}
var msg = generateMsg(object);
//var dataReq = sendDataConnHttp2Request(msg);
logger.debug("Sending " + msg);
}
function defer(f) { setTimeout(f.bind.apply(f, arguments), 2000); }
collectorMsg.on('send-require', function (event) {
sendRequire(event);
});
collectorMsg.on('receive-require', function (event) {
collectorMsg.receiveRequire(event);
});
collectorMsg.on('send-metric', function (metric) {
sendMetricInternal(metric);
});
function sendMetricInternal(metric) {
//Check to find if the command connection is up: Only then proceed with the further execution of this function
if (http2Client == null) {
if (logMetricsSendFlag) {
logger.debug('Metrics cannot be sent since the command connection is down.');
logMetricsSendFlag = false;
}
return;
}
if (metricsReportingEnabled) {
var msg = generateMsg(metric);
logger.debug("sending: <%s>", msg);
var dataReq = sendDataConnHttp2Request(msg);
}
};
function reportPlatformMetrics() {
var reporter = require('./metrics').getReporter();
var envInfo = require('./envinfo');
reporter.reportStringMetric('Node Version', process.version);
reporter.reportStringMetric('Platform', envInfo.getPlatformInfo());
if (envInfo.getProbePackage()) {
reporter.reportStringMetric('Probe Version', envInfo.getProbePackage().version);
}
// report collector agent info
reporter.reportStringMetric('Collector Host', currentHost);
reporter.reportStringMetric('Collector Port', currentPort);
}
function checkMetricReportingStatus() {
if (config.metrics.enabled && !metricsReportingEnabled) {
if (!config.metrics.checkCompatibility) {
metricsReportingEnabled = true;
} else {
logger.warn("Probe has disabled runtime metrics reporting because it could not validate Collector Agent version. Please check probe compatibility guide.")
}
}
}
findDeletedAttributes = function(prms,customAttributes){
var deletedAttr = {};
Object.keys(prms).forEach(function(key){
if(key && key.startsWith('attribute.decoration') && !customAttributes.hasOwnProperty(key)){
deletedAttr[key] = "null";
}
});
return deletedAttr;
}
readExternalAttributes = function(path){
var extAttributes = {};
try {
delete require.cache[require.resolve(path)];
var extConfig = require(path);
Object.keys(extConfig).forEach(function(key){
if(!probeUtil.isArray(extConfig[key]) && !probeUtil.isObject(extConfig[key])){
extAttributes["attribute\.decoration\.ext\."+key] = extConfig[key];
}
});
} catch(e){
logger.error('got exception: %s while reading attributes from path: %s', e.message, path);
}
return extAttributes;
}
module.exports.sendMetric = sendMetricInternal;