UNPKG

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