ca-apm-probe
Version:
CA APM Node.js Agent monitors real-time health and performance of Node.js applications
283 lines (236 loc) • 9.61 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.
*/
// probe for mongodb driver 1.4.*
var agent = require('../../../agent');
var proxy = require('../../../proxy');
var util = require('util');
var instrUtil = require("../../../utils/instrument-util");
var config = require('../../../configdata').getConfigData();
var logger = require("../../../logger.js");
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;
// truncate parameter value string to this limit
var MAX_PARAM_VALUE_LENGTH = 100;
var debug = logger.isDebug();
var reportRawQuery = (config.mongodb.reportRawQuery == undefined) ? false : config.mongodb.reportRawQuery;
// one of the cursor properties which enables special selector
var CURSOR_PROPERTY = 'showDiskLoc';
var CURSOR_PROPERTY_DEFAULT_VALUE = false;
var commandIndexMap = {
'find': 0,
'insert': 1,
'update': 2,
'remove': 3
};
var targetModule = new Object;
module.exports = function (mongodb) {
logger.info('Loading mongodb probe');
targetModule.mongodb = mongodb;
targetModule.methodMap = getMethodsWithProbes();
var methodMap = targetModule.methodMap;
// we instrument lower level '_executeQueryCommand' rather than CRUD(insert,find,update,remove) APIs because
//- callback(wrapper) is always passed to this API irrespective of callback passed to CRUD.
// So it enables us tracking the finish of async mongodb operation.
//- find() API itself is sync and returns cursor(or calls back with cursor).
// So instrumenting find() won't give timing for data fetching operation.
// Data fetching calls made by cursor also converges to '_executeQueryCommand' and hence can be tracked here.
proxy.before(mongodb.Db.prototype, '_executeQueryCommand', function caMongodbQueryBeforeHook(db, args, storage) {
var command = args[0] || {};
var callbackIndex = instrUtil.findCallbackIndex(args);
// if callaback in not available, skip tracing
if (callbackIndex == -1) {
return;
}
var queryInfo = parseCommand(command, db);
var operation = queryInfo.operation;
if (operation && !shouldSkipInstrumentation(operation)) {
var eventNameFormatted = 'mongodb14.' + operation;
var ctx = storage.get('ctx');
if (debug) {
logDiagnosticInfo(eventNameFormatted + ' start', ctx);
}
var eventArgs = queryInfo.eventArgs || {};
updateWithDBInfo(eventArgs, db);
// send start trace event
ctx = agent.asynchEventStart(ctx, eventNameFormatted, eventArgs);
storage.set('ctx', ctx);
instrumentCallback(args, callbackIndex, storage, ctx, eventNameFormatted);
}
});
proxy.after(mongodb.Collection.prototype, 'find', function caMongodbColAfterHook(collectionObj, args, rval, storage) {
var operation = 'find';
var callbackIndex = instrUtil.findCallbackIndex(args);
// we rely on special selector -'$query' in command to identify find operation.
// e.g. query: { '$query': { name: 'user1' }, '$showDiskLoc': false }
// command generated by find() called without 'options' will not this key.
// e.g.: query: { name: 'user1' }
// so in order to trace find() in such case, we update the cursor to create 'options' scenario.
if (callbackIndex !== -1) {
proxy.callback(args, callbackIndex, function (obj, args, storage) {
var cursor = args[0];
if (cursor && typeof cursor == 'object') {
if (cursor[CURSOR_PROPERTY] == null) {
cursor[CURSOR_PROPERTY] = CURSOR_PROPERTY_DEFAULT_VALUE;
}
}
});
} else {
var cursor = rval;
if (cursor && typeof cursor == 'object') {
if (cursor[CURSOR_PROPERTY] == null) {
cursor[CURSOR_PROPERTY] = CURSOR_PROPERTY_DEFAULT_VALUE;
}
}
}
});
function shouldSkipInstrumentation(funcName) {
return (methodMap[commandIndexMap[funcName]] === "skip_instrument");
}
}
function parseCommand(command, db) {
var queryInfo = {};
var query = command.query; //value is 'command hash' which is sent to the server, ex: {ping:1}.
if (query) {
// 'find' is likely to be most frequent operation, check for it first
if (query['$query'] || command.cursorId) {
queryInfo.operation = 'find';
queryInfo.eventArgs = {collectionName: command.collectionName || DEFAULT_COLLECTION_NAME};
if (reportRawQuery) {
// query['$query'] - special selector available when user set options
var selector = sanitizeParameterValue(JSON.stringify(query['$query'] || query));
queryInfo.eventArgs.query = util.format('%s (%s)', queryInfo.operation, selector);
}
}
else if (query.insert) {
var namespace = query.insert;
var docs = query.documents;
queryInfo.operation = 'insert';
queryInfo.eventArgs = {};
updateWithCollectionInfo(queryInfo.eventArgs, db, namespace);
// 'documents' is required argument for insert
if (reportRawQuery && docs) {
if (Array.isArray(docs)) {
docs = docs.slice(0, MAX_ARRAY_ELEMENTS);
}
docs = sanitizeParameterValue(JSON.stringify(docs));
queryInfo.eventArgs.query = util.format('%s (%s)', queryInfo.operation, docs);
}
} else if (query.update) {
var namespace = query.update;
var data = Array.isArray(query.updates) ? query.updates[0] : {};
queryInfo.operation = 'update';
queryInfo.eventArgs = {};
updateWithCollectionInfo(queryInfo.eventArgs, db, namespace);
// selector('q') and document('u') are required arguments for update
if (reportRawQuery && data && data.q && data.u) {
var selector = sanitizeParameterValue(JSON.stringify(data.q));
var updator = sanitizeParameterValue(JSON.stringify(data.u));
queryInfo.eventArgs.query = util.format('%s (%s, %s)', queryInfo.operation, selector, updator);
}
}
else if (query.delete) {
var namespace = query.delete;
var data = Array.isArray(query.deletes) ? query.deletes[0] : {};
queryInfo.operation = 'remove';
queryInfo.eventArgs = {};
updateWithCollectionInfo(queryInfo.eventArgs, db, namespace);
// selector('q') is optional argument
if (reportRawQuery && data && data.q) {
var selector = sanitizeParameterValue(JSON.stringify(data.q));
queryInfo.eventArgs.query = util.format('%s (%s)', queryInfo.operation, selector);
}
} else {
if (debug) {
logger.debug('[mongodb] unsupported query: %s', sanitizeParameterValue(JSON.stringify(query)));
}
}
}
return queryInfo;
}
function instrumentCallback(args, callbackIndex, storage, ctx, eventName) {
proxy.callback(args, callbackIndex, function (obj, args, storage) {
//var errorObject = agent.checkAndSetErrorObject(args, 'MongodbError');
if (debug) {
logDiagnosticInfo(eventName + ' done - callback start', ctx);
}
if (ctx) {
ctx = agent.asynchEventDone(ctx, eventName, null);
//ctx = agent.asynchEventDone(ctx, eventNameFormatted, null, errorObject);
storage.set('ctx', ctx);
}
},
function (obj, args) {
if (debug) {
logDiagnosticInfo(eventName + ' finish - callback finish', ctx);
}
if (ctx) {
agent.asynchEventFinish(ctx);
}
});
}
function updateWithDBInfo(eventArgs, db) {
if (db && db.serverConfig) {
var serverConfig = db.serverConfig;
eventArgs.url = 'mongodb://' + (serverConfig.host || DEFAULT_HOST) + ':' + (serverConfig.port || DEFAULT_PORT) + '/' + db.databaseName;
} else {
eventArgs.url = DEFAULT_DB_URL;
}
}
function updateWithCollectionInfo(eventArgs, db, collectionName) {
if (collectionName) {
eventArgs.collectionName = db.databaseName + '.' + collectionName || DEFAULT_COLLECTION_NAME;
}
}
function logDiagnosticInfo(eventName, ctx) {
if (ctx != null) {
logger.debug('event name: %s [%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 sanitizeParameterValue(query) {
//truncation logic
if (query.length > MAX_PARAM_VALUE_LENGTH) {
query = query.substr(0, MAX_PARAM_VALUE_LENGTH) + ' ...(truncated)';
}
// other sanitization ops may follow
return query;
}
function getMethodsWithProbes() {
if (!targetModule.methodMap) {
var mt = new Object;
mt[0] = 'mongodb#find';
mt[1] = 'mongodb#insert';
mt[2] = 'mongodb#update';
mt[3] = 'mongodb#remove';
targetModule.methodMap = mt;
}
return targetModule.methodMap;
}
function instrument(methodMap) {
targetModule.methodMap = methodMap;
}
module.exports.getMethodsWithProbes = getMethodsWithProbes;
module.exports.instrument = instrument.bind(module);