├── NodeReaction.js ├── lib ├── modules │ ├── http.js │ ├── postreSql.js │ ├── ioredis.js │ ├── redis.js │ ├── fs.js │ ├── mySql.js │ └── mongo.js ├── TraceTimer.js ├── Trace.js ├── DataExporter.js ├── RequestValidator.js ├── Transaction.js ├── Logger.js ├── LibraryLoader.js └── Agent.js ├── package.json ├── LICENSE ├── .gitignore └── README.md /NodeReaction.js: -------------------------------------------------------------------------------- 1 | const logger = require("./lib/logger").logStartup(); 2 | const Agent = require("./lib/Agent"); 3 | const libraries = require("./lib/LibraryLoader"); 4 | module.exports = Agent; 5 | -------------------------------------------------------------------------------- /lib/modules/http.js: -------------------------------------------------------------------------------- 1 | const nodeReactionAgent = require("../Agent.js"); 2 | const http = require("http"); 3 | const RequestValidator = require("./../RequestValidator"); 4 | 5 | let original = http.Server.prototype.emit; 6 | 7 | http.Server.prototype.emit = function(event, req, res) { 8 | if (event === "request") { 9 | if (RequestValidator.isValidRequest(req)) { 10 | let transaction = nodeReactionAgent.createTransaction(req); 11 | res.on("finish", function() { 12 | transaction.endTransaction(); 13 | }); 14 | } 15 | } 16 | return original.apply(this, arguments); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/TraceTimer.js: -------------------------------------------------------------------------------- 1 | class TraceTimer { 2 | constructor(uuid) { 3 | this.uuid; 4 | this.startTime; 5 | this.duration; 6 | this.startTimestamp; 7 | this.endTimestamp; 8 | } 9 | start() { 10 | this.startTimestamp = new Date(Date.now()).toISOString().slice(0, 23).replace('T', ' '); 11 | this.startTime = process.hrtime(); 12 | } 13 | end(){ 14 | let durationArr = process.hrtime(this.startTime); 15 | this.duration = durationArr[0] / 1000 + durationArr[1] / 1000000; 16 | this.endTimestamp = new Date(Date.now()).toISOString().slice(0, 23).replace('T', ' '); 17 | } 18 | } 19 | 20 | module.exports = TraceTimer; -------------------------------------------------------------------------------- /lib/Trace.js: -------------------------------------------------------------------------------- 1 | const uuidv4 = require("uuid/v4"); 2 | const TraceTimer = require("./TraceTimer"); 3 | class Trace { 4 | constructor(transaction, library, type) { 5 | this.transaction = transaction; 6 | this.library = library; 7 | this.type = type; 8 | this.uuid = uuidv4(); 9 | this.finished = false; 10 | this.traceTimer = new TraceTimer(this.uuid); 11 | this.traceTimer.start(); 12 | } 13 | 14 | end() { 15 | this.traceTimer.end(); 16 | // update our state and notify toplevel singleton of completed state 17 | this.finished = true; 18 | if(this.transaction) 19 | this.transaction.becomeGlobalTransaction(); 20 | } 21 | } 22 | 23 | module.exports = Trace; 24 | -------------------------------------------------------------------------------- /lib/modules/postreSql.js: -------------------------------------------------------------------------------- 1 | const nodeReactionAgent = require("../Agent"); 2 | const pg = require("pg"); 3 | const library = "pg"; 4 | 5 | let queryOriginal = pg.Client.prototype.query; 6 | 7 | pg.Client.prototype.query = function(sql) { 8 | if (arguments.length > 0) { 9 | let queryType = arguments[0].substr(0,arguments[0].indexOf(' ')); 10 | // console.log(`Node Reaction Agent - ${library} - ${queryType}`); 11 | let trace = nodeReactionAgent.createTrace(library, queryType); 12 | const index = arguments.length - 1; 13 | const cb = arguments[index]; 14 | 15 | if (typeof cb === "function") { 16 | arguments[index] = function() { 17 | trace.end(); 18 | return cb.apply(this, arguments); 19 | }; 20 | } 21 | } 22 | return queryOriginal.apply(this, arguments); 23 | }; 24 | 25 | module.exports = pg; 26 | -------------------------------------------------------------------------------- /lib/DataExporter.js: -------------------------------------------------------------------------------- 1 | const Logger = require("./logger.js"); 2 | const fetch = require("node-fetch"); 3 | class DataExporter { 4 | constructor(sendTransactions) { 5 | this.setUrl(); 6 | } 7 | 8 | setUrl(url) { 9 | this.transactionsUrl = (url !== null) 10 | ? url 11 | : `http://www.nodereaction.com/api/agent/data/save`; 12 | } 13 | 14 | sendTransactionsToServer(data) { 15 | Logger.logTransactions(data); 16 | fetch(this.transactionsUrl, { 17 | method: "POST", 18 | body: JSON.stringify(data), 19 | headers: { "Content-Type": "application/json" } 20 | }) 21 | .then(data => { 22 | Logger.logDataSent(data); 23 | }) //fix this 24 | .catch(err => Logger.logErrorSendingData(err)); 25 | } 26 | } 27 | 28 | let dataExporter = new DataExporter(); 29 | 30 | module.exports = dataExporter; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodereactionagent", 3 | "version": "0.3.4", 4 | "description": "NodeReaction Agent is node application performance monitoring tool", 5 | "main": "NodeReaction.js", 6 | "scripts": { 7 | "start": "node NodeReaction.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/retrowavelabs/nodereactionagent.git" 12 | }, 13 | "keywords": [ 14 | "nodereaction", 15 | "analysis", 16 | "apm", 17 | "async", 18 | "deploy", 19 | "deploying", 20 | "deployment", 21 | "devops", 22 | "log", 23 | "logging", 24 | "monitor", 25 | "monitoring", 26 | "performance", 27 | "ops", 28 | "release" 29 | ], 30 | "author": "Retro Wave Labs", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/retrowavelabs/nodereactionagent/issues" 34 | }, 35 | "homepage": "https://github.com/retrowavelabs/nodereactionagent#readme", 36 | "dependencies": { 37 | "node-fetch": "^2.1.1", 38 | "uuid": "^3.2.1" 39 | }, 40 | "devDependencies": {} 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 retro wave labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | #Package-lock.json 9 | package-lock.json 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | -------------------------------------------------------------------------------- /lib/modules/ioredis.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const nodeReactionAgent = require("../Agent"); 3 | const library = "ioredis"; 4 | 5 | let initPromise = ioredis.Command.prototype.initPromise; 6 | 7 | ioredis.Command.prototype.initPromise = function() { 8 | if (arguments.length > 0) { 9 | let trace = nodeReactionAgent.createTrace(library, "InitPromise"); 10 | 11 | const command = this; 12 | var cb = this.callback; 13 | 14 | if (typeof cb === "function") { 15 | this.callback = function() { 16 | trace.end(); 17 | return cb.apply(this, arguments); 18 | }; 19 | } 20 | } 21 | 22 | return initPromise.apply(this, arguments); 23 | }; 24 | 25 | let sendCommand = ioredis.prototype.sendcommand; 26 | 27 | ioredis.prototype.sendcommand = function() { 28 | if (arguments.length > 0) { 29 | let trace = nodeReactionAgent.createTrace(library, "SendCommand"); 30 | 31 | const index = arguments.length - 1; 32 | const cb = arguments[index]; 33 | 34 | if (typeof cb === "function") { 35 | arguments[index] = function() { 36 | trace.end(); 37 | return cb.apply(this, arguments); 38 | }; 39 | } 40 | } 41 | 42 | return sendCommand.apply(this, arguments); 43 | }; 44 | 45 | module.exports = ioredis; 46 | -------------------------------------------------------------------------------- /lib/modules/redis.js: -------------------------------------------------------------------------------- 1 | const redis = require("redis"); 2 | const nodeReactionAgent = require("../Agent"); 3 | const library = "redis"; 4 | 5 | let internalSendCommandOriginal = 6 | redis.RedisClient.prototype.internal_send_command; 7 | 8 | redis.RedisClient.prototype.internal_send_command = function() { 9 | let trace = nodeReactionAgent.createTrace(library, "InternalSendCommand"); 10 | const index = arguments[0]; 11 | const cbArr = []; 12 | 13 | for (key in index) { 14 | cbArr.push(index[key]); 15 | } 16 | 17 | const cb = cbArr[3]; 18 | 19 | cbArr[3] = function() { 20 | trace.end(); 21 | return cb.apply(this, arguments); 22 | }; 23 | 24 | return internalSendCommandOriginal.apply(this, arguments); 25 | }; 26 | 27 | // Send Command... 28 | let sendCommandOriginal = redis.RedisClient.prototype.send_command; 29 | 30 | redis.RedisClient.prototype.send_command = function() { 31 | let trace = nodeReactionAgent.createTrace(library, "SendCommand"); 32 | const index = arguments[0]; 33 | const cbArr = []; 34 | 35 | for (key in index) { 36 | cbArr.push(index[key]); 37 | } 38 | 39 | const cb = cbArr[3]; 40 | 41 | cbArr[3] = function() { 42 | trace.end(); 43 | return cb.apply(this, arguments); 44 | }; 45 | 46 | return sendCommandOriginal.apply(this, arguments); 47 | }; 48 | 49 | module.exports = redis; 50 | -------------------------------------------------------------------------------- /lib/modules/fs.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const nodeReactionAgent = require("../Agent"); 3 | const library = "fs"; 4 | 5 | let fsFunc = ['access', 'exists', 'readFile', 'close', 'open', 'read', 'write', 'rename', 'truncate' 6 | , 'ftruncate', 'rmdir', 'fsync', 'mkdir', 'readdir', 'fstat', 'lstat', 'stat', 'readlink' 7 | , 'symlink', 'link', 'unlink', 'fchmod', 'lchmod', 'chmod', 'lchown', 'fchown', 'chown' 8 | , 'utimes', 'futimes', 'writeFile', 'appendFile', 'realpath', 'mkdtemp', 'copyFile'] 9 | 10 | for (let key in fs) { 11 | //find the method that's executing; 12 | for (let i = 0; i < fsFunc.length; i++) { 13 | if (key === fsFunc[i]) { 14 | let originalFunction = fs[fsFunc[i]]; 15 | fs[fsFunc[i]] = function () { 16 | if (arguments.length > 0) { 17 | //set trace; 18 | // console.log("Node Reaction Agent - FS - ", key); 19 | let trace = nodeReactionAgent.createTrace(library, key); 20 | const index = arguments.length - 1; 21 | const cb = arguments[index]; 22 | if (typeof cb === "function") { 23 | arguments[index] = function () { 24 | //end trace in the callback; 25 | if(trace) trace.end() 26 | return cb.apply(this, arguments); 27 | } 28 | } 29 | } 30 | return originalFunction.apply(this, arguments); 31 | } 32 | } 33 | } 34 | } 35 | 36 | module.exports = fs -------------------------------------------------------------------------------- /lib/RequestValidator.js: -------------------------------------------------------------------------------- 1 | const Logger = require('./logger.js'); 2 | const fileExtensions = { 3 | ".jpg": ".jpg", 4 | ".jpeg": ".jpeg", 5 | ".bmp": ".bmp", 6 | ".gif": ".gif", 7 | ".png": ".png", 8 | ".ico": ".ico", 9 | ".pdf": ".pdf", 10 | ".css": ".css", 11 | ".html": ".html", 12 | ".js": ".js", 13 | ".zip": ".zip", 14 | ".pdf": ".pdf", 15 | ".doc": ".doc", 16 | ".docx": ".docx", 17 | ".xls": ".xls", 18 | ".xlsx": ".xlsx", 19 | ".ppt": ".ppt", 20 | ".pptx": ".pptx", 21 | }; 22 | 23 | class RequestValidator { 24 | // filter incoming request and eliminate ones that are not async 25 | // example: file request 26 | isValidRequest(req) { 27 | let validRequest = true; 28 | let url = req.url.toLowerCase(); 29 | let method = req.method.toLowerCase(); 30 | if (method === "get") { 31 | if (this.isFileRequest(url)) validRequest = false; 32 | if (this.isRootPage(url)) validRequest = false; 33 | if (!validRequest) { 34 | Logger.logInvalidRequest(url, method) 35 | } 36 | } 37 | return validRequest; 38 | } 39 | 40 | isRootPage(url) { 41 | let rootPage = false; 42 | if (url === "/") { 43 | rootPage = true; 44 | } 45 | return rootPage; 46 | } 47 | 48 | isFileRequest(url) { 49 | let fileRequest = false; 50 | let fileExt = url.substr(url.lastIndexOf(".")); 51 | if (fileExt === -1) return fileRequest; 52 | if (fileExtensions[fileExt] !== undefined) fileRequest = true; 53 | return fileRequest; 54 | } 55 | } 56 | // create a singleton 57 | const requestValidator = new RequestValidator(); 58 | 59 | module.exports = requestValidator; 60 | -------------------------------------------------------------------------------- /lib/modules/mySql.js: -------------------------------------------------------------------------------- 1 | const nodeReactionAgent = require("../Agent"); 2 | const mysql = require("mysql"); 3 | const library = "mysql"; 4 | 5 | let mysqlFunc = ['createConnection'] 6 | 7 | // ['createPool', 8 | // 'createPoolCluster', 9 | // 'createQuery'] 10 | 11 | //identify the mysql connection 12 | for (let key in mysql) { 13 | for (let i = 0; i < mysqlFunc.length; i++) { 14 | //select the match 15 | if (key === mysqlFunc[i]) { 16 | //create a copy of original connection 17 | let orig = mysql[mysqlFunc[i]]; 18 | mysql[mysqlFunc[i]] = function () { 19 | //create trace for connection 20 | let conn_trace = nodeReactionAgent.createTrace(library, key); 21 | //apply overwitten to original connection 22 | let conn = orig.apply(this, arguments); 23 | //create copy of original query 24 | let q_orig = conn.query; 25 | conn.query = function () { 26 | //create trace for query 27 | let query_trace = nodeReactionAgent.createTrace(library, "query"); 28 | let index = arguments.length - 1; 29 | let qcb = arguments[index] 30 | if (typeof qcb === 'function') { 31 | arguments[index] = function () { 32 | //end trace for query 33 | if (query_trace) query_trace.end(); 34 | //end trace for connection assoicated with the query 35 | if (conn_trace) conn_trace.end(); 36 | //apply overwritten to original query callback 37 | return qcb.apply(this, arguments) 38 | } 39 | 40 | } 41 | //apply overwritten to original query 42 | return q_orig.apply(this, arguments); 43 | }; 44 | //return connection and attach it to the query 45 | return conn; 46 | } 47 | } 48 | } 49 | } 50 | 51 | module.exports = mysql; 52 | -------------------------------------------------------------------------------- /lib/Transaction.js: -------------------------------------------------------------------------------- 1 | const uuidv4 = require("uuid/v4"); 2 | const Trace = require("./Trace"); 3 | const TraceTimer = require("./TraceTimer"); 4 | 5 | class Transaction { 6 | constructor(nodeReactionAgent, request) { 7 | this.uuid = uuidv4(); 8 | this.nodeReactionAgent = nodeReactionAgent; 9 | this.request = request; 10 | this.finshed = false; 11 | this.traces = []; 12 | // pull these props out of request obj now (it is deleted before being sent to server) 13 | this.route = this.request.url; 14 | this.method = this.request.method; 15 | this.userAgent = ''; 16 | this.rawHeaders = ''; 17 | this.cookies = ''; 18 | this.userAgent = this.request.headers['user-agent']; 19 | this.rawHeaders = this.request.rawHeaders; 20 | this.cookies = this.request.headers.cookie; 21 | // 22 | let xForwardedFor = this.request.headers['x-forwarded-for']; 23 | this.remoteAddress = (xForwardedFor)? xForwardedFor.split(',')[0] : this.request.connection.remoteAddress; 24 | // stores timer info 25 | this.traceTimer = new TraceTimer(this.uuid); 26 | this.traceTimer.start(); 27 | } 28 | 29 | createTrace(library, type) { 30 | let trace = new Trace(this, library, type); 31 | this.traces.push(trace); 32 | return trace; 33 | } 34 | 35 | //restore ourselves in singleton state. Called by Trace so state is changed before user cb 36 | becomeGlobalTransaction() { 37 | if(this.nodeReactionAgent) 38 | this.nodeReactionAgent.restoreCurrentTransaction(this); 39 | } 40 | 41 | //set flag so singleton can flush us out and get GC'd 42 | endTransaction() { 43 | this.finished = true; 44 | this.traceTimer.end(); 45 | } 46 | 47 | //remove circular refrences 48 | prepareForExport() { 49 | delete this.nodeReactionAgent; 50 | delete this.request; 51 | this.traces.forEach(trace => delete trace.transaction); 52 | } 53 | } 54 | 55 | module.exports = Transaction; 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Reaction Agent 2 | 3 | 4 | ## Overview 5 | 6 | NodeReactionAgent is an open source performance monitoring framework, included in application code to analyzes http requests and asynchronous operations while working in conjunction with a cloud service NodeReaction.com to store analyze and display information the agent gathers. 7 | 8 | NodeReaction.com provides a detailed performance breakdown of a developer’s Node.js web applications and better understand where bottlenecks exist in their application. 9 | git 10 | ## Installation 11 | Using npm: 12 | ```shell 13 | $ npm install nodereactionagent 14 | ``` 15 | 16 | ## Agent Configuration Options 17 | Default confguration (place on line 1 of your server.js): 18 | ```shell 19 | const NRA = require('nodereactionagent').setApiToken('TokenFromNodeReaction.com'); 20 | ``` 21 | ### Set Agent URL 22 | The agent is set up to post to our cloud servers, however, the agent can post to a URL of your choosing. 23 | ```shell 24 | NRA.setAgentUrl('localhost:3000/YourEndPoint'); 25 | ``` 26 | ### Post Transactions to Server 27 | The agent can be prevented from posting any information to servers and can be used as a screen logger or to log information to a file. 28 | ```shell 29 | NRA.sendTransactionsToServer(false); // defaults: true; 30 | ``` 31 | 32 | ### Save logs to disk 33 | The agent can optionally save logs to disk with the following command. 34 | ```shell 35 | NRA.saveLogToDisk(true, pathToSave); // defaults: false and project root folder 36 | ``` 37 | 38 | ### Log to Screen 39 | ```shell 40 | NRA.logToScreen(true); // defaults: true 41 | ``` 42 | 43 | ## Overriding your own Async Functions 44 | You can use Node Reaction Agent in your any of your own async functions that occur during and HTTP request. In order to develop timing data just add the following lines into your application. 45 | 46 | ```shell 47 | const nodeReactionAgent = require("../Agent.js"); 48 | 49 | // add this line when you want the trace to starts 50 | let trace = nodeReactionAgent.createTrace('ModuleName', 'FunctionName'); 51 | 52 | 53 | // add this line when async function completes; 54 | trace.end(); 55 | ``` 56 | 57 | ## Adding a module 58 | You can write your own modules for Node Reaction to further extend it's utility. 59 | 60 | 61 | ## Contributions 62 | We welcome contributions. Please submit issues [nodereaction@gmail.com](mailto:nodereaction@gmail.com) 63 | 64 | 65 | 66 | ## Authors 67 | - [Chris Jeon](https://github.com/blackink000) 68 | - [Eric Fileti](https://github.com/ericfileti) 69 | - [James Edwards](https://github.com/JamesThomasEdwards) 70 | - [Kunal Patel](https://github.com/kunalpatel73) 71 | - [Michael Dalton](https://github.com/modalton) 72 | -------------------------------------------------------------------------------- /lib/Logger.js: -------------------------------------------------------------------------------- 1 | class Logger { 2 | constructor() { 3 | this.logToDisk = false; 4 | this.showScreenLog = true; 5 | this.logFilePath = ""; 6 | } 7 | 8 | log(msg) { 9 | if (this.showScreenLog) { 10 | console.log(msg); 11 | } 12 | // if (this.logToDisk){} 13 | // pass the msg on to function that saves file 14 | // append log file 15 | // save up a bunch of logs or log periodically? 16 | // jump to root directory 17 | // const packageJsonPath = __dirname + "/../../../"; 18 | // let packageJsonExists = fs.statSync(packageJsonPath); 19 | // if (packageJsonExists) {} 20 | } 21 | 22 | error(msg) { 23 | if (this.logToScreen) { 24 | console.error(msg); 25 | } 26 | } 27 | 28 | saveLogToDisk(logToDisk, path){ 29 | this.logToDisk = logToDisk; 30 | } 31 | 32 | logToScreen(showScreenLog){ 33 | this.showScreenLog = showScreenLog; 34 | } 35 | 36 | logStartup() { 37 | const d = new Date(Date.now()) 38 | .toISOString() 39 | .slice(0, 23) 40 | .replace("T", " "); 41 | const str = ` 42 | =========== Node Reaction Agent 0.3.4 ===========\n 43 | time data sent: ${d} 44 | `; 45 | this.log(str); 46 | } 47 | 48 | logNoPackageJsonDependencies() { 49 | this.log(`Node Reaction Agent - Library Loader - no modules loaded. no dependencies found in packageJson`); 50 | } 51 | 52 | logModuleLoaded(module) { 53 | this.log(`Node Reaction Agent - Library Loader - module loaded - ${module}`); 54 | } 55 | 56 | logInvalidRequest(url, method) { 57 | this.log(`Node Reaction Agent - RequestValidator - request ignored - method: ${method} url: ${url}`); 58 | } 59 | 60 | logTransactions(data) { 61 | const d = new Date(Date.now()).toISOString().slice(0, 23).replace("T", " "); 62 | let str = ` 63 | =========== Agent sent data to server ===========\n 64 | time data sent: ${d} 65 | transactions sent: ${JSON.stringify(data.transactions.length)} 66 | \n\n`; 67 | data.transactions.forEach(transaction => { 68 | str += `transaction: ${transaction.method} - ${transaction.route} 69 | - ${transaction.traceTimer.duration}ms - ${ 70 | transaction.traceTimer.startTimestamp 71 | }\n`; 72 | // trace info 73 | transaction.traces.forEach(trace => { 74 | str += `\ttrace: ${trace.library} - ${trace.type} - 75 | ${trace.traceTimer.duration}ms - ${ 76 | trace.traceTimer.startTimestamp 77 | } \n`; 78 | //str += `\ttrace:\n\t\t ${JSON.stringify(trace)} \n`; 79 | }); 80 | str += `=================================================\n`; 81 | }); 82 | this.log(str); 83 | } 84 | 85 | logDataSent(data){ 86 | const str = `Node Reaction Agent - DataExporter - data sent`; 87 | this.log(str); 88 | } 89 | 90 | logErrorSendingData(err) { 91 | const str = `Node Reaction Agent - DataExporter - error - ${err}`; 92 | this.log(str); 93 | } 94 | 95 | } 96 | let logger = new Logger(); 97 | 98 | module.exports = logger; 99 | -------------------------------------------------------------------------------- /lib/LibraryLoader.js: -------------------------------------------------------------------------------- 1 | // class reads packageJson file 2 | // if package json can't be found it tells the user it will only load http 3 | // only modules found in the package json are loaded 4 | const Logger = require('./logger.js'); 5 | const fs = require("fs"); 6 | class LibraryLoader { 7 | // possibly add appId, userId, token for cloud hosted configuration 8 | constructor() { 9 | this.initializeLibraries(); 10 | this.checkPackageJson(); 11 | } 12 | 13 | initializeLibraries() { 14 | this.libraries = [ 15 | { 16 | name: "fs", 17 | moduleName: "fs", 18 | path: "./modules/fs", 19 | module: null, 20 | load: true, 21 | loaded: false 22 | }, 23 | { 24 | name: "http", 25 | moduleName: "http", 26 | path: "./modules/http", 27 | module: null, 28 | load: true, 29 | loaded: false 30 | }, 31 | { 32 | name: "mongo", 33 | moduleName: "mongodb-core", 34 | path: "./modules/mongo", 35 | module: null, 36 | load: false, 37 | loaded: false 38 | }, 39 | { 40 | name: "mongoose", 41 | moduleName: "mongoose", 42 | path: "./modules/mongo", 43 | module: null, 44 | load: false, 45 | loaded: false 46 | }, 47 | { 48 | name: "mySql", 49 | moduleName: "mysql", 50 | path: "./modules/mySql", 51 | module: null, 52 | load: false, 53 | loaded: false 54 | }, 55 | { 56 | name: "postreSql", 57 | moduleName: "pg", 58 | path: "./modules/postreSql", 59 | module: null, 60 | load: false, 61 | loaded: false 62 | }, 63 | { 64 | name: "redis", 65 | moduleName: "redis", 66 | path: "./modules/redis", 67 | module: null, 68 | load: false, 69 | loaded: false 70 | } 71 | ]; 72 | } 73 | 74 | checkPackageJson() { 75 | const packageJsonPath = __dirname + "/../../../package.json"; 76 | let packageJsonExists = fs.statSync(packageJsonPath); 77 | if (packageJsonExists) { 78 | const packageJson = require(packageJsonPath); 79 | const moduleNames = Object.keys(packageJson.dependencies); 80 | if (moduleNames.length) { 81 | const uniqueModuleNames = moduleNames.filter((value, index, self) => { 82 | return self.indexOf(value) === index; 83 | }); 84 | return this.loadModules(uniqueModuleNames); 85 | } 86 | } else { 87 | Logger.logNoPackageJsonDependencies(); 88 | } 89 | } 90 | 91 | loadModules(moduleNames) { 92 | Logger.log( 93 | `Node Reaction Agent - Library Loader - check package.json dependencies:\n\t ${moduleNames}\n` 94 | ); 95 | moduleNames.forEach(moduleName => { 96 | for (let i = 0; i < this.libraries.length; i++) { 97 | if (moduleName === this.libraries[i].moduleName) { 98 | this.libraries[i].load = true; 99 | break; 100 | } 101 | } 102 | }); 103 | this.libraries.forEach((library, i) => { 104 | if (library.load) { 105 | library.module = require(library.path); 106 | library.loaded = true; 107 | Logger.logModuleLoaded(library.name) 108 | } 109 | }); 110 | return this.libraries; 111 | } 112 | } 113 | 114 | let libraryLoader = new LibraryLoader(); 115 | 116 | module.exports = libraryLoader; 117 | 118 | -------------------------------------------------------------------------------- /lib/Agent.js: -------------------------------------------------------------------------------- 1 | const Transaction = require("./Transaction"); 2 | const DataExporter = require("./DataExporter"); 3 | const Logger = require("./logger.js"); 4 | 5 | class NodeReactionAgent { 6 | constructor() { 7 | this.currentTransaction = null; 8 | this.transactions = []; 9 | this.activeTransactionCount = 0; 10 | this.transactionFlushInterval; 11 | this.transactionFlushIntervalTime = 10000; 12 | this.minimumTransactionsToSend = 10; 13 | this.maximumTransactionsToQueue = 100; 14 | this.sendToServer = true; 15 | this.credentials = {}; 16 | this.resetTransactionFlushInterval(); 17 | } 18 | 19 | // store api crendentials for calls to server 20 | setApiToken(apiToken) { 21 | this.credentials.apiToken = apiToken ? apiToken : "defaultApiToken"; 22 | return this; 23 | } 24 | 25 | // set data exporter prefs 26 | setAgentUrl(url) { 27 | DataExporter.setUrl(url); 28 | } 29 | 30 | // set data exporter prefs 31 | sendTransactionsToServer(sendToServer) { 32 | this.sendToServer = sendToServer; 33 | } 34 | 35 | // set logger file saving prefs 36 | saveLogToDisk(saveToDisk, path) { 37 | Logger.saveLogToDisk(saveToDisk, path); 38 | } 39 | 40 | // set logger screen logging 41 | logToScreen(showScreenLog) { 42 | Logger.logToScreen(showScreenLog); 43 | } 44 | 45 | // create a new transaction and check to see if it's time to clear out some 46 | createTransaction(req) { 47 | let transaction = new Transaction(this, req); 48 | this.currentTransaction = transaction; 49 | this.transactions.push(transaction); 50 | this.activeTransactionCount += 1; 51 | this.checkTransactionsQueue(); 52 | return transaction; 53 | } 54 | 55 | // have our current transaction return a new internal trace reference 56 | createTrace(library, type) { 57 | // prevents traces from being made outside of an existing transaction which causes agent failure 58 | if (this.currentTransaction !== null) { 59 | return this.currentTransaction.createTrace(library, type); 60 | } 61 | } 62 | 63 | // will receive transaction from end of trace and restore 64 | restoreCurrentTransaction(transaction) { 65 | this.currentTransaction = transaction; 66 | } 67 | 68 | // forces a flush of completed transactions if there are more than 100 in the queue 69 | checkTransactionsQueue() { 70 | if (this.activeTransactionCount >= this.maximumTransactionsToQueue) { 71 | this.flushTransactions(); 72 | } 73 | } 74 | 75 | // resets the interval to clear the transaction queue 76 | resetTransactionFlushInterval() { 77 | if (this.transactionFlushInterval) 78 | clearInterval(this.transactionFlushInterval); 79 | this.transactionFlushInterval = setInterval( 80 | () => this.flushTransactions(true), 81 | this.transactionFlushIntervalTime 82 | ); 83 | } 84 | 85 | // prepare transactions for export and try to send them 86 | flushTransactions(hardFlush) { 87 | let completedTransactions = this.transactions.filter(t => t.finished); 88 | let activeTransactions = this.transactions.filter(t => !t.finished); 89 | if ( 90 | completedTransactions.length >= this.minimumTransactionsToSend || 91 | hardFlush 92 | ) { 93 | if (completedTransactions.length > 0) { 94 | this.transactions = activeTransactions; 95 | // remove circular refrences 96 | completedTransactions.forEach(t => t.prepareForExport()); 97 | // add appId, userId and token in the future 98 | const data = { 99 | credentials: this.credentials, 100 | transactions: completedTransactions 101 | }; 102 | if (this.sendToServer) { 103 | DataExporter.sendTransactionsToServer(data); 104 | } 105 | } 106 | } 107 | this.resetTransactionFlushInterval(); 108 | } 109 | } 110 | 111 | // create a singleton and export it 112 | let nodeReactionAgent = new NodeReactionAgent(); 113 | 114 | module.exports = nodeReactionAgent; 115 | -------------------------------------------------------------------------------- /lib/modules/mongo.js: -------------------------------------------------------------------------------- 1 | const NodeReactionAgent = require("../Agent"); 2 | const mongo = require("mongodb-core"); 3 | const library = "MongoDB"; 4 | 5 | //mongo insert 'Server' callback is the last parameter 6 | let insertOriginal = mongo.Server.prototype.insert; 7 | 8 | mongo.Server.prototype.insert = function() { 9 | // console.log("Node Reaction Agent - MongoDB - Insert"); 10 | if (arguments.length > 0) { 11 | let trace = NodeReactionAgent.createTrace(library, "Insert"); 12 | 13 | const index = arguments.length - 1; 14 | const cb = arguments[index]; 15 | 16 | if (typeof cb === "function") { 17 | arguments[index] = function() { 18 | if (trace) trace.end(); 19 | return cb.apply(this, arguments); 20 | }; 21 | } 22 | } 23 | 24 | return insertOriginal.apply(this, arguments); 25 | }; 26 | //mongo _find 'Cursor' callback is the first parameter 27 | let _findOriginal = mongo.Cursor.prototype._find; 28 | 29 | mongo.Cursor.prototype._find = function() { 30 | // console.log("Node Reaction Agent - MongoDB - Find"); 31 | if (arguments.length > 0) { 32 | let trace = NodeReactionAgent.createTrace(library, "Find"); 33 | 34 | const cb = arguments[0]; 35 | 36 | if (typeof cb === "function") { 37 | arguments[0] = function() { 38 | if (trace) trace.end(); 39 | return cb.apply(this, arguments); 40 | }; 41 | } 42 | } 43 | 44 | return _findOriginal.apply(this, arguments); 45 | }; 46 | //mongo update 'Server' callback is the last parameter 47 | let updateOriginal = mongo.Server.prototype.update; 48 | 49 | mongo.Server.prototype.update = function() { 50 | // console.log("Node Reaction Agent - MongoDB - Update"); 51 | if (arguments.length > 0) { 52 | let trace = NodeReactionAgent.createTrace(library, "Update"); 53 | 54 | const index = arguments.length - 1; 55 | const cb = arguments[index]; 56 | if (typeof cb === "function") { 57 | arguments[index] = function() { 58 | if (trace) trace.end(); 59 | return cb.apply(this, arguments); 60 | }; 61 | } 62 | } 63 | return updateOriginal.apply(this, arguments); 64 | }; 65 | //mongo remove 'Server' callback is the last parameter 66 | let removeOriginal = mongo.Server.prototype.remove; 67 | 68 | mongo.Server.prototype.remove = function() { 69 | // console.log("Node Reaction Agent - MongoDB - Remove"); 70 | if (arguments.length > 0) { 71 | let trace = NodeReactionAgent.createTrace(library, "Remove"); 72 | 73 | const index = arguments.length - 1; 74 | const cb = arguments[index]; 75 | if (typeof cb === "function") { 76 | arguments[index] = function() { 77 | if (trace) trace.end(); 78 | return cb.apply(this, arguments); 79 | }; 80 | } 81 | } 82 | return removeOriginal.apply(this, arguments); 83 | }; 84 | 85 | //mongo auth 'Server' callback is the last parameter 86 | let authOriginal = mongo.Server.prototype.auth; 87 | 88 | mongo.Server.prototype.auth = function() { 89 | // console.log("Node Reaction Agent - MongoDB - Auth"); 90 | if (arguments.length > 0) { 91 | let trace = NodeReactionAgent.createTrace(library, "Auth"); 92 | 93 | const index = arguments.length - 1; 94 | const cb = arguments[index]; 95 | if (typeof cb === "function") { 96 | arguments[index] = function() { 97 | if (trace) trace.end(); 98 | return cb.apply(this, arguments); 99 | }; 100 | } 101 | } 102 | return authOriginal.apply(this, arguments); 103 | }; 104 | //mongo _getmore 'Cursor' callback is the first parameter 105 | let getMoreOriginal = mongo.Cursor.prototype._getmore; 106 | 107 | mongo.Cursor.prototype._getmore = function() { 108 | // console.log("Node Reaction Agent - MongoDB - Get More"); 109 | if (arguments.length > 0) { 110 | let trace = NodeReactionAgent.createTrace(library, "GetMore"); 111 | 112 | const cb = arguments[0]; 113 | 114 | if (typeof cb === "function") { 115 | arguments[0] = function() { 116 | if (trace) trace.end(); 117 | return cb.apply(this, arguments); 118 | }; 119 | } 120 | } 121 | 122 | return getMoreOriginal.apply(this, arguments); 123 | }; 124 | 125 | module.exports = mongo; 126 | --------------------------------------------------------------------------------