-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(dbtraces): add auto-tracing for MongoDB
Merge pull request #67 from mrickard/issue/65-mongodb-autotrace
- Loading branch information
Showing
6 changed files
with
754 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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`. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
Oops, something went wrong.