ca-apm-probe
Version:
CA APM Node.js Agent monitors real-time health and performance of Node.js applications
417 lines (332 loc) • 14.7 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 cusorProbe = require('./mongodb-cursor');
var DEFAULT_HOST = 'default_host';
var DEFAULT_PORT = 'default_port';
var DEFAULT_DB = 'default_db';
var DEFAULT_COLLECTION_NAME = 'unknown_collection';
var DEFAULT_DB_URL = util.format('mongodb://%s:%s/%s', DEFAULT_HOST, DEFAULT_PORT, DEFAULT_DB);
// max elements in array of docs to consider for parameter value
var MAX_ARRAY_ELEMENTS = 5;
var DEFAULT_ERROR_CLASS_NAME = 'MongoDBError';
var EVENT_CLASS_NAME = 'mongodbCollection';
var debug = logger.isDebug();
var reportRawQuery = (config.mongodb.reportRawQuery == undefined) ? false : config.mongodb.reportRawQuery;
var targetModule = new Object;
module.exports = function (mongodb) {
logger.info('Loading mongodb probe');
targetModule.mongodb = mongodb;
targetModule.methodMap = getMethodsWithProbes();
var methodMap = targetModule.methodMap;
// probe for insert operations
['insertMany', 'insertOne', 'insert'].forEach(function (op) {
var probes = getInsertProbes(op);
proxy.around(mongodb.Collection.prototype, op, probes[0], probes[1]);
});
function getInsertProbes(operation) {
var probes = [];
probes[0] = function caMongodbInsertBeforeHook(collection, args, storage) {
var callbackIndex = instrUtil.findCallbackIndex(args);
var ctx = storage.get('ctx');
if (shouldSkipTracing(operation, ctx)) {
return;
}
var docs = args[0];
var db = collection.s.db;
var eventNameFormatted = EVENT_CLASS_NAME + '.' + operation;
var eventArgs = {
collectionName: collection.collectionName || DEFAULT_COLLECTION_NAME,
dbName: db.databaseName || DEFAULT_DB
};
updateWithDBServerInfo(eventArgs, db);
if (reportRawQuery && docs) {
if (Array.isArray(docs)) {
docs = docs.slice(0, MAX_ARRAY_ELEMENTS);
}
eventArgs.query = util.format('%s (%s)', operation, traceUtil.formatEventArgValueObject(docs));
}
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' start', ctx);
}
// send start trace event
ctx = agent.asynchEventStart(ctx, eventNameFormatted, eventArgs);
storage.set('ctx', ctx);
ctx.mongodbOpReentry = operation;
ctx.callbackIndex = callbackIndex;
if (callbackIndex != -1) {
instrumentCallback(args, callbackIndex, storage, ctx, eventNameFormatted);
}
};
probes[1] = function caMongodbInsertAfterHook(collection, args, promise, storage) {
registerPromiseHandlers(storage, promise, operation);
};
return probes;
}
// probe for update operations
['updateMany', 'updateOne', 'update'].forEach(function (op) {
var probes = getUpdateProbes(op);
proxy.around(mongodb.Collection.prototype, op, probes[0], probes[1]);
});
function getUpdateProbes(operation) {
var probes = [];
probes[0] = function caMongodbUpdateBeforeHook(collection, args, storage) {
var callbackIndex = instrUtil.findCallbackIndex(args);
var ctx = storage.get('ctx');
if (shouldSkipTracing(operation, ctx)) {
return;
}
var db = collection.s.db;
var eventNameFormatted = EVENT_CLASS_NAME + '.' + operation;
var eventArgs = {
collectionName: collection.collectionName || DEFAULT_COLLECTION_NAME,
dbName: db.databaseName || DEFAULT_DB
};
updateWithDBServerInfo(eventArgs, db);
if (reportRawQuery && args[0] && args[1]) {
var filter = traceUtil.formatEventArgValueObject(args[0]);
var update = traceUtil.formatEventArgValueObject(args[1]);
eventArgs.query = util.format('%s (%s, %s)', operation, filter, update);
}
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' start', ctx);
}
// send start trace event
ctx = agent.asynchEventStart(ctx, eventNameFormatted, eventArgs);
storage.set('ctx', ctx);
ctx.mongodbOpReentry = operation;
ctx.callbackIndex = callbackIndex;
if (callbackIndex != -1) {
instrumentCallback(args, callbackIndex, storage, ctx, eventNameFormatted);
}
};
probes[1] = function caMongodbUpdateAfterHook(collection, args, promise, storage) {
registerPromiseHandlers(storage, promise, operation);
};
return probes;
}
// probe for delete operations
['remove', 'deleteMany', 'deleteOne'].forEach(function (op) {
var probes = getRemoveProbes(op);
proxy.around(mongodb.Collection.prototype, op, probes[0], probes[1]);
});
function getRemoveProbes(operation) {
var probes = [];
probes[0] = function caMongodbRemoveBeforeHook(collection, args, storage) {
var callbackIndex = instrUtil.findCallbackIndex(args);
var ctx = storage.get('ctx');
if (shouldSkipTracing(operation, ctx)) {
return;
}
var db = collection.s.db;
var eventNameFormatted = EVENT_CLASS_NAME + '.' + operation;
var eventArgs = {
collectionName: collection.collectionName || DEFAULT_COLLECTION_NAME,
dbName: db.databaseName || DEFAULT_DB
};
updateWithDBServerInfo(eventArgs, db);
if (reportRawQuery && args[0]) {
var filter = traceUtil.formatEventArgValueObject(args[0]);
eventArgs.query = util.format('%s (%s)', operation, filter);
}
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' start', ctx);
}
// send start trace event
ctx = agent.asynchEventStart(ctx, eventNameFormatted, eventArgs);
storage.set('ctx', ctx);
ctx.mongodbOpReentry = operation;
ctx.callbackIndex = callbackIndex;
if (callbackIndex != -1) {
instrumentCallback(args, callbackIndex, storage, ctx, eventNameFormatted);
}
};
probes[1] = function caMongodbRemoveAfterHook(collection, args, promise, storage) {
registerPromiseHandlers(storage, promise, operation);
};
return probes;
}
// probe for find operations
['find'].forEach(function (op) {
var probes = getFindProbes(op);
proxy.around(mongodb.Collection.prototype, op, probes[0], probes[1]);
});
function getFindProbes(operation) {
var probes = [];
probes[0] = function caMongodbFindBeforeHook(collection, args, storage) {
// if this operation is not selected, skip tracing
if (shouldSkipInstrumentation(operation)) {
return;
}
var query = args[0];
var eventNameFormatted = EVENT_CLASS_NAME + '.' + operation;
var ctx = storage.get('ctx');
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' start', ctx);
}
var db = collection.s.db;
var eventArgs = {
collectionName: collection.collectionName || DEFAULT_COLLECTION_NAME,
dbName: db.databaseName || DEFAULT_DB
};
updateWithDBServerInfo(eventArgs, db);
if (reportRawQuery && query) {
eventArgs.query = util.format('%s (%s)', operation, traceUtil.formatEventArgValueObject(query));
}
ctx = agent.asynchEventStart(ctx, eventNameFormatted, eventArgs);
storage.set('ctx', ctx);
}
probes[1] = function caMongodbFindAfterHook(collection, args, retValue, storage) {
// if this operation is not selected, skip tracing
if (shouldSkipInstrumentation(operation)) {
return;
}
var eventNameFormatted = EVENT_CLASS_NAME + '.' + operation;
var ctx = storage.get('ctx');
if (ctx) {
var errorObject = (retValue instanceof Error) ? traceUtil.createErrorObject(retValue.name, retValue.message) : {};
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' done and finish', ctx);
}
ctx = agent.asynchEventDone(ctx, eventNameFormatted, null, errorObject);
storage.set('ctx', ctx);
agent.asynchEventFinish(ctx);
}
}
return probes;
}
// now load cursor probe
// for maintainablity, we have kept mongodb cursor probe in separate file
cusorProbe(mongodb);
function shouldSkipInstrumentation(funcName) {
return (methodMap[0] === "skip_instrument");
}
function shouldSkipTracing(operation, ctx) {
// if this operation is not selected, or already tracing another mongodb operation
// in call stack (example case: insert -> insertMany internally, reentry==true for insertMany),
// skip tracing
return (shouldSkipInstrumentation(operation) || (ctx && ctx.mongodbOpReentry));
}
// registers handlers to track promise excution
function registerPromiseHandlers(storage, 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, eventNameFormatted), getPromiseRejectionHandler(storage, ctx, eventNameFormatted));
}
}
};
}
function getPromiseSuccessHandler(storage, ctx, eventNameFormatted) {
return function caMongodbPromiseSuccessHandler(val) {
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' done - promise resolved successfully', ctx);
}
if (ctx) {
var eventArgs = {};
if (ctx.serverInfo) {
eventArgs.dbServer = ctx.serverInfo;
delete ctx.serverInfo;
}
ctx = agent.asynchEventDone(ctx, eventNameFormatted, eventArgs, null);
storage.set('ctx', ctx);
agent.asynchEventFinish(ctx);
}
};
}
function getPromiseRejectionHandler(storage, ctx, 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 (ctx.serverInfo) {
eventArgs.dbServer = ctx.serverInfo;
delete ctx.serverInfo;
}
ctx = agent.asynchEventDone(ctx, eventNameFormatted, eventArgs, errorObject);
storage.set('ctx', ctx);
agent.asynchEventFinish(ctx);
}
};
}
function instrumentCallback(args, callbackIndex, storage, 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 (ctx.serverInfo) {
eventArgs.dbServer = ctx.serverInfo;
delete ctx.serverInfo;
}
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 updateWithDBServerInfo(eventArgs, db) {
if (db && db.serverConfig && db.serverConfig.host) {
var serverConfig = db.serverConfig;
eventArgs.dbServer = serverConfig.host + ':' + serverConfig.port;
}
}
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);