Skip to content

Commit

Permalink
feat(dbtraces): add auto-tracing for MongoDB
Browse files Browse the repository at this point in the history
Merge pull request #67 from mrickard/issue/65-mongodb-autotrace
  • Loading branch information
mrickard authored Sep 25, 2019
2 parents abd158a + 6f2274b commit 81b52e6
Show file tree
Hide file tree
Showing 6 changed files with 754 additions and 8 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ If you're using [email protected] or earlier, turn on auto-tracing with the `IOPIPE_TR

Set the environment variable `IOPIPE_TRACE_IOREDIS` to `true`, and your function will enable automatic traces on Redis commands: the name of the command, name of the host, port, and connection (if defined in your connection options), and the key being written or read. Commands batched with multi/exec are traced individually, so you can measure individual performance within batch operations.

#### `autoMongoDb` Automatically trace MongoDB commands

Set the environment variable `IOPIPE_TRACE_MONGODB` to `true`, and IOpipe will trace commands automatically: which command, which key is being read or written, database and collection name (if available), and the connection's hostname and port. This plugin supports these commands:

* `command`, `insert`, `update`, and `remove` on the Server class
* `connect`, `close`, and `db` on the MongoClient class.
* `find`, `findOne`, `insertOne`, `insertMany`, `updateOne`, `updateMany`, `replaceOne`, `deleteOne`, `deleteMany`, `createIndex` on collections. `bulkWrite` is also supported, and generates a list of which commands were part of the bulk operation.
* `next`, `filter`, `sort`, `hint`, and `toArray` on the Cursor class.

Commands used with a callback parameter generate end traces and duration metrics. (Note that MongoDB commands that don't take callback params--like `find`--won't generate durations.)

This plugin supports MongoDB Node.JS Driver v3.3 and newer.

#### `autoMeasure` (bool: optional = true)

By default, the plugin will create auto-measurements for marks with matching `mark.start` and `mark.end`. These measurements will be displayed in the [IOpipe Dashboard](https://dashboard.iopipe.com). If you'd like to turn this off, set `autoMeasure: false`.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"got": "^8.3.1",
"ioredis": "^4",
"lodash": "^4.17.4",
"mongodb": "^3.3",
"redis": "^2",
"superagent": "^3.8.3"
},
Expand Down
20 changes: 16 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const plugins = {
flag: 'IOPIPE_TRACE_IOREDIS',
entries: 'dbTraceEntries'
},
mongodb: {
config: 'autoMongoDb',
flag: 'IOPIPE_TRACE_MONGODB',
entries: 'dbTraceEntries'
},
redis: {
config: 'autoRedis',
flag: 'IOPIPE_TRACE_REDIS',
Expand Down Expand Up @@ -50,6 +55,7 @@ function getConfig(config = {}) {
: getBooleanFromEnv('IOPIPE_TRACE_AUTO_HTTP_ENABLED')
},
autoIoRedis = { enabled: getBooleanFromEnv('IOPIPE_TRACE_IOREDIS') },
autoMongoDb = { enabled: getBooleanFromEnv('IOPIPE_TRACE_MONGODB') },
autoRedis = { enabled: getBooleanFromEnv('IOPIPE_TRACE_REDIS') }
} = config;
return {
Expand All @@ -66,6 +72,12 @@ function getConfig(config = {}) {
? autoIoRedis.enabled
: getBooleanFromEnv('IOPIPE_TRACE_IOREDIS')
},
autoMongoDb: {
enabled:
typeof autoMongoDb.enabled === 'boolean'
? autoMongoDb.enabled
: getBooleanFromEnv('IOPIPE_TRACE_MONGODB')
},
autoRedis: {
enabled:
typeof autoRedis.enabled === 'boolean'
Expand Down Expand Up @@ -126,13 +138,13 @@ class TracePlugin {
const context = this;
const pluginKeys = Object.keys(plugins);

pluginKeys.forEach(k => {
pluginKeys.map(async k => {
const conf = plugins[k].config;
const namespace = `${conf}Data`;

if (context.config[conf].enabled) {
if (context.config[conf] && context.config[conf].enabled) {
// getting plugin; allows this to be loaded only if enabled.
load(`${k}`).then(mod => {
await load(`${k}`).then(mod => {
plugins[k].wrap = mod.wrap;
plugins[k].unwrap = mod.unwrap;
context[namespace] = {
Expand Down Expand Up @@ -166,7 +178,7 @@ class TracePlugin {
pluginKeys.forEach(k => {
const conf = plugins[k].config;
if (context.config[conf].enabled) {
if (context.config[conf].enabled) {
if (plugins[k].unwrap && typeof plugins[k].unwrap === 'function') {
plugins[k].unwrap();
}
}
Expand Down
238 changes: 238 additions & 0 deletions src/plugins/mongodb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { debuglog } from 'util';
import shimmer from 'shimmer';
import { MongoClient, Server, Cursor, Collection } from 'mongodb';
import Perf from 'performance-node';
import uuid from 'uuid/v4';
import get from 'lodash/get';

const dbType = 'mongodb';
const serverOps = ['command', 'insert', 'update', 'remove'];
const collectionOps = [
'find',
'findOne',
'insertOne',
'insertMany',
'updateOne',
'updateMany',
'replaceOne',
'deleteOne',
'deleteMany',
'bulkWrite',
'createIndex'
];
const cursorOps = ['next', 'filter', 'sort', 'hint', 'toArray'];
const clientOps = ['connect', 'close', 'db'];

const clientTarget = MongoClient && MongoClient.prototype;
const collectionTarget = Collection && Collection.prototype;
const serverTarget = Server && Server.prototype;
const cursorTarget = Cursor && Cursor.prototype;

const debug = debuglog('@iopipe/trace');

/*eslint-disable babel/no-invalid-this*/
/*eslint-disable func-name-matching */
/*eslint-disable prefer-rest-params */
/*eslint-disable prefer-spread */

const createId = () => `mongodb-${uuid()}`;

const filterArrayArgs = args =>
args.map(arg => {
if (arg._id) {
return arg._id;
}
return Object.keys(arg).join(', ');
});

const extractHostAndPort = ctx => {
let host, port, targetObj;
const obj = ctx.s;
const tServers = get(obj, 'topology.s.options.servers');

if (ctx instanceof Cursor) {
targetObj = get(ctx, 'options.db.s.topology.s.options.servers');
} else if (tServers) {
targetObj = tServers;
}

if (obj.clonedOptions) {
host = obj.clonedOptions.host;
port = obj.clonedOptions.port;
} else if (targetObj && targetObj.length && targetObj.length > 0) {
const server = targetObj[0];
host = server.host;
port = server.port;
} else if (obj.url) {
const urlArray = obj.url.split(':');
host = urlArray[1].replace('//', '');
port = urlArray[2];
}
return { host, port };
};

const extractDbAndTable = obj => {
let db, table;
if (!db && obj.s.namespace) {
db = obj.s.namespace.db;
} else if (!db && obj.namespace) {
db = obj.namespace.db;
table = obj.namespace.collection;
}
if (!table && obj.s.namespace) {
table = obj.s.namespace.collection;
}
return { db, table };
};

const filterRequest = (params, context) => {
if (!context || !context.s) {
return null;
}
const { command, args } = params;
let { host, port } = context.s;
let db, table;

let filteredArgs = [];
let bulkCommands = [];

for (let i = 0; i < args.length; i++) {
let argData;
const isObject = typeof args[i] === 'object';

if (args[i].db || args[i].collection) {
db = args[i].db ? args[i].db : null;
table = args[i].collection ? args[i].collection : null;
}

if (isObject && args[i].length) {
argData = filterArrayArgs(args[i]);
} else if (isObject) {
argData = Object.keys(args[i]);
} else if (typeof args[i] !== 'function') {
argData = args[i];
}

if (argData && argData.length && command === 'bulkWrite') {
bulkCommands = [...bulkCommands, ...argData];
} else if (typeof argData === 'object' && argData.length) {
filteredArgs = [...filteredArgs, ...argData];
} else if (argData) {
filteredArgs = [...filteredArgs, argData];
}
}

if (!host && !port) {
const obj = extractHostAndPort(context);
host = obj.host;
port = obj.port;
}

if (!db) {
const dbInfo = extractDbAndTable(context);
db = dbInfo.db;
table = dbInfo.table;
}

return {
command,
key:
typeof filteredArgs === 'object' ? filteredArgs.join(', ') : filteredArgs,
bulkCommands: bulkCommands.join(', '),
hostname: host,
port,
db,
table
};
};

function wrap({ timeline, data = {} } = {}) {
if (!(timeline instanceof Perf)) {
debug(
'Timeline passed to plugins/mongodb.wrap not an instance of performance-node. Skipping.'
);
return false;
}

if (!clientTarget.__iopipeShimmer) {
shimmer.massWrap(clientTarget, clientOps, wrapCommand);
clientTarget.__iopipeShimmer = true;
}
if (!collectionTarget.__iopipeShimmer) {
shimmer.massWrap(collectionTarget, collectionOps, wrapCommand);
collectionTarget.__iopipeShimmer = true;
}
if (!serverTarget.__iopipeShimmer) {
shimmer.massWrap(serverTarget, serverOps, wrapCommand);
serverTarget.__iopipeShimmer = true;
}
if (!cursorTarget.__iopipeShimmer) {
shimmer.massWrap(cursorTarget, cursorOps, wrapCommand);
cursorTarget.__iopipeShimmer = true;
}

return true;

function wrapCommand(original, command) {
if (typeof original !== 'function') {
return original;
}
return function wrappedCommand() {
const context = this;
const id = createId();
const args = Array.prototype.slice.call(arguments);
let cb, cbIdx, wrappedCb;

for (const i in args) {
if (typeof args[i] === 'function') {
cb = args[i];
cbIdx = i;
}
}
if (!data[id]) {
timeline.mark(`start:${id}`);
data[id] = {
name: command,
dbType,
request: filterRequest({ command, args }, context)
};
}

if (typeof cb === 'function' && !cb.__iopipeTraceId) {
wrappedCb = function wrappedCallback(err) {
if (err) {
data[id].error = err.message;
data[id].errorStack = err.stack;
}
timeline.mark(`end:${id}`);
return cb.apply(this, arguments);
};
wrappedCb.__iopipeTraceId = id;
args[cbIdx] = wrappedCb;
}
this.__iopipeTraceId = id;
return original.apply(this, args);
};
}
}

function unwrap() {
if (serverTarget.__iopipeShimmer) {
shimmer.massUnwrap(serverTarget, serverOps);
delete serverTarget.__iopipeShimmer;
}
if (collectionTarget.__iopipeShimmer) {
shimmer.massUnwrap(collectionTarget, collectionOps);
delete collectionTarget.__iopipeShimmer;
}
if (cursorTarget.__iopipeShimmer) {
shimmer.massUnwrap(cursorTarget, cursorOps);
delete cursorTarget.__iopipeShimmer;
}
if (clientTarget.__iopipeShimmer) {
shimmer.massUnwrap(clientTarget, clientOps); // mass just seems to hang and not complete
delete clientTarget.__iopipeShimmer;
}
}

export { unwrap, wrap };
Loading

0 comments on commit 81b52e6

Please sign in to comment.