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
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.
*/
/**
* 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);
}