UNPKG

ca-apm-probe

Version:

CA APM Node.js Agent monitors real-time health and performance of Node.js applications

695 lines (571 loc) 19.6 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 net = require('net'); var os = require('os'); 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"); var probeClientUtil = require("./probe-client-util"); var pid = process.pid; var instanceId = util.format('%s-%s', os.hostname(), pid); var maxPingDelay = config.maxPingDelay || 15000; var lastPingResponseTime = new Date().getTime(); var reconnectAttempt = 1; var tryReconnect = config.tryReconnect || false; var buff = []; var currBuffSize = 0; var maxAllowedBuffSize = 50000; var buffFull = false; var currentHost; var currentPort; var currentProbeName; var commandClient = null; var dataClient = 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 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 () { return config.hostName; }; var resolveAppName = function () { return config.appName; }; module.exports.start = start; function connectCommandClient(host, port) { var client = net.connect(port, host, function () { logger.info('Command connection established with collector agent listening on host: %s, port: %d', host, port); client.write(generateMsg(commandConnMsg())); }); client.on('end', function () { logger.info('Command connection disconnected'); isCommandConnected = false; }); arfMessageParser = new ArfMessageParser(); // pass data to arf parser client.pipe(arfMessageParser); arfMessageParser.on('arf-message', function (message) { message = message.trim(); if (message && message.length > 0) { try { processSingleMessage(message); } catch (e) { logger.debug('got exception: %s while processing message: %s', e.message, message); } } }); return client; } function closeCommandClient() { /*Clear the speakPingProbeTimer since the Command Connection has been closed. No need to check now if the Collector is sending Speak messages or not. */ isCommandConnected = false; clearInterval(speakPingProbeTimer); if (commandClient) { commandClient.unpipe(); commandClient.removeAllListeners(); commandClient.destroy(); delete commandClient; commandClient = null; } } function reconnectCommandClient() { setTimeout(function () { logger.debug('Command reconnection Started'); commandClient = connectCommandClient(currentHost, currentPort); logger.debug('Command reconnection Completed'); commandClient.on('error', function (error) { logger.error('[Command Conn Error]' + error); reconnectAttempt = 1; closeCommandClient(); reconnectCommandClient(); }); commandClient.on('close', function () { logger.info('Command connection Closed'); reconnectAttempt = 1; closeCommandClient(); reconnectCommandClient(); }); }, 2000); } function start(opts) { currentHost = config.infrastructureAgent.host; currentPort = config.infrastructureAgent.port; currentProbeName = config.probeName; opts.currentHost = currentHost; opts.currentPort = currentPort; logger.debug('Trying to connect with collector agent listening on host: %s, port: %d', currentHost, currentPort); commandClient = connectCommandClient(currentHost, currentPort); // on connection announce custom attributes setTimeout(reportConfigAttributes,metricsEnableDelay+2000); // register reporting of custom attributes at reporting interval setInterval(reportConfigAttributes,reportingInterval()); commandClient.on('error', function (error) { logger.info('[Command Conn Error]' + error); closeCommandClient(); reconnectCommandClient(); }); commandClient.on('close', function () { closeCommandClient(); reconnectCommandClient(); }); } 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 = commandConnMsg(); 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 (commandClient != null && commandClient.readyState !== 'close') { commandClient.write(generateMsg(commMsg)); reportedAttributes = customAttributes; } else { logger.warn("Unable to report custom attributes , commandClient unavailable"); } } catch (e) { logger.debug("got exception: %s , unable to process custom attributes", e.message); } } module.exports.reportConfigAttributes = reportConfigAttributes; function processSingleMessage(message) { if (message.indexOf('speak') != -1) { commandClient.write(generateMsg(arfMsg())); lastPingResponseTime = new Date().getTime(); logger.debug("Sent reply for 'speak' message received. lastPingResponseTime updated to: %d", lastPingResponseTime); } else { processConfigMessage(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); logger.info("Command connection Response: %s", message.toString()); isCommandConnected = true; logTraceEventStartFlag = true; logTraceEventEndFlag = true; logMetricsSendFlag = true; // we need to wait for agent configuration message setTimeout(checkMetricReportingStatus, metricsEnableDelay); sendDataConnMsg(); speakPingProbeTimer = setInterval(speakPingProbe, 2000); } } } 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; } } //close the commandClient and dataClient if the speak message has not been received till the maxPingDelay time gap function speakPingProbe() { try{ var currentTime = new Date().getTime(); //logger.info('current time millis %d', currentTime); if (lastPingResponseTime < (currentTime - maxPingDelay)) { var logMsg = "Timeout for Speak Message. CommandClient state: " + (commandClient != null ? commandClient.readyState : "null"); if (commandClient != null && commandClient.readyState !== 'close' && commandClient.readyState !== 'readOnly') { //commandClient.removeAllListeners('close'); logMsg += " Command connection is closed."; commandClient.end(); if (dataClient != null && dataClient.readyState !== 'close' && dataClient.readyState !== 'readOnly') { logMsg += " Data connection is closed."; dataClient.end(); } } logMsg += " from Probe End. Reconnect attempt count: " + reconnectAttempt + " lastPingResponseTime: " + lastPingResponseTime; logger.info(logMsg); if(tryReconnect && (commandClient == null || commandClient.readyState === "readOnly") && reconnectAttempt++ > 30) { lastPingResponseTime = new Date().getTime(); reconnectAttempt = 1; closeCommandClient(); reconnectCommandClient(); } } } catch(e){ logger.error("Unable to reestablish connection due to error in speak probe. Error is:" + e); } } var sendDataConnMsg = function (isReconnection) { dataClient = net.connect(currentPort, currentHost, function () { if (isReconnection) { logger.info('Data connection re-established with collector agent'); } else { logger.info('Data connection established with collector agent'); } isDataConnected = true; lastPingResponseTime = new Date().getTime(); dataClient.write(generateMsg(dataConnMsg())); // report platform metrics once data connection is up // and collector agent compatibility has be verified setTimeout(probeClientUtil.reportPlatformMetrics, (metricsEnableDelay + 2000)); }); dataClient.on('data', function (data) { logger.info("Server data on Data Conn: %s", data.toString()); //client.end(); }); dataClient.on('end', function () { logger.info('Data connection disconnected'); isDataConnected = false; }); dataClient.on('error', function (error) { logger.info('[Data Conn Error]' + error); isDataConnected = false; if(error.message.includes('This socket has been ended by the other party')){ reconnectAttempt = 1; closeCommandClient(); reconnectCommandClient(); } }); dataClient.on('close', function () { logger.info('Data connection Closed'); isDataConnected = false; setTimeout(function () { if (commandClient && commandClient.readyState.toString() === 'open') { dataClient.removeAllListeners('connect'); sendDataConnMsg(true); } else { logger.info('Data Connection not re-established since Command Connection is down'); } }, 2000); }); }; 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; } dataClient.write(buff.join(os.EOL) + os.EOL); buff = []; currBuffSize = 0; } buffFull = false; }, 500); // 500 msecs function writeToSocket(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; dataClient.write(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 (commandClient == null || commandClient.readyState.toString() !== 'open') { if (logTraceEventStartFlag) { logger.info('Start Trace cannot be initiated since the command connection is down.'); logTraceEventStartFlag = false; } return; } if (dataClient == null) { sendDataConnMsg(); } 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 (commandClient == null || commandClient.readyState.toString() !== 'open') { if (logTraceEventEndFlag) { logger.info('End Trace cannot be initiated since the command connection is down.'); logTraceEventEndFlag = false; } return; } if (dataClient == null) { sendDataConnMsg(); } 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); dataClient.write(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 (commandClient == null || commandClient.readyState.toString() !== 'open') { if (logMetricsSendFlag) { logger.debug('Metrics cannot be sent since the command connection is down.'); logMetricsSendFlag = false; } return; } if (dataClient == null) { sendDataConnMsg(); } if (metricsReportingEnabled) { var msg = generateMsg(metric); logger.debug("sending: <%s>", msg); dataClient.write(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; } module.exports.sendMetric = sendMetricInternal;