UNPKG

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