UNPKG

ca-apm-probe

Version:

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

386 lines (343 loc) 11.9 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. */ /** * Virtual stack for tracking asynch transaction paths */ var util = require('util'); var events = require("events"); var logger = require("./logger.js"); var coridgenerator = require('./utils/coridgenerator'); var config = require('./configdata').getConfigData(); var watchDogPeriod = config.virtualStackPollPeriod || 10000; var maxMapSize = config.maxTrxMapSize || 5000; require('string.prototype.startswith'); function VirtualStack() { events.EventEmitter.call(this); } util.inherits(VirtualStack, events.EventEmitter); var virtualstack = new VirtualStack(); module.exports = virtualstack; var nextTxId = 10; var nextLaneId = 100; var nextEventId = 1000; var corrIdSeed = Date.now(); var startTraceHandler = null; var endTraceHandler = null; var virtualStackMap = new Object(); function getFragmentId(level, lane) { return '#' + level + '-' + lane; } var VirtualStackContext = function (previous, evtTypeNum, evtName, evtArgs) { this.evtTypeNum = evtTypeNum; this.previous = previous; this.childLanes = 0; this.name = evtName; this.args = evtArgs; if (previous == null) { this.parent = null; this.txid = nextTxId++; this.evtid = nextEventId++; this.level = 0; this.lane = nextLaneId++; this.seq = 0; this.corIdObj = coridgenerator.generateCorIdObj(); if (!this.args) { this.args = {}; } // in case, we have seen correlation header in request // parse and reset the correlation instance if (this.args.corId) { this.corIdObj.parseAndSet(this.args.corId); } // create new Correlation id instance this.args.corId = this.corIdObj.getCurrentCorId(); this.args.traceId = this.corIdObj.getTransactionTraceId(); return; } this.txid = previous.txid; this.evtid = previous.evtid; this.level = previous.level; this.lane = previous.lane; this.seq = previous.seq; this.corIdObj = previous.corIdObj; if (evtTypeNum == 0) { //logger.info('start %d %s', this.txid, util.inspect(parent, { showHidden: true, depth: 0 }) ); this.evtid = nextEventId++; if (previous.evtTypeNum == 0) { // previous component is still executing, // so previous context is parent for this context this.parent = previous; // Parent is start event, we go to next level and start new lane this.level = this.parent.level + 1; this.lane = previous.lane; this.seq = 0; } else { // previous component has finished. // so previous start context's parent is parent for this context // startCtx == previous.parent this.parent = previous.parent.parent; // parent is finish event, so we just start new component in same lane and level this.level = previous.level; this.lane = previous.lane; this.seq = previous.seq + 1; } if (this.parent) { // If this not first child pick another lane and start a new fragment if (this.parent.asyncCall || this.parent.childLanes > 0) { this.lane = nextLaneId++; this.seq = 0; //create fragment start event //logger.debug("Parent: %s", util.inspect(parent, { showHidden: true, depth: 0 })); if (logger.isDebug()) { logger.debug('create new lane: %s', this.toString()); } startNewLane(this); } this.parent.childLanes++; } } else { this.parent = previous; } }; VirtualStackContext.prototype.toString = function () { return util.format('{txid: %d, lane: %d, level: %d, eventName: %s, eventType: %d}', this.txid, this.lane, this.level, this.name, this.evtTypeNum); } function getLogPrefix(ctx) { var levelstr = ''; for (var i = 0; i < ctx.level;i++) { levelstr=levelstr + ' '; } var colorstr = '\x1b[1;3' + (ctx.lane%6 + 1) + 'm'; return levelstr + colorstr + '[' + ctx.txid + ',' + ctx.lane + ','+ ctx.seq + ','+ ctx.evtid + '] '; } function getLogSuffix() { return '\x1b[0m'; } function startNewLane(ctx) { var txid = ctx.txid; var lane = nextLaneId++; ctx.lane = lane; ctx.seq = 0; // Create new fragment data structure var fragmentCtx = Object.create(ctx); if (!virtualStackMap.hasOwnProperty(txid)) { // Create new transaction structure virtualStackMap[txid] = { fragments: { count: 0 } }; } var fragName = "NoContext"; if (ctx.previous) { fragName = ctx.previous.name.replace("\.","_"); ctx.corIdObj = ctx.previous.corIdObj; ctx.args.fragmentEntryPoint = ctx.previous.fragmentEntryPoint; } virtualStackMap[txid].fragments[lane] = { name: fragName, evtid: fragmentCtx.evtid, startlevel: fragmentCtx.level, lane: fragmentCtx.lane }; virtualStackMap[txid].fragments.count++; if (ctx.corIdObj) { // use correlation id from parent if (!ctx.args) { ctx.args = {}; } ctx.args.corId = ctx.corIdObj.getCurrentCorId(); ctx.args.frontendTraceId = ctx.corIdObj.getTransactionTraceId(); } // Send out fragment start event to collector var ts = Date.now(); createTracerEvent(fragmentCtx, 'tracer-start', 'fragment.' + virtualStackMap[txid].fragments[lane].name, ts, ctx.args); // allocate new event id for original event. ctx.evtid = nextEventId++; ctx.parent = fragmentCtx; return ctx; } function finishLane(ctx) { var txid = ctx.txid; var lane = ctx.lane; // If this is main fragment, where event id is same as in context, we do not need to send end event if (virtualStackMap[txid].fragments[lane].evtid !== ctx.evtid) { var fragmentCtx = Object.create(ctx); fragmentCtx.evtid = virtualStackMap[txid].fragments[lane].evtid; var fragName = virtualStackMap[txid].fragments[lane].name; var ts = Date.now(); createTracerEvent(fragmentCtx, 'tracer-finish', 'fragment.' + fragName, ts, null); } delete virtualStackMap[txid].fragments[lane]; // If it's last fragment release txid virtualStackMap[txid].fragments.count--; if (virtualStackMap[txid].fragments.count === 0) { logger.debug("Transaction %d finished.", txid); delete virtualStackMap[txid]; } } function pushAsyncTracerEvent(previousCtx, evtName, evtArgs) { var evtTs = Date.now(); if(evtArgs && evtArgs.tracePreviousCtx && previousCtx != null){ createTracerEvent(previousCtx, 'tracer-start', previousCtx.eventNameFormatted, evtTs, previousCtx.args); } var ctx = new VirtualStackContext(previousCtx, 0, evtName, evtArgs); var txid = ctx.txid; var lane = ctx.lane; if (!virtualStackMap.hasOwnProperty(txid)) { // Create new transaction structure virtualStackMap[txid] = { fragments: { count: 0 } }; if (previousCtx == null && evtName.startsWith('http.')) { // This is brand new transaction, create new main fragment virtualStackMap[txid].fragments.count = 1; virtualStackMap[txid].fragments[lane] = { evtid: ctx.evtid, startlevel: ctx.level, lane: ctx.lane }; logger.debug('main fragment [%d, %d]', txid, lane); } else { // This is a fragment following main fragment that has finished ctx = startNewLane(ctx); lane = ctx.lane; } } logger.debug('fragment resume [%d, %d]', txid, lane); if (!virtualStackMap[txid].fragments[lane]) { logger.debug('fragment has ended creating new lane [%d, %d]', txid, lane); // The fragment has ended, create a new lane and start new fragment ctx = startNewLane(ctx); lane = ctx.lane; } virtualStackMap[txid].fragments[lane].level = ctx.level; virtualStackMap[txid].fragments[lane].active = true; if(!evtArgs || !evtArgs.skipTrace){ createTracerEvent(ctx, 'tracer-start', evtName, evtTs, ctx.args); } return ctx; } function popAsyncTracerEvent(startCtx, evtName, evtArgs, errorObj) { var ctx = new VirtualStackContext(startCtx, 1, evtName, evtArgs); var txid = ctx.txid; var evtTs = Date.now(); if (virtualStackMap[txid] == undefined) { // This can happen for events that are not detected // as parallel fragments, but outlive the transaction logger.debug('pop %s with unknown txid [%d]', evtName, txid); // Send event to collector anyway, so that it can createTracerEvent(ctx, 'tracer-finish', evtName, evtTs, evtArgs, errorObj); return null; } var lane = ctx.lane; if (virtualStackMap[txid].fragments.hasOwnProperty(lane)) { if (logger.isDebug()) { logger.debug('fragment break [%d, %d]', txid, lane); } virtualStackMap[txid].fragments[lane].active = false; // on pop, update child lanes count of parent ctx var parentCtx = startCtx.parent; if (parentCtx) { parentCtx.childLanes--; } } createTracerEvent(ctx, 'tracer-finish', evtName, evtTs, evtArgs, errorObj); return ctx; } function finishAsyncTracer(finishCtx) { if (!finishCtx) { return; } var txid = finishCtx.txid; var level = finishCtx.level; var lane = finishCtx.lane; if (virtualStackMap.hasOwnProperty(txid) && virtualStackMap[txid].fragments.hasOwnProperty(lane) && virtualStackMap[txid].fragments[lane].startlevel === level && virtualStackMap[txid].fragments[lane].active === false) { finishLane(finishCtx); } } function createTracerEvent(ctx, evtType, evtName, evtTs, evtArgs, errorObj) { // TODO Is this txid formation scheme safe in all cases? var txid = ctx.txid*1000 + ctx.lane; if (evtType === 'tracer-start' && startTraceHandler) { // Fancy logging: logger.info(getLogPrefix(ctx) + 'start: ' + evtName + ' - ts:' + evtTs + getLogSuffix()); if (logger.isDebug()) { logger.debug('start: %s[%d %d %d] - ts:%d',evtName, ctx.txid, ctx.lane, ctx.evtid, evtTs); } startTraceHandler(evtName, evtTs, txid, ctx.evtid, evtArgs); return; } if (evtType === 'tracer-finish' && endTraceHandler) { // Fancy logging: //logger.info(getLogPrefix(ctx) + 'finish: ' + evtName + ' '+ durStr + '- ts:' + evtTs + getLogSuffix()); // More efficient logging if (logger.isDebug()) { logger.debug('finish: %s [%d %d %d] - ts:%d', evtName, ctx.txid, ctx.lane, ctx.evtid, evtTs); } endTraceHandler(evtName, evtTs, txid, ctx.evtid, evtArgs, errorObj); return; } } function setTraceHandlers(startTrace, endTrace) { startTraceHandler = startTrace; endTraceHandler = endTrace; } module.exports.setTraceHandlers = setTraceHandlers; module.exports.pushAsyncTracerEvent = pushAsyncTracerEvent; module.exports.popAsyncTracerEvent = popAsyncTracerEvent; module.exports.finishAsyncTracer = finishAsyncTracer; function virtualStackWatchDog() { var txKeys = Object.keys(virtualStackMap); var len = txKeys.length; if (len > maxMapSize) { txKeys = txKeys.sort(); var toBeDeleted = len - maxMapSize; logger.error('Active tx count %d, cleaning old transactions...', len); var lastTxid = 0; var i = 0; for (i = 0; i < len; i++) { if (txKeys[i] <= lastTxid) { logger.debug('tx keys not in order %d %d', lastTxid, txKeys[i]); } lastTxid = txKeys[i]; if (i < toBeDeleted) { if (i == 0) { if (logger.isDebug()) { logger.debug('Deleting %d %s', txKeys[i], util.inspect(virtualStackMap[txKeys[i]], { showHidden: true, depth: 5 }) ); } } delete virtualStackMap[txKeys[i]]; } } } } if (watchDogPeriod > 0) { if (!maxMapSize || maxMapSize === 0) { maxMapSize = 5000; } setInterval(virtualStackWatchDog, watchDogPeriod); }