diff --git a/src/index.js b/src/index.js index 48e7341..93e0a66 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,8 @@ import Perf from 'performance-node'; - import pkg from '../package.json'; // eslint-disable-line import/extensions import { addToReport, addTraceData } from './addToReport'; -const load = plugin => { +const loadPlugin = plugin => { /*eslint-disable camelcase, no-undef*/ if (typeof __non_webpack_require__ === 'function') { return __non_webpack_require__(`./plugins/${plugin}`); @@ -144,7 +143,7 @@ class TracePlugin { if (context.config[conf] && context.config[conf].enabled) { // getting plugin; allows this to be loaded only if enabled. - await load(`${k}`).then(mod => { + await loadPlugin(`${k}`).then(mod => { plugins[k].wrap = mod.wrap; plugins[k].unwrap = mod.unwrap; context[namespace] = { @@ -153,7 +152,7 @@ class TracePlugin { data: {}, config: context.config[conf] }; - plugins[k].wrap(context[namespace]); + return plugins[k].wrap(context[namespace]); }); } }); diff --git a/src/loadHelper.js b/src/loadHelper.js new file mode 100644 index 0000000..10f9090 --- /dev/null +++ b/src/loadHelper.js @@ -0,0 +1,52 @@ +import { debuglog } from 'util'; + +const debug = debuglog('@iopipe:trace:loadHelper'); + +// The module being traced might not be in the lambda NODE_PATH, +// particularly if IOpipe has been installed with lambda layers. +// So here's an attempt at a fallback. + +const deriveTargetPath = manualPath => { + const path = manualPath ? manualPath : process.env.LAMBDA_TASK_ROOT; + if (!path) { + return false; + } + const targetPath = `${path}/node_modules`; + process.env.IOPIPE_TARGET_PATH = targetPath; + return targetPath; +}; + +const appendToPath = manualPath => { + if (!process.env || !process.env.NODE_PATH) { + return false; + } + const targetPath = deriveTargetPath(manualPath); + const pathArray = process.env.NODE_PATH.split(':'); + if (!targetPath && pathArray.indexOf(targetPath) === -1) { + process.env.NODE_PATH = `${process.env.NODE_PATH}:${targetPath}`; + } + return targetPath; +}; + +appendToPath(); + +const loadModuleForTracing = async (module, path) => { + const targetPath = path ? path : process.env.IOPIPE_TARGET_PATH; + let loadedModule; + try { + loadedModule = await require(module); + } catch (e) { + debug(`Unable to load ${module} from require; trying TASK_ROOT path.`, e); + try { + if (targetPath) { + loadedModule = await require(`${targetPath}/${module}`); + } + } catch (err) { + debug(`Unable to load ${module} from path ${targetPath}`, err); + } + } + + return loadedModule; +}; + +export default loadModuleForTracing; diff --git a/src/plugins/https.js b/src/plugins/https.js index 93a3c68..7ba43aa 100644 --- a/src/plugins/https.js +++ b/src/plugins/https.js @@ -9,7 +9,7 @@ import pickBy from 'lodash.pickby'; import isArray from 'isarray'; import { flatten } from 'flat'; -const debug = debuglog('@iopipe/trace'); +const debug = debuglog('@iopipe:trace:https'); /*eslint-disable babel/no-invalid-this*/ diff --git a/src/plugins/ioredis.js b/src/plugins/ioredis.js index c3bf35a..d606625 100644 --- a/src/plugins/ioredis.js +++ b/src/plugins/ioredis.js @@ -1,10 +1,27 @@ import { debuglog } from 'util'; import shimmer from 'shimmer'; -import Redis from 'ioredis'; import Perf from 'performance-node'; import uuid from 'uuid/v4'; +import loadModuleForTracing from '../loadHelper'; -const debug = debuglog('@iopipe/trace'); +const debug = debuglog('@iopipe:trace:ioredis'); + +let Redis, RedisTarget, RedisCmdTarget; + +const loadModule = () => + loadModuleForTracing('ioredis') + .then(module => { + Redis = module; + RedisTarget = Redis && Redis.prototype; + if (Redis.Command && Redis.Command.prototype) { + RedisCmdTarget = Redis.Command && Redis.Command.prototype; + } + return module; + }) + .catch(e => { + debug('Not loading ioredis', e); + return false; + }); /*eslint-disable babel/no-invalid-this*/ /*eslint-disable func-name-matching */ @@ -28,7 +45,12 @@ const filterRequest = (command, context) => { }; }; -function wrap({ timeline, data = {} } = {}) { +async function wrap({ timeline, data = {} } = {}) { + await loadModule(); + if (!Redis) { + debug('ioredis plugin not accessible from trace plugin. Skipping.'); + return false; + } if (!(timeline instanceof Perf)) { debug( 'Timeline passed to plugins/ioredis.wrap not an instance of performance-node. Skipping.' @@ -36,15 +58,11 @@ function wrap({ timeline, data = {} } = {}) { return false; } - if (!Redis.__iopipeShimmer) { + if (Redis && !Redis.__iopipeShimmer) { if (process.env.IOPIPE_TRACE_IOREDIS_INITPROMISE) { - shimmer.wrap( - Redis.Command && Redis.Command.prototype, - 'initPromise', - wrapPromise - ); + shimmer.wrap(RedisCmdTarget, 'initPromise', wrapPromise); } - shimmer.wrap(Redis && Redis.prototype, 'sendCommand', wrapSendCommand); + shimmer.wrap(RedisTarget, 'sendCommand', wrapSendCommand); Redis.__iopipeShimmer = true; } @@ -128,11 +146,18 @@ function wrap({ timeline, data = {} } = {}) { } function unwrap() { + if (!Redis) { + debug( + 'ioredis plugin not accessible from trace plugin. Nothing to unwrap.' + ); + return false; + } if (process.env.IOPIPE_TRACE_IOREDIS_INITPROMISE) { - shimmer.unwrap(Redis.Command && Redis.Command.prototype, 'initPromise'); + shimmer.unwrap(RedisCmdTarget, 'initPromise'); } - shimmer.unwrap(Redis && Redis.prototype, 'sendCommand'); + shimmer.unwrap(RedisTarget, 'sendCommand'); delete Redis.__iopipeShimmer; + return true; } export { unwrap, wrap }; diff --git a/src/plugins/ioredis.test.js b/src/plugins/ioredis.test.js index 1a18aa6..37fe2d6 100644 --- a/src/plugins/ioredis.test.js +++ b/src/plugins/ioredis.test.js @@ -64,8 +64,8 @@ xtest('Redis works as normal if wrap is not called', done => { }); }); -test('Bails if timeline is not instance of performance-node', () => { - const bool = wrap({ timeline: [] }); +test('Bails if timeline is not instance of performance-node', async () => { + const bool = await wrap({ timeline: [] }); expect(bool).toBe(false); }); @@ -128,7 +128,7 @@ xdescribe('Wrapping Redis', () => { db: 1 }); - expect(redis.sendCommand.__wrapped).toBeDefined(); + expect(redis.__wrapped).toBeDefined(); const expectedStr = 'wrapped ioredis, async/await'; @@ -152,7 +152,7 @@ xdescribe('Wrapping Redis', () => { redis = new Redis({ host: '127.0.0.1', connectionName: 'Test 2', db: 1 }); - expect(redis.sendCommand.__wrapped).toBeDefined(); + expect(redis.__wrapped).toBeDefined(); const expectedStr = 'wrapped ioredis, promise syntax'; @@ -185,7 +185,7 @@ xdescribe('Wrapping Redis', () => { redis = new Redis({ host: 'localhost', connectionName: 'Test 3', db: 1 }); - expect(redis.sendCommand.__wrapped).toBeDefined(); + expect(redis.__wrapped).toBeDefined(); const expectedStr = 'wrapped ioredis, callback syntax'; redis.set('testString', expectedStr); diff --git a/src/plugins/mongodb.js b/src/plugins/mongodb.js index 7ae6b70..94ba6ea 100644 --- a/src/plugins/mongodb.js +++ b/src/plugins/mongodb.js @@ -1,9 +1,42 @@ 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'; +import loadModuleForTracing from '../loadHelper'; + +const debug = debuglog('@iopipe:trace:mongodb'); + +let MongoClient, + Server, + Cursor, + Collection, + clientTarget, + collectionTarget, + serverTarget, + cursorTarget; + +const loadModule = async () => { + const mod = await loadModuleForTracing('mongodb') + .then(module => { + MongoClient = module.MongoClient; + Server = module.Server; + Cursor = module.Cursor; + Collection = module.Collection; + + clientTarget = MongoClient && MongoClient.prototype; + collectionTarget = Collection && Collection.prototype; + serverTarget = Server && Server.prototype; + cursorTarget = Cursor && Cursor.prototype; + + return module; + }) + .catch(e => { + debug('Not loading mongodb', e); + return null; + }); + return mod; +}; const dbType = 'mongodb'; const serverOps = ['command', 'insert', 'update', 'remove']; @@ -23,13 +56,6 @@ const collectionOps = [ 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 */ @@ -146,7 +172,13 @@ const filterRequest = (params, context) => { }; }; -function wrap({ timeline, data = {} } = {}) { +async function wrap({ timeline, data = {} } = {}) { + await loadModule(); + + if (!clientTarget) { + debug('mongodb plugin not accessible from trace plugin. Skipping.'); + return false; + } if (!(timeline instanceof Perf)) { debug( 'Timeline passed to plugins/mongodb.wrap not an instance of performance-node. Skipping.' @@ -217,6 +249,13 @@ function wrap({ timeline, data = {} } = {}) { } function unwrap() { + if (!clientTarget) { + debug( + 'mongodb plugin not accessible from trace plugin. Nothing to unwrap.' + ); + return false; + } + if (serverTarget.__iopipeShimmer) { shimmer.massUnwrap(serverTarget, serverOps); delete serverTarget.__iopipeShimmer; @@ -233,6 +272,7 @@ function unwrap() { shimmer.massUnwrap(clientTarget, clientOps); // mass just seems to hang and not complete delete clientTarget.__iopipeShimmer; } + return true; } export { unwrap, wrap }; diff --git a/src/plugins/mongodb.test.js b/src/plugins/mongodb.test.js index 7296f64..26345de 100644 --- a/src/plugins/mongodb.test.js +++ b/src/plugins/mongodb.test.js @@ -229,8 +229,8 @@ xdescribe('MongoDb works as normal if wrap is not called', () => { }); }); -test('Bails if timeline is not instance of performance-node', () => { - const bool = wrap({ timeline: [] }); +test('Bails if timeline is not instance of performance-node', async () => { + const bool = await wrap({ timeline: [] }); expect(bool).toBe(false); }); @@ -290,36 +290,36 @@ xdescribe('Wrapping MongoDB', () => { }); }); // */ - test('Wrap works with connect', done => { + test('Wrap works with connect', async () => { const timeline = new Perf({ timestamp: true }); const data = {}; - wrap({ timeline, data }); + await wrap({ timeline, data }); const c = createClient(); expect(c.__iopipeShimmer).toBe(true); - c.connect(err => { + return c.connect(err => { expect(err).toBeNull(); timelineExpect(timeline, data, { commandName: 'connect', dbName: 'admin' }); - c.close(); - return done(err); + return c.close(); + //return done(err); }); }); - test('Wrap generates traces for insert', done => { + test('Wrap generates traces for insert', async () => { const timeline = new Perf({ timestamp: true }); const data = {}; expect(timeline.data).toHaveLength(0); - wrap({ timeline, data }); + await wrap({ timeline, data }); const c = createClient(); expect(c.__iopipeShimmer).toBe(true); - c.connect((clErr, client) => { + c.connect(async (clErr, client) => { expect(clErr).toBeNull(); const documents = [{ a: 1 }, { b: 2 }, { c: 3 }]; const db = client.db(dbName); - insertDocuments(documents, collection, db, (err, result) => { + await insertDocuments(documents, collection, db, (err, result) => { expect(err).toBeNull(); expect(result.result.ok).toBe(1); expect(result.result.n).toBe(documents.length); @@ -331,15 +331,15 @@ xdescribe('Wrapping MongoDB', () => { dbName, collection }); - c.close(); - return done(err); + return c.close(); + // return done(err); }); }); }); - test('Wrap generates traces for find', done => { + test('Wrap generates traces for find', async () => { const timeline = new Perf({ timestamp: true }); const data = {}; - wrap({ timeline, data }); + await wrap({ timeline, data }); expect(timeline.data).toHaveLength(0); const c = createClient(); expect(c.__iopipeShimmer).toBe(true); @@ -357,42 +357,48 @@ xdescribe('Wrapping MongoDB', () => { dbName, collection }); - c.close(); - return done(err); + return c.close(); + //return done(err); }); }); }); - test('Wrap generates traces for update', done => { + test('Wrap generates traces for update', async () => { const timeline = new Perf({ timestamp: true }); const data = {}; - wrap({ timeline, data }); + await wrap({ timeline, data }); expect(timeline.data).toHaveLength(0); const c = createClient(); - c.connect((clErr, client) => { + c.connect(async (clErr, client) => { expect(clErr).toBeNull(); const db = client.db(dbName); - updateDocument({ a: 1 }, { a: 7 }, collection, db, (err, results) => { - expect(err).toBeNull(); - expect(results).toBeDefined(); - expect(results.result.n).toBe(1); - expect(results.result.nModified).toBe(1); - expect(results.result.ok).toBe(1); - - timelineExpect(timeline, data, { - commandName: 'updateOne', - dbName, - collection - }); - c.close(); - return done(err); - }); + await updateDocument( + { a: 1 }, + { a: 7 }, + collection, + db, + (err, results) => { + expect(err).toBeNull(); + expect(results).toBeDefined(); + expect(results.result.n).toBe(1); + expect(results.result.nModified).toBe(1); + expect(results.result.ok).toBe(1); + + timelineExpect(timeline, data, { + commandName: 'updateOne', + dbName, + collection + }); + return c.close(); + // return done(err); + } + ); }); }); - test('Wrap generates traces for delete', done => { + test('Wrap generates traces for delete', async () => { const timeline = new Perf({ timestamp: true }); const data = {}; - wrap({ timeline, data }); + await wrap({ timeline, data }); expect(timeline.data).toHaveLength(0); const c = createClient(); c.connect((clErr, client) => { @@ -410,15 +416,15 @@ xdescribe('Wrapping MongoDB', () => { dbName, collection }); - c.close(); - return done(err); + return c.close(); + // return done(err); }); }); }); - test('Client can trace bulk writes', done => { + test('Client can trace bulk writes', async () => { const timeline = new Perf({ timestamp: true }); const data = {}; - wrap({ timeline, data }); + await wrap({ timeline, data }); expect(timeline.data).toHaveLength(0); const c = createClient(); c.connect(async (clErr, client) => { @@ -477,7 +483,7 @@ xdescribe('Wrapping MongoDB', () => { expect(data[lastKey].name).toBe('bulkWrite'); expect(data[lastKey].request.bulkCommands).toBeDefined(); expect(data[lastKey].request.bulkCommands).toBe(bulkCommands); - return done(); + return data; }); }); }); diff --git a/src/plugins/redis.js b/src/plugins/redis.js index 10cd3a9..42b0392 100644 --- a/src/plugins/redis.js +++ b/src/plugins/redis.js @@ -1,10 +1,25 @@ import { debuglog } from 'util'; import shimmer from 'shimmer'; -import redis from 'redis'; +// import redis from 'redis'; import Perf from 'performance-node'; import uuid from 'uuid/v4'; +import loadModuleForTracing from '../loadHelper'; -const debug = debuglog('@iopipe/trace'); +let redis, redisTarget; + +const debug = debuglog('@iopipe:trace:redis'); + +const loadModule = () => + loadModuleForTracing('redis') + .then(module => { + redis = module; + redisTarget = redis.RedisClient && redis.RedisClient.prototype; + return module; + }) + .catch(e => { + debug('Not loading redis', e); + return false; + }); /*eslint-disable babel/no-invalid-this*/ /*eslint-disable func-name-matching */ @@ -30,8 +45,12 @@ const filterRequest = (command, context) => { }; }; -function wrap({ timeline, data = {} } = {}) { - const target = redis.RedisClient && redis.RedisClient.prototype; +async function wrap({ timeline, data = {} } = {}) { + await loadModule(); + if (!redis) { + debug('redis plugin not accessible from trace plugin. Skipping.'); + return false; + } if (!(timeline instanceof Perf)) { debug( @@ -42,11 +61,15 @@ function wrap({ timeline, data = {} } = {}) { if (!redis.__iopipeShimmer) { if (process.env.IOPIPE_TRACE_REDIS_CB) { - shimmer.wrap(target, 'send_command', wrapSendCommand); // redis < 2.5.3 + shimmer.wrap(redisTarget, 'send_command', wrapSendCommand); // redis < 2.5.3 } else { - shimmer.wrap(target, 'internal_send_command', wrapInternalSendCommand); + shimmer.wrap( + redisTarget, + 'internal_send_command', + wrapInternalSendCommand + ); } - target.__iopipeShimmer = true; + redisTarget.__iopipeShimmer = true; } return true; @@ -118,14 +141,18 @@ function wrap({ timeline, data = {} } = {}) { } function unwrap() { - const target = redis.RedisClient && redis.RedisClient.prototype; + if (!redis) { + debug('redis plugin not accessible from trace plugin. Nothing to unwrap.'); + return false; + } if (process.env.IOPIPE_TRACE_REDIS_CB) { - shimmer.unwrap(target, 'send_command'); + shimmer.unwrap(redisTarget, 'send_command'); } else { - shimmer.unwrap(target, 'internal_send_command'); + shimmer.unwrap(redisTarget, 'internal_send_command'); } - delete redis.__iopipeShimmer; + delete redisTarget.__iopipeShimmer; + return true; } export { unwrap, wrap }; diff --git a/src/plugins/redis.test.js b/src/plugins/redis.test.js index 9116c77..fb3acf9 100644 --- a/src/plugins/redis.test.js +++ b/src/plugins/redis.test.js @@ -87,8 +87,8 @@ xtest('Redis works as normal if wrap is not called', done => { client.set('testString', expectedStr, setCb); }); -test('Bails if timeline is not instance of performance-node', () => { - const bool = wrap({ timeline: [] }); +test('Bails if timeline is not instance of performance-node', async () => { + const bool = await wrap({ timeline: [] }); expect(bool).toBe(false); });