ca-apm-probe
Version:
CA APM Node.js Agent monitors real-time health and performance of Node.js applications
272 lines (218 loc) • 9.51 kB
JavaScript
/**
* Copyright (c) 2016 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.
*/
// probe for mongodb driver 2.1.*
;
var agent = require('../../../agent');
var proxy = require('../../../proxy');
var util = require('util');
var instrUtil = require("../../../utils/instrument-util");
var traceUtil = require("../../../utils/trace-util");
var config = require('../../../configdata').getConfigData();
var logger = require("../../../logger.js");
var DEFAULT_DB = 'default_db';
var DEFAULT_COLLECTION_NAME = 'unknown_collection';
var DEFAULT_ERROR_CLASS_NAME = 'MongoDBError';
var EVENT_CLASS_NAME = 'mongodbCursor';
var debug = logger.isDebug();
var reportRawQuery = (config.mongodb.reportRawQuery == undefined) ? false : config.mongodb.reportRawQuery;
var targetModule = new Object;
module.exports = function (mongodb) {
targetModule.mongodb = mongodb;
targetModule.methodMap = getMethodsWithProbes();
var methodMap = targetModule.methodMap;
// probe for cursor operations which supports Promise and have common tracing requirements
['toArray', 'next'].forEach(function (op) {
var probes = getCursorProbes(op);
proxy.around(mongodb.Cursor.prototype, op, probes[0], probes[1]);
});
function getCursorProbes(operation) {
var probes = [];
probes[0] = function caMongodbCursorBeforeHook(cursor, args, storage) {
// callback index is supposed to be static - 0, don't pay cost of finding it at run time
var callbackIndex = 0;
var ctx = storage.get('ctx');
if (shouldSkipInstrumentation(operation)) {
return;
}
var eventNameFormatted = EVENT_CLASS_NAME + '.' + operation;
var eventArgs = {};
updateEventArgs(eventArgs, cursor);
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' start', ctx);
}
// send start trace event
ctx = agent.asynchEventStart(ctx, eventNameFormatted, eventArgs);
storage.set('ctx', ctx);
if (isCallbackAFunction(args, callbackIndex)) {
ctx.callbackIndex = callbackIndex;
instrumentCallback(args, callbackIndex, storage, cursor, ctx, eventNameFormatted);
} else {
ctx.callbackIndex = -1;
}
};
probes[1] = function caMongodbCursorAfterHook(cursor, args, promise, storage) {
registerPromiseHandlers(storage, cursor, promise, operation);
};
return probes;
}
proxy.before(mongodb.Cursor.prototype, 'forEach', function caMongodbBeforeHook(cursor, args, storage) {
var operation = 'forEach';
var callbackIndex = 1;
var ctx = storage.get('ctx');
if (!isCallbackAFunction(args, callbackIndex) || shouldSkipInstrumentation(operation)) {
return;
}
var eventNameFormatted = EVENT_CLASS_NAME + '.' + operation;
var eventArgs = {};
updateEventArgs(eventArgs, cursor);
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' start', ctx);
}
// send start trace event
ctx = agent.asynchEventStart(ctx, eventNameFormatted, eventArgs);
storage.set('ctx', ctx);
//ctx.mongodbOpReentry = operation;
instrumentCallback(args, callbackIndex, storage, cursor, ctx, eventNameFormatted);
});
function shouldSkipInstrumentation(funcName) {
return (methodMap[0] === "skip_instrument");
}
function shouldSkipTracing(callbackIndex, operation, ctx) {
// if callaback in not available or this operation is not selected, or
// already tracing another mongodb operation in call stack (reentry==true), skip tracing
return (callbackIndex == -1 || shouldSkipInstrumentation(operation) || (ctx && ctx.mongodbOpReentry));
}
function isCallbackAFunction(args, callbackIndex) {
return (args[callbackIndex] && typeof(args[callbackIndex]) === 'function');
}
function registerPromiseHandlers(storage, cursor, promise, operation) {
var ctx = storage.get('ctx');
if (ctx) {
if (ctx.callbackIndex == -1 && !shouldSkipInstrumentation(operation)) {
var eventNameFormatted = EVENT_CLASS_NAME + '.' + operation;
promise.then(getPromiseSuccessHandler(storage, ctx, cursor, eventNameFormatted), getPromiseRejectionHandler(storage, ctx, cursor, eventNameFormatted));
}
}
};
}
function getPromiseSuccessHandler(storage, ctx, cursor, eventNameFormatted) {
return function caMongodbPromiseSuccessHandler(val) {
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' done - promise resolved successfully', ctx);
}
if (ctx) {
var eventArgs = {};
if (cursor) {
if (cursor.server && cursor.server.s.serverDetails) {
eventArgs.dbServer = cursor.server.s.serverDetails.name;
}
}
ctx = agent.asynchEventDone(ctx, eventNameFormatted, eventArgs, null);
storage.set('ctx', ctx);
agent.asynchEventFinish(ctx);
}
};
}
function getPromiseRejectionHandler(storage, ctx, cursor, eventNameFormatted) {
return function caMongodbPromiseRejectionHandler(val) {
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' done - promise rejected', ctx);
}
if (ctx) {
var eventArgs = {};
var errorObject = traceUtil.getFormattedErrorObject(val) || traceUtil.createErrorObject(DEFAULT_ERROR_CLASS_NAME, val);
if (cursor) {
if (cursor.server && cursor.server.s.serverDetails) {
eventArgs.dbServer = cursor.server.s.serverDetails.name;
}
}
ctx = agent.asynchEventDone(ctx, eventNameFormatted, eventArgs, errorObject);
storage.set('ctx', ctx);
agent.asynchEventFinish(ctx);
}
};
}
function instrumentCallback(args, callbackIndex, storage, cursor, ctx, eventNameFormatted) {
proxy.callback(args, callbackIndex,
function (obj, args, storage) {
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' done - callback start ', ctx);
}
if (ctx) {
var eventArgs = {};
var errorObject = traceUtil.checkCallbackArgsForError(args, DEFAULT_ERROR_CLASS_NAME);
if (cursor) {
if (cursor.server && cursor.server.s.serverDetails) {
eventArgs.dbServer = cursor.server.s.serverDetails.name;
}
// FUTURE WORK: confirm cursor id is useful
//if (cursor.cursorState) {
// eventArgs.cursorId = cursor.cursorState.cursorId;
//}
}
ctx = agent.asynchEventDone(ctx, eventNameFormatted, eventArgs, errorObject);
storage.set('ctx', ctx);
}
},
function (obj, args) {
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' finish - callback finish', ctx);
}
if (ctx) {
agent.asynchEventFinish(ctx);
}
});
}
function updateEventArgs(eventArgs, cursor) {
var ns = cursor.s.ns;
if (ns) {
var info = ns.split('.');
if (info) {
eventArgs.dbName = info[0] || DEFAULT_DB;
eventArgs.collectionName = info[1] || DEFAULT_COLLECTION_NAME;
}
}
if (reportRawQuery) {
var cmd = cursor.s.cmd;
if (cmd) {
eventArgs.query = traceUtil.formatEventArgValueObject(cmd.query);
}
}
}
function logDiagnosticInfo(eventName, ctx) {
if (ctx != null) {
logger.debug('event name: %s , current context: [%d %d %d]', eventName, ctx.txid, ctx.lane, ctx.evtid);
}
else {
logger.debug('event name: %s - no context', eventName);
logger.debug((new Error('No context')).stack);
}
}
function getMethodsWithProbes() {
if (!targetModule.methodMap) {
var mt = new Object;
mt[0] = 'mongodb#all';
targetModule.methodMap = mt;
}
return targetModule.methodMap;
}
function instrument(methodMap) {
targetModule.methodMap = methodMap;
}
module.exports.getMethodsWithProbes = getMethodsWithProbes;
module.exports.instrument = instrument.bind(module);