ca-apm-probe
Version:
CA APM Node.js Agent monitors real-time health and performance of Node.js applications
743 lines (622 loc) • 22.8 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 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 configData = require('./configdata')
var config = configData.getConfigData();
var probeUtil = require('./utils/common-utils');
var virtualstack = require('./virtualstack');
var collectorMsg = require("./collectormsg");
const { reportPlatformMetrics } = require("./probe-client-util");
var dataConnEndpoint = '/apmia/datacollector/dataConn';
var speakEndpoint = '/apmia/datacollector/speak';
var httpsEnabled = config.security.enabled || false;
var http = httpsEnabled ? require('https') : require('http');
var pid = process.pid;
var instanceId = util.format('%s-%s', os.hostname(), pid);
var buff = [];
var currBuffSize = 0;
var maxAllowedBuffSize = 50000;
var buffFull = false;
var currentHost;
var currentPort;
var currentProbeName;
var isConnectionInitialized = false;
var isDataConnected = false;
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 reportedAttributes = {};
var retryCommandDataConnMsgEncoded = "";
var includeRetryConnectionHeader = false;
var speakIntervalTime = config.speakInterval | 5000;
var certPath = config.security.certPath;
var signedCert = httpsEnabled ? fs.readFileSync(path.resolve(__dirname, certPath)) : "";
var cookie = null;
var cookieEnabled = config.cookieEnabled;
var commandDataConnMsg = function () {
var object = {
op: 'commandDataConn',
probe: 'nodejs',
ver: ARFP_VERSION,
instid: instanceId,
pid: pid,
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;
object.pid = containerId + "-" + pid;
}
if(!containerId){
require('./pod-details')(object);
}
return object;
};
var resolveHostName = function () {
return config.hostName;
};
var resolveAppName = function () {
return config.appName;
};
module.exports.start = start;
function requestHeaders(message, speak) {
var requestOptions = {
hostname: currentHost,
port: currentPort,
path: dataConnEndpoint,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": message.length,
"probe": 'nodejs',
"ver": ARFP_VERSION,
"instid": instanceId,
"pid": pid
}
};
if (speak) {
requestOptions.path = speakEndpoint;
requestOptions.method = "GET";
if (includeRetryConnectionHeader) {
var httpHeaders = requestOptions.headers;
httpHeaders.retryConn = retryCommandDataConnMsgEncoded;
includeRetryConnectionHeader = false;
logger.debug("Speak - Adding Retry Connection Header ");
}
}
if(cookieEnabled && cookie){
requestOptions.headers.cookie = cookie;
}
if (httpsEnabled) {
requestOptions.ca = signedCert;
}
return requestOptions;
}
function commandDataConnection() {
var cmdDataConnMsg = generateMsg(commandDataConnMsg());
logger.info("Command Data Connection Message : " + cmdDataConnMsg);
retryCommandDataConnMsgEncoded = Buffer.from(cmdDataConnMsg).toString('base64');
logger.debug("Retry Command Data Connection Message Encoded : " + retryCommandDataConnMsgEncoded);
var dataRequest = http.request(
requestHeaders(cmdDataConnMsg),
(response) => {
var statusCode = response.statusCode;
logger.debug("Data Connection Response Status code : " + statusCode);
var responseMsg = ''
response.on('data', function (chunk) {
responseMsg += chunk;
});
response.on('end', function () {
logger.info("$$$$$ Command Data Connection Response : " + responseMsg);
if (statusCode == 200) {
logTraceEventStartFlag = true;
logTraceEventEndFlag = true;
logMetricsSendFlag = true;
isDataConnected = true;
isConnectionInitialized = true;
// we need to wait for agent configuration message
processHttpConfigMessage(responseMsg);
setInterval(speakPingProbe, speakIntervalTime); //Configurable Interval
setTimeout(checkMetricReportingStatus, metricsEnableDelay);
setTimeout(reportPlatformMetrics, (metricsEnableDelay + 2000));
handleCookie(response);
} else {
setTimeout(commandDataConnection, 10000);
}
});
});
dataRequest.on('error', error => {
logger.error("Connection Initiation Message failed : " + error);
setTimeout(commandDataConnection, 10000);
});
dataRequest.write(cmdDataConnMsg);
dataRequest.end();
}
function speakPingProbe() {
var currentTime = new Date().getTime();
sendSpeakConnHttpRequest();
}
//Used for batch message sending
function sendDataConnHttpRequest(msg) {
getAndSendDataConnHttpRequest(msg, dataResponseCallback)
}
//Use this method when response has to be processed using callbacks
function getAndSendDataConnHttpRequest(msg, reqResponseCallback) {
logger.debug("Data Message ---> " + msg);
if (!isDataConnected) {
return;
}
var request = http.request(requestHeaders(msg), reqResponseCallback);
request.on('error', error => {
logger.error("Transaction Message Connection : " + error);
includeRetryConnectionHeader = true;
});
request.write(msg);
request.end();
}
function sendSpeakConnHttpRequest() {
var request = http.request(requestHeaders("", "speak"), (response) => {
var statusCode = response.statusCode;
if (statusCode != 200) {
logger.warn("Speak Response Status Code : " + statusCode);
}
var responseMsg = ''
response.on('data', function (chunk) {
responseMsg += chunk;
});
response.on('end', function () {
if (statusCode == 200) {
logger.debug("##### Speak Response : " + responseMsg);
processReponseMessageForReconnection(responseMsg);
handleCookie(response);
}
});
if (statusCode >= 400) {
includeRetryConnectionHeader = true;
}
});
request.on('error', error => {
logger.error("Speak Message Connection : " + error);
includeRetryConnectionHeader = true;
});
request.end();
}
function handleCookie(response){
if(!cookieEnabled){
return;
}
var newCookie = response.headers["set-cookie"];
if(newCookie){
newCookie = (newCookie + "").split(";").shift();
cookie = newCookie != cookie ? newCookie : cookie;
}
}
//For Transaction Message Response Callback
var dataResponseCallback = function (response) {
var statusCode = response.statusCode;
if (statusCode != 200) {
logger.debug("Transaction Response Status Code : " + statusCode);
}
var responseMsg = ''
response.on('data', function (chunk) {
responseMsg += chunk;
});
response.on('end', function () {
if (statusCode == 200) {
processReponseMessageForReconnection(responseMsg);
handleCookie(response);
}
});
if (statusCode >= 400) {
includeRetryConnectionHeader = true;
}
}
function start(opts) {
currentHost = config.infrastructureAgent.host;
currentPort = config.infrastructureAgent.port;
currentProbeName = config.probeName;
opts.currentHost = currentHost;
opts.currentPort = currentPort;
logger.info('Trying to connect with collector agent listening on host: %s, port: %d', currentHost, currentPort);
commandDataConnection();
// on connection announce custom attributes
setTimeout(reportConfigAttributes, metricsEnableDelay + 2000);
// register reporting of custom attributes at reporting interval
setInterval(reportConfigAttributes, reportingInterval());
}
function reportingInterval() {
if (config.hasOwnProperty("attribute")) {
return config.attribute.decoration.reportingInterval || 900000;
} else if (config.hasOwnProperty("attribute.decoration")) {
//It is for backward compatible
return config["attribute.decoration"]["reporting.interval"] || 900000;
} else {
return 900000;
}
}
function reportConfigAttributes() {
try {
config = configData.getConfigData();
commMsg = commandDataConnMsg();
var customAttributes = {};
// check attribute decoration is enabled
if (!config.attribute.decoration) {
return;
}
if (config.attribute.decoration.enableExtension != true) {
logger.debug("custom attribute decoration is not enabled");
return;
}
// load env attributes
var envParams = config.attribute.decoration.environment.properties;
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.attributes;
if (staticParams) {
Object.keys(staticParams).forEach(function (key) {
customAttributes["attribute\.decoration\.user\." + key] = staticParams[key];
});
}
var extParams = config.attribute.decoration.externalfile;
if (extParams && extParams.fileName) {
var extAttributes = readExternalAttributes(extParams.fileName);
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 (isDataConnected) {
sendDataConnHttpRequest(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);
}
}
module.exports.reportConfigAttributes = reportConfigAttributes;
function processReponseMessageForReconnection(message) {
var obj = JSON.parse(message);
if (obj != null) {
if (obj.processed == 'false') {
logger.debug("$$$$$ Marked for Reconnection : " + message);
includeRetryConnectionHeader = true;
} else if (obj.op != null && obj.op == 'config') {
processHttpConfigMessage(message);
isConnectionInitialized = false;
}
}
if (!isConnectionInitialized) {
logger.debug("$$$$$ ConnectionInitialized : " + message);
logTraceEventStartFlag = true;
logTraceEventEndFlag = true;
logMetricsSendFlag = true;
isDataConnected = true;
setTimeout(checkMetricReportingStatus, metricsEnableDelay);
setTimeout(reportPlatformMetrics, (metricsEnableDelay + 2000));
isConnectionInitialized = true;
}
}
function processHttpConfigMessage(message) {
var obj = JSON.parse(message);
logger.debug("$$$$$ Connection Message Response - Parse : " + obj);
if (obj != null) {
logger.debug("$$$$$ Command : " + obj.cmd);
if (obj.cmd == 'version') {
var params = obj.prms;
if (params != null && params.collector) {
logger.debug("$$$$$ Collector : " + params.collector);
processCollectorVersion(params.collector);
}
if (params != null && params.arfp) {
logger.debug("$$$$$ ARFP : " + params.arfp);
processArfpVersion(params.arfp);
}
}
}
}
function processRequireConfigMessage(message) {
var obj = JSON.parse(message);
logger.debug("$$$$$ Require Message Response - Parse : " + obj + "Message " + message);
if (obj != null && obj.op == 'config') {
if (obj.cmd == 'require') {
logger.debug("$$$$$ Command : " + obj.cmd);
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);
}
}
}
}
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) {
if (!isDataConnected) {
logger.warn(' writeToSocketByAnonymous - Cannot send message as data connection is down.' + buff.join(os.EOL));
return;
}
var dataReq = sendDataConnHttpRequest(buff.join(os.EOL) + os.EOL);
buff = [];
currBuffSize = 0;
}
buffFull = false;
}, 500); // 500 msecs
function writeToSocket(msg) {
logger.debug(" Message > " + 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;
sendDataConnHttpRequest(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 (!isDataConnected) {
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 (!isDataConnected) {
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 (!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);
getAndSendDataConnHttpRequest(msg, requireResponseCallback);
}
var requireResponseCallback = function (response) {
var statusCode = response.statusCode;
var responseMsg = ''
response.on('data', function (chunk) {
responseMsg += chunk;
});
response.on('end', function () {
if (statusCode == 200) {
processRequireConfigMessage(responseMsg);
}
});
}
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 (!isDataConnected) {
if (logMetricsSendFlag) {
logger.debug('Metrics cannot be sent since the command connection is down.');
logMetricsSendFlag = false;
}
return;
}
if (metricsReportingEnabled && isDataConnected) {
var msg = JSON.stringify(metric);
logger.debug("MetricsReporting Message --> " + msg);
writeToSocket(msg);
//sendDataConnHttpRequest(msg);
}
};
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;
}
process.on('SIGINT', () => {
logger.debug('SIGINT signal received.');
sendDisconnectionRequest();
});
process.on('SIGTERM', () => {
logger.debug('SIGKILL signal received.');
sendDisconnectionRequest();
});
process.on('EXIT', () => {
logger.debug('EXIT signal received.');
sendDisconnectionRequest();
});
function sendDisconnectionRequest() {
logTraceEventStartFlag = false;
logTraceEventEndFlag = false;
logMetricsSendFlag = false;
setTimeout(terminateApp, 20000);
var object = {
op: 'disconnect'
};
var msg = generateMsg(object);
logger.info('Initiating disconnection with Collector agent : %s', msg );
var request = http.request(requestHeaders(msg), disconnectResponseCallback);
request.on('error', error => {
logger.error("Disconnection : " + error);
process.exit(0);
});
request.write(msg);
request.end();
isDataConnected = false;
}
var disconnectResponseCallback = function (response) {
var statusCode = response.statusCode;
if (statusCode == 200) {
logger.debug("Gracefully Disconnected from Collector");
}
process.exit(0);
}
function terminateApp() {
logger.info("Terminating Forcefully");
process.exit(0);
}
module.exports.sendMetric = sendMetricInternal;