appdynamics
Version:
Performance Profiler and Monitor
460 lines (407 loc) • 15.9 kB
JavaScript
/*
Copyright (c) AppDynamics, Inc., and its affiliates
2015
All Rights Reserved
*/
;
var utility = require('../utility');
function MongodbProbe(agent) {
this.agent = agent;
this.packages = ['mongodb'];
}
exports.MongodbProbe = MongodbProbe;
var collectionCommands = ['createCollection', 'dropCollection'];
MongodbProbe.prototype.attach = function (obj) {
var self = this;
var proxy = self.agent.proxy;
var profiler = self.agent.profiler;
var mongoObj = obj;
if(process.env.APPDYNAMICS_MONGODB_PROBE_DISABLE == true || process.env.APPDYNAMICS_MONGODB_PROBE_DISABLE == 'true') {
return;
}
if (obj.__appdynamicsProbeAttached__) return;
obj.__appdynamicsProbeAttached__ = true;
function withoutAPMAfterHandler(obj, args, ret, locals) {
if (locals.exitCall && locals.exitCall.command === 'find') return;
if (locals.methodHasCb) return;
if (!ret || !ret.__appdynamicsIsPromiseResult__)
self.addExitCall(locals.time, locals.exitCall, args);
else if (ret.error)
self.addExitCall(locals.time, locals.exitCall, ret.error);
else
self.addExitCall(locals.time, locals.exitCall);
}
function withAPMBeforeHandler(obj, args) {
proxy.callback(args, -1, null, null, self.agent.thread.current());
}
self.agent.on('destroy', function () {
if (obj.__appdynamicsProbeAttached__) {
delete obj.__appdynamicsProbeAttached__;
}
proxy.release(obj.Db.prototype.createCollection);
proxy.release(obj.Db.prototype.dropCollection);
if(obj.Server){
proxy.release(obj.Server.prototype.cursor);
proxy.release(obj.Server.prototype.insert);
proxy.release(obj.Server.prototype.update);
proxy.release(obj.Server.prototype.remove);
}
if(obj.ReplSet){
proxy.release(obj.ReplSet.prototype.cursor);
proxy.release(obj.ReplSet.prototype.insert);
proxy.release(obj.ReplSet.prototype.update);
proxy.release(obj.ReplSet.prototype.remove);
}
if(obj.Cursor){
proxy.release(obj.Cursor.prototype.next);
}
if(obj.Collection){
proxy.release(obj.Collection.prototype.find);
proxy.release(obj.Collection.prototype.insert);
proxy.release(obj.Collection.prototype.aggregate);
proxy.release(obj.Collection.prototype.remove);
proxy.release(obj.Collection.prototype.update);
}
if(obj.AbstractCursor){
proxy.release(obj.AbstractCursor.prototype.next);
}
});
if ('instrument' in obj) {
// driver 2.x with APM API
var serverPool;
var opQueue = {};
var listener = obj.instrument();
var apmCollectionEvents = ['create', 'drop'];
var collectionCommandMap = {
'create': 'createCollection',
'drop': 'dropCollection',
'find': 'find',
'cursor': 'cursor',
'insert': 'insert',
'update': 'update',
'remove': 'remove'
};
var supportedCommands = ['find', 'cursor', 'insert', 'update', 'remove'].concat(apmCollectionEvents);
// expose opQueue for integration testing
self.__opQueue = opQueue;
listener.on('started', function (event) {
var requestId = event.requestId, request, commandQuery;
if (supportedCommands.indexOf(event.commandName) < 0) return;
if (event.connectionId) {
var cid = event.connectionId;
if (typeof (cid) === 'string')
serverPool = [cid];
else if (typeof (cid) === 'number')
serverPool = [event.address];
else
serverPool = [cid.host + ':' + cid.port];
}
if (serverPool) {
if (!opQueue[requestId]) {
opQueue[requestId] = {
time: profiler.time(),
serverPool: serverPool
};
}
request = opQueue[requestId];
commandQuery = event.command.filter;
if (event.command.filter && Object.prototype.toString.call(event.command.filter) == "[object Object]") {
commandQuery = utility.filterSensitiveDataFromObject(utility.deepCopy(event.command.filter));
}
var commandDetails = {
command: collectionCommandMap[event.commandName],
databaseName: event.databaseName,
collectionName: event.command[event.commandName],
query: profiler.sanitize(JSON.stringify(commandQuery)),
numberToSkip: event.command.skip,
numberToReturn: event.command.limit
};
request.exitCall = self.createExitCall(request.time, serverPool,
commandDetails, event.commandName == 'find' ? 'read' : 'write',
profiler.stackTrace());
}
});
listener.on('succeeded', function (event) {
var requestId = event.requestId, request = opQueue[requestId];
if (request) {
self.addExitCall(request.time, request.exitCall);
opQueue[requestId] = undefined;
}
});
listener.on('failed', function (event) {
var requestId = event.requestId, request = opQueue[requestId];
if (request) {
self.addExitCall(event.time, event.exitCall, event.failure);
opQueue[requestId] = undefined;
}
});
supportedCommands.forEach(function (command) {
proxy.before(obj.Collection.prototype, command, withAPMBeforeHandler);
proxy.before(obj.Server.prototype, command, withAPMBeforeHandler);
proxy.before(obj.ReplSet.prototype, command, withAPMBeforeHandler);
});
collectionCommands.forEach(function (command) {
proxy.before(obj.Db.prototype, command, withAPMBeforeHandler);
});
} else if (!('version' in obj)) {
if (obj.Cursor){
// driver 2.x w/out APM API
var commands = ['cursor', 'insert', 'update', 'remove'];
// queries via a cursor find command must be reported after they complete:
proxy.around(obj.Cursor.prototype, '_next', function (obj, args, locals) {
locals.methodHasCb = proxy.callback(args, -1, function (obj_, args) {
complete(args, obj);
});
}, after);
collectionCommands.forEach(function (command) {
function withoutAPMBeforeHandler(obj, args, locals) {
var commandDetails;
var category;
var commandName = command == 'cursor' ? 'find' : command;
if (command == 'cursor' && !args[1].find) return;
var serverPool = [];
if (obj.serverConfig instanceof mongoObj.ReplSet)
serverPool = self.getServerPool(obj.serverConfig.s, true);
else
serverPool = self.getServerPool(obj);
if (serverPool.length) {
commandDetails = {
command: commandName,
databaseName: obj.s.databaseName,
collectionName: args[0]
};
if (obj.auths && obj.auths.length > 0) {
commandDetails.auth = obj.auths[0];
}
category = "write";
locals.time = profiler.time();
locals.exitCall = self.createExitCall(locals.time, serverPool,
commandDetails, category, profiler.stackTrace());
}
locals.methodHasCb = proxy.callback(args, -1, function (obj, args) {
self.addExitCall(locals.time, locals.exitCall, args);
});
}
proxy.around(obj.Db.prototype, command, withoutAPMBeforeHandler, withoutAPMAfterHandler);
});
commands.forEach(function (command) {
var commandName = command == 'cursor' ? 'find' : command;
function withoutAPMBeforeHandler(obj, args, locals) {
var commandDetails;
var category;
var opts = {};
var query = '';
if (command == 'cursor' && !args[1].find) return;
var serverPool = [];
if (obj instanceof mongoObj.ReplSet)
serverPool = self.getServerPool(obj.s, true);
else
serverPool = self.getServerPool(obj.s);
if (serverPool.length) {
opts = {};
if (args[1] && args[1].query) {
query = args[1].query;
if (Object.prototype.toString.call(query) == "[object Object]") {
query = utility.filterSensitiveDataFromObject(Object.assign({}, args[1].query));
}
query = profiler.sanitize(JSON.stringify(query));
Object.keys(args[1]).forEach(function (key) {
if (key !== 'query') {
opts[key] = args[1][key];
}
});
}
commandDetails = {
command: commandName,
databaseName: args[0].split('.')[0],
collectionName: args[0].split('.')[1],
query: query,
numberToSkip: opts.skip,
numberToReturn: opts.limit
};
if (obj.s.auths && obj.s.auths.length > 0) {
commandDetails.auth = obj.s.auths[0];
}
if (commandName == 'find') {
category = "read";
} else {
category = "write";
}
locals.time = profiler.time();
locals.exitCall = self.createExitCall(locals.time, serverPool,
commandDetails, category, profiler.stackTrace());
}
if (commandName == 'find') {
// stash exit call for later processing
args[1].__appd_exitcall_info = {
time: locals.time,
exitCall: locals.exitCall
};
locals.methodHasCb = true;
} else {
locals.methodHasCb = proxy.callback(args, -1, function (obj, args) {
self.addExitCall(locals.time, locals.exitCall, args);
}, null, self.agent.thread.current());
}
}
proxy.around(obj.Server.prototype, command, withoutAPMBeforeHandler, withoutAPMAfterHandler);
proxy.around(obj.ReplSet.prototype, command, withoutAPMBeforeHandler, withoutAPMAfterHandler);
});
}
else{
proxy.before(obj.MongoClient.prototype, 'connect', function(obj){
obj.monitorCommands = true;
var newServerPool;
var newOpQueue = new Map();
var newApmCollectionEvents = ['create', 'drop'];
var newCollectionCommandMap = {
'create': 'createCollection',
'drop': 'dropCollection',
'find': 'find',
'cursor': 'cursor',
'insert': 'insert',
'update': 'update',
'remove': 'remove',
'aggregate': 'aggregate'
};
var newSupportedCommands = ['find', 'cursor', 'insert', 'update', 'remove', 'aggregate'].concat(newApmCollectionEvents);
// expose opQueue for integration testing
self.__opQueue = newOpQueue;
obj.on('commandStarted', (event) => {
var requestId = event.requestId, newRequest, newCommandQuery;
if (newSupportedCommands.indexOf(event.commandName) < 0) return;
if (event.address) {
newServerPool = [event.address];
}
if (newServerPool) {
if (!newOpQueue.has(requestId)) {
newOpQueue.set(requestId, {
time: profiler.time(),
serverPool: newServerPool
});
}
newRequest = newOpQueue.get(requestId);
newCommandQuery = event.command.filter;
if (event.command.filter && Object.prototype.toString.call(event.command.filter) == "[object Object]") {
newCommandQuery = utility.filterSensitiveDataFromObject(utility.deepCopy(event.command.filter));
}
var commandDetails = {
command: newCollectionCommandMap[event.commandName],
databaseName: event.databaseName,
collectionName: event.command[event.commandName],
query: profiler.sanitize(JSON.stringify(newCommandQuery)),
numberToSkip: event.command.skip,
numberToReturn: event.command.limit
};
newRequest.exitCall = self.createExitCall(newRequest.time, newServerPool,
commandDetails, event.commandName == 'find' ? 'read' : 'write',
profiler.stackTrace());
}
});
obj.on('commandSucceeded', function (event) {
var requestId = event.requestId, newRequest = newOpQueue.get(requestId);
if (newRequest) {
self.addExitCall(newRequest.time, newRequest.exitCall);
newOpQueue.delete(requestId);
}
});
obj.on('commandFailed', function (event) {
var requestId = event.requestId, newRequest = newOpQueue.get(requestId);
if (newRequest) {
self.addExitCall(event.time, event.exitCall, event.failure);
newOpQueue.delete(requestId);
}
});
});
}
}
function complete(err, obj, driver) {
var exitCallInfoHolder;
if (driver && driver == '1.x') {
exitCallInfoHolder = obj;
} else {
if (!obj.cursorState.dead && (!obj.cursorState.notified || !(obj.cursorState.documents.length == 0))) return;
if (!obj.cmd || !obj.cmd.__appd_exitcall_info) return;
exitCallInfoHolder = obj.cmd.__appd_exitcall_info;
}
if (!exitCallInfoHolder.exitCall) return;
if (!exitCallInfoHolder.time) return;
self.addExitCall(exitCallInfoHolder.time, exitCallInfoHolder.exitCall, err);
}
function after(obj, args, ret, locals) {
if (locals.methodHasCb) return;
if (locals.driver == '1.x')
obj = locals;
if (!ret || !ret.__appdynamicsIsPromiseResult__)
complete(null, obj, locals.driver);
else if (ret.error)
complete(ret.error, obj, locals.driver);
else {
complete(null, obj, locals.driver);
}
}
};
MongodbProbe.prototype.getServerPool = function (db, isReplicaSet) {
var serverPool = [];
// db.replset keeps server details for MongoDriver 2.x
// db.coreTopology keeps server details for MongoDriver 3.x
var serverConfig;
if (isReplicaSet) {
serverConfig = db && (db.replset || db.coreTopology || db);
if (serverConfig && serverConfig.ismaster && serverConfig.ismaster.primary) {
serverPool.push(serverConfig.ismaster.primary);
}
} else {
serverConfig = db && (db.serverConfig || db.serverDetails || db);
if (serverConfig) {
if (serverConfig.s && serverConfig.s.host && serverConfig.s.port) {
serverPool.push(serverConfig.s.host + ':' + serverConfig.s.port);
}
else if (serverConfig.host && serverConfig.port) {
serverPool.push(serverConfig.host + ':' + serverConfig.port);
}
else if (Array.isArray(serverConfig.servers)) {
serverConfig.servers.forEach(function (server) {
serverPool.push(server.host + ':' + server.port);
});
}
}
}
if (serverPool.length) {
serverPool.sort();
}
return serverPool;
};
MongodbProbe.prototype.createExitCall = function (time, serverPool, commandDetails, category, stackTrace) {
var address = serverPool[serverPool.length - 1],
exitCallCommand;
try {
exitCallCommand = JSON.stringify(commandDetails);
} catch (e) {
return;
}
var supportedProperties = {
'HOST': address.split(':')[0],
'PORT': address.split(':')[1],
'DATABASE': commandDetails.databaseName
};
return this.agent.profiler.createExitCall(time, {
exitType: 'EXIT_CUSTOM',
exitSubType: 'Mongo DB',
configType: 'Mongodb',
supportedProperties: supportedProperties,
category: category,
command: exitCallCommand,
stackTrace: stackTrace,
vendor: "MONGODB"
});
};
MongodbProbe.prototype.addExitCall = function (time, exitCall, args) {
var self = this;
if (!time || !time.done()) return;
var error = self.agent.proxy.getErrorObject(args);
var profiler = self.agent.profiler;
if (exitCall) {
profiler.addExitCall(time, exitCall, error);
}
};