UNPKG

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
/** * 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.* 'use strict'; 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);