UNPKG

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