├── mocha.opts ├── .babelrc ├── docs ├── profiler-log.jpg └── profiler-stats.jpg ├── .npmignore ├── .editorconfig ├── .istanbul.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── CHANGELOG.md ├── package.json ├── example └── app.js ├── README.md ├── src └── index.js └── test └── loggerStats.test.js /mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive test/ 2 | --compilers js:babel-core/register -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "add-module-exports" ], 3 | "presets": [ "es2015" ] 4 | } 5 | -------------------------------------------------------------------------------- /docs/profiler-log.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-profiler/HEAD/docs/profiler-log.jpg -------------------------------------------------------------------------------- /docs/profiler-stats.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-profiler/HEAD/docs/profiler-stats.jpg -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .jshintrc 3 | .travis.yml 4 | .istanbul.yml 5 | .babelrc 6 | .idea/ 7 | src/ 8 | test/ 9 | useful/ 10 | !lib/ 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: ./src/ 4 | excludes: 5 | - lib/ 6 | include-all-sources: true 7 | reporting: 8 | print: summary 9 | reports: 10 | - html 11 | - text 12 | - lcov 13 | watermarks: 14 | statements: [50, 80] 15 | lines: [50, 80] 16 | functions: [50, 80] 17 | branches: [50, 80] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | 32 | # The compiled/babelified modules 33 | lib/ 34 | 35 | # editor files 36 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - '6' 5 | - '4' 6 | addons: 7 | code_climate: 8 | repo_token: a877074555c5f1836623cdaf5cf48c350ed17fcee5c8e3dc3ad53993c719e215 9 | notifications: 10 | slack: 11 | rooms: 12 | secure: jBc9A9zXVCw6UgDLEUKRCzjp5br0qxEp1aNJDUA8/PnOa/H2lcv/tM4w1Zszrw/Z3EvF85/HOV2y949SZ/7x9OlYJS2pbGxzqlzwtGncXHFGwIyktr+EyGXsb8KrdYUkNE7P3xZCH8L6aNDMoXbuvhtE4CskyaUgs/veg4jJq+rD3O1ZY0bqLkDTFoN09s0ZaT8po4Ou9nQZLFejWezxZeWmsJyo8ODxhYdTGgTuhABvYqmAKy0zycVrkvQ9LP/x0B3Qz/4jJm2RUEbmBc7UaNN/EWpn6Mn3YrMkfPmJ/W6AdzF+S8F40Uf2ofmwFb0awjoe6dmPGVS8JgIWP+/Tn+QYP81EUrPY6Kp7I8pYnuZt6ZHRDq00qXYKGQxLX9PBypueermIF1m9NXLurbWJHektykOW+kt9RqFkcDgTgpIt1VcSGeKmoYxzUV80IlREK1u79vBB6huL8X6USJRW6Kf3yQjiajG9t76wMGieVxtb1NkUxb07hOGyLdQ5/rj+N8Xk6tmUJJsroAShBawlP1cKkHnCgrzdt25OvsxKOOQRLmLEMm9kwWubFSdJw2Q0z4rUpjBt8O2RuxfZ0lbV8dJvci92jyUsp7kFfBqTKk5EA6aN6xKLVuFK5FYwz0L2luaxFvjjaTi1a/o+fuW7EujY7QEb2uYHWG7WvDxpGT4= 13 | email: false 14 | before_script: 15 | - npm install -g codeclimate-test-reporter 16 | after_script: 17 | - codeclimate-test-reporter < coverage/lcov.info 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Feathers 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 | 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v0.1.5](https://github.com/feathersjs/feathers-profiler/tree/v0.1.5) (2017-05-02) 4 | [Full Changelog](https://github.com/feathersjs/feathers-profiler/compare/v0.1.4...v0.1.5) 5 | 6 | **Closed issues:** 7 | 8 | - Support Rethink changefeed watching [\#6](https://github.com/feathersjs/feathers-profiler/issues/6) 9 | - Add documentation [\#2](https://github.com/feathersjs/feathers-profiler/issues/2) 10 | 11 | **Merged pull requests:** 12 | 13 | - Ignores RethinkDB changefeeds issue \#6 [\#7](https://github.com/feathersjs/feathers-profiler/pull/7) ([eddyystop](https://github.com/eddyystop)) 14 | - Update semistandard to the latest version 🚀 [\#5](https://github.com/feathersjs/feathers-profiler/pull/5) ([greenkeeper[bot]](https://github.com/integration/greenkeeper)) 15 | - Add documentation to the readme [\#4](https://github.com/feathersjs/feathers-profiler/pull/4) ([daffl](https://github.com/daffl)) 16 | - Update feathers-hooks to the latest version 🚀 [\#3](https://github.com/feathersjs/feathers-profiler/pull/3) ([greenkeeper[bot]](https://github.com/integration/greenkeeper)) 17 | - Update dependencies to enable Greenkeeper 🌴 [\#1](https://github.com/feathersjs/feathers-profiler/pull/1) ([greenkeeper[bot]](https://github.com/integration/greenkeeper)) 18 | 19 | ## [v0.1.4](https://github.com/feathersjs/feathers-profiler/tree/v0.1.4) (2016-12-01) 20 | [Full Changelog](https://github.com/feathersjs/feathers-profiler/compare/v0.1.3...v0.1.4) 21 | 22 | ## [v0.1.3](https://github.com/feathersjs/feathers-profiler/tree/v0.1.3) (2016-12-01) 23 | [Full Changelog](https://github.com/feathersjs/feathers-profiler/compare/v0.1.2...v0.1.3) 24 | 25 | ## [v0.1.2](https://github.com/feathersjs/feathers-profiler/tree/v0.1.2) (2016-12-01) 26 | 27 | 28 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-profiler", 3 | "description": "Log feathers service calls and gather profile information on them.", 4 | "version": "0.1.5", 5 | "homepage": "https://github.com/feathersjs/feathers-profiler", 6 | "main": "lib/", 7 | "keywords": [ 8 | "feathers", 9 | "feathers-plugin" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/feathersjs/feathers-profiler.git" 15 | }, 16 | "author": { 17 | "name": "Feathers contributors", 18 | "email": "hello@feathersjs.com", 19 | "url": "https://feathersjs.com" 20 | }, 21 | "contributors": [], 22 | "bugs": { 23 | "url": "https://github.com/feathersjs/feathers-feathers-profiler/issues" 24 | }, 25 | "engines": { 26 | "node": ">= 4.6.0" 27 | }, 28 | "scripts": { 29 | "prepublish": "npm run compile", 30 | "publish": "git push origin --tags && npm run changelog && git push origin", 31 | "release:patch": "npm version patch && npm publish", 32 | "release:minor": "npm version minor && npm publish", 33 | "release:major": "npm version major && npm publish", 34 | "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 35 | "compile": "rimraf lib/ && babel -d lib/ src/", 36 | "watch": "babel --watch -d lib/ src/", 37 | "lint": "semistandard src/**/*.js test/**/*.js --fix", 38 | "mocha": "mocha --opts mocha.opts", 39 | "coverage": "istanbul cover _mocha -- --opts mocha.opts", 40 | "test": "npm run compile && npm run lint && npm run coverage", 41 | "start": "npm run compile && node example/app" 42 | }, 43 | "semistandard": { 44 | "sourceType": "module", 45 | "env": [ 46 | "mocha" 47 | ] 48 | }, 49 | "directories": { 50 | "lib": "lib" 51 | }, 52 | "dependencies": { 53 | "debug": "^3.0.0" 54 | }, 55 | "devDependencies": { 56 | "babel-cli": "^6.18.0", 57 | "babel-core": "^6.18.0", 58 | "babel-plugin-add-module-exports": "^0.2.1", 59 | "babel-polyfill": "^6.26.0", 60 | "babel-preset-es2015": "^6.18.0", 61 | "chai": "^4.0.0", 62 | "feathers": "^2.0.2", 63 | "feathers-hooks": "^2.0.0", 64 | "istanbul": "^1.1.0-alpha.1", 65 | "mocha": "^3.1.2", 66 | "rimraf": "^2.5.4", 67 | "semistandard": "^11.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | 2 | const feathers = require('feathers'); 3 | const hooks = require('feathers-hooks'); 4 | const util = require('util'); 5 | 6 | const { profiler, getProfile, getPending } = require('../lib'); 7 | 8 | const app = feathers() 9 | .configure(hooks()) 10 | .configure(services) 11 | .configure(profiler({ stats: 'detail' })); 12 | 13 | function services () { 14 | this.use('/messages', { 15 | before: { 16 | all: () => {}, 17 | create: [ 18 | (hook, cb) => { 19 | if (hook.data.delay) { 20 | setTimeout(() => { 21 | cb(null, hook); 22 | }, 500); 23 | return; 24 | } 25 | 26 | cb(null, hook); 27 | }, 28 | hook => { 29 | if (hook.data.throwBefore) { 30 | throw new Error('..Before throw requested'); 31 | } 32 | } 33 | ] 34 | }, 35 | after: { 36 | create: [ 37 | hook => { 38 | if (hook.data.throwAfter) { 39 | throw new Error('..After throw requested'); 40 | } 41 | } 42 | ] 43 | }, 44 | find (params) { 45 | if (params.type !== 'paginated') { 46 | return new Promise(resolve => { 47 | setTimeout(() => { 48 | resolve([{ a: 1 }, { a: 2 }]); 49 | }, 200); 50 | }); 51 | } 52 | 53 | return Promise.resolve({ 54 | total: 3, 55 | data: [{ a: 1 }, { a: 2 }, { a: 3 }] 56 | }); 57 | }, 58 | create () { 59 | return new Promise(resolve => { 60 | setTimeout(() => { 61 | resolve(); 62 | }, 100); 63 | }); 64 | } 65 | }); 66 | } 67 | 68 | const service = app.service('messages'); 69 | 70 | Promise.all([ 71 | service.create({ value: 1 }, { query: {} }), 72 | service.create({ throwBefore: true }, { query: { throwBefore: true } }) // throw before 73 | .catch(() => {}), 74 | service.create({ value: 2, delay: true }, { query: {} }), // delay 75 | service.create({ value: 3 }, { query: {} }), 76 | service.create({ throwAfter: true }, { query: { throwAfter: true } }) // throw after 77 | .catch(() => {}), 78 | service.create({ value: 4 }, { query: {} }), 79 | 80 | service.find({ query: { name: 'John Doe' }, type: 'paginated' }), 81 | service.find({ query: { name: 'Jane Doe' } }) 82 | ]) 83 | .then(() => { 84 | console.log('\n\npending', getPending()); 85 | console.log(util.inspect(getProfile(), { depth: 5, colors: true })); 86 | }); 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-profiler 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/feathersjs/feathers-profiler.svg)](https://greenkeeper.io/) 4 | 5 | [![Build Status](https://travis-ci.org/feathersjs/feathers-profiler.png?branch=master)](https://travis-ci.org/feathersjs/feathers-profiler) 6 | [![Code Climate](https://codeclimate.com/github/feathersjs/feathers-profiler/badges/gpa.svg)](https://codeclimate.com/github/feathersjs/feathers-profiler) 7 | [![Test Coverage](https://codeclimate.com/github/feathersjs/feathers-profiler/badges/coverage.svg)](https://codeclimate.com/github/feathersjs/feathers-profiler/coverage) 8 | [![Dependency Status](https://img.shields.io/david/feathersjs/feathers-profiler.svg?style=flat-square)](https://david-dm.org/feathersjs/feathers-profiler) 9 | [![Download Status](https://img.shields.io/npm/dm/feathers-profiler.svg?style=flat-square)](https://www.npmjs.com/package/feathers-profiler) 10 | 11 | > Log service method calls and gather profile information on them. 12 | 13 | ## Installation 14 | 15 | ``` 16 | npm install feathers-profiler --save 17 | ``` 18 | 19 | ## Example 20 | 21 | ``` 22 | npm start 23 | ``` 24 | 25 | ## Documentation 26 | 27 | Service calls transported by websockets are not passed through Express middleware. 28 | `feathers-profiler` logs service calls from all transports 29 | and gathers performance information on them. 30 | 31 | ##### `import { profiler, getProfile, clearProfile, getPending, timestamp } from 'feathers-profiler';` 32 | 33 | ### `app.configure(profiler(options))` 34 | 35 | Start logging and/or profiling service calls. 36 | 37 | __Options:__ 38 | 39 | - logger 40 | - defaults to logging on `console.log`. 41 | - `null` disables logging. 42 | - `require('winston')` routes logs to the popular winston logger. 43 | - `{ log: payload => {} }` routes logs to your customized logger. 44 | - logMsg 45 | - default message is shown [below](#logs-service-calls). 46 | - `hook => {}` returns a custom string or object payload for the logger. 47 | `hook._log` contains log information; 48 | `hook.original` and `hook.error` contain error information. 49 | - stats 50 | - `null` or `'none'` profile information will not be gathered. 51 | - `total` gathers profile information by service and method only. The default. 52 | - `detail` gathers profile information by characteristics of the call. 53 | - statsDetail 54 | - default is shown [below](#gathers-profile-information-on-service-calls). 55 | - `hook => {}` returns a custom category for the call. 56 | 57 | 58 | ### `getProfile()` 59 | 60 | Returns profile information as an object. 61 | 62 | ### `clearProfile()` 63 | 64 | Re-initializes the profile information. 65 | The profile internal counts may not add up perfectly unless `getPending() === 0`. 66 | 67 | ### `getPending()` 68 | 69 | Returns the number of currently pending service calls. 70 | 71 | ### `timestamp()` 72 | 73 | Returns a timestamp suitable for logging to the console. 74 | 75 | ## Example 76 | 77 | ```js 78 | const feathers = require('feathers'); 79 | const rest = require('feathers-rest'); 80 | const sockets = require('feathers-socketio'); 81 | const hooks = require('feathers-hooks'); 82 | const bodyParser = require('body-parser'); 83 | const errorHandler = require('feathers-errors/handler'); 84 | 85 | const { profiler, getProfile, getPending } = require('feathers-profiler'); 86 | 87 | // Initialize the application 88 | const app = feathers() 89 | .configure(rest()) 90 | .configure(sockets()) 91 | .configure(hooks()) 92 | .use(bodyParser.json()) // Needed for parsing bodies (login) 93 | .use(bodyParser.urlencoded({ extended: true })) 94 | .use('users', { ...}) // services 95 | .use('messages', { ... }) 96 | .configure(profiler({ stats: 'detail' }) // must be configured after all services 97 | .use(errorHandler()); 98 | 99 | // ... once multiple service calls have been made 100 | console.log('pending', getPending()); 101 | console.log(require('util').inspect(getProfile(), { 102 | depth: 5, 103 | colors: true 104 | })); 105 | ``` 106 | 107 | ## Usage 108 | 109 | ### Logs service calls 110 | 111 | The log message may be customized. The default log message includes: 112 | 113 | - Service name, method and transport provider. 114 | - Elapsed time between the method being called and its completion. 115 | - Number of service calls pending when call was made. 116 | - Where service call failed and why. 117 | 118 | ![logs](./docs/profiler-log.jpg) 119 | 120 | ### Gathers profile information on service calls 121 | 122 | Profile information is: 123 | 124 | - Grouped by service and method. 125 | - Grouped by characteristics of the call. These may be customized. 126 | - Average pending count provides information on how busy the server was during these calls. 127 | - Average, min and max elapsed time provide information on how responsive the server is. 128 | - The number of returned items provides information on how large the `find` results were. 129 | 130 | ![stats](./docs/profiler-stats.jpg) 131 | 132 | ## License 133 | 134 | Copyright (c) 2016 135 | 136 | Licensed under the [MIT license](LICENSE). 137 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | const debug = require('debug')('feathers-profiler'); 3 | 4 | let app; 5 | let options; 6 | let pending = 0; 7 | let cache = {}; 8 | 9 | const cacheEntry = { 10 | // sum all calls 11 | calledCount: 0, // #times service called 12 | pendingTotal: 0, // total #pending service calls at start of these calls 13 | pendingAvg: 0, // average #pending service calls at start of a call 14 | // sum successful calls 15 | resolvedCount: 0, // #times service call completed successfully 16 | nanoTotal: 0, // total nano-secs (sec/1e9) spent on successful calls 17 | avgMs: 0, // average milli-sec per successful call 18 | nanoMin: Infinity, // shortest successful call 19 | nanoMax: -1, // longest successful call 20 | resultItemsCount: 0 // #items read in find and get calls 21 | }; 22 | 23 | export function profiler (options1 = {}) { 24 | options = Object.assign({}, { 25 | logger: { log: msg => console.log(msg) }, 26 | logMsg: defaultLogMsg, 27 | stats: 'total', 28 | statsDetail: hook => `id:${typeof hook.id}, params:${JSON.stringify(hook.params)}` 29 | }, 30 | options1); 31 | 32 | options.stats = options.stats || 'none'; 33 | 34 | if (!['detail', 'total', 'none'].includes(options.stats)) { 35 | throw new Error('stats option invalid. (profiler)'); 36 | } 37 | if (options.logger && typeof options.logger.log !== 'function') { 38 | throw new Error('Logger.log is not a function. (feathers-profiler)'); 39 | } 40 | if (typeof options.logMsg !== 'function') { 41 | throw new Error('logMsg is not a function. (feathers-profiler)'); 42 | } 43 | if (typeof options.statsDetail !== 'function') { 44 | throw new Error('statsDetail is not a function. (feathers-profiler)'); 45 | } 46 | 47 | return function () { 48 | debug('Initializing logger-stats plugin'); 49 | app = this; 50 | 51 | if (typeof app.hooks !== 'function') { 52 | throw new Error('feathers-hooks >= 1.6.0 is needed. (logger-stats)'); 53 | } 54 | 55 | instrumentServices(); 56 | }; 57 | } 58 | 59 | export function defaultLogMsg (hook) { 60 | hook._log = hook._log || {}; 61 | const elapsed = Math.round(hook._log.elapsed / 1e5) / 10; 62 | const header = `${timestamp()} ${hook._log.route}::${hook.method}`; 63 | const trailer = `(${hook.params.provider || 'server'}) ${elapsed} ms - ${pending} pending`; 64 | 65 | return `${header} ${trailer}` + 66 | (hook.error ? ` - FAILED ${(hook.original || {}).type} ${hook.error.message || ''}` : ''); 67 | } 68 | 69 | function instrumentServices () { 70 | debug('Creating app.hooks'); 71 | app.hooks({ 72 | before: { all: timeStart }, 73 | after: { all: timeEnd }, 74 | error: { 75 | all: (hook) => { 76 | hook._log = hook._log || { route: '', key: '', hrtime: [0, 0], elapsed: 0 }; 77 | 78 | if (hook._log.hrtime !== 0) { 79 | const diff = process.hrtime(hook._log.hrtime || [0, 0]); 80 | hook._log.elapsed = (diff[0] * 1e9) + diff[1]; 81 | } 82 | 83 | pending += -1; 84 | 85 | if (options.logger) { 86 | options.logger.log(options.logMsg(hook)); 87 | } 88 | } 89 | } 90 | }); 91 | } 92 | 93 | export function timeStart (hook) { 94 | const route = hook.path; // feathers-hooks v1.7.0 required 95 | let key = ''; 96 | 97 | debug(`timeStart ${route} ${hook.method} ${hook.params.provider}`); 98 | 99 | if (options.stats !== 'none') { 100 | cache[route] = cache[route] || {}; 101 | cache[route][hook.method] = cache[route][hook.method] || {}; 102 | 103 | key = getKey(options); 104 | 105 | const routeMethod = cache[route][hook.method]; 106 | routeMethod[key] = routeMethod[key] || Object.assign({}, cacheEntry); 107 | routeMethod[key].calledCount += 1; 108 | routeMethod[key].pendingTotal += pending; 109 | } 110 | 111 | pending += 1; 112 | 113 | hook._log = { 114 | route, 115 | key, 116 | hrtime: [0, 0], 117 | elapsed: 0 118 | }; 119 | hook._log.hrtime = process.hrtime(); // V8 bug: breaks if inside the above object literal 120 | 121 | return hook; 122 | 123 | function getKey (options) { // to reduce complexity for code climate 124 | return options.stats === 'detail' ? (options.statsDetail(hook) || '_misc') : '_total'; 125 | } 126 | } 127 | 128 | export function timeEnd (hook) { 129 | if (!hook._log || !hook._log.hrtime) { 130 | return; // ignore RethinkDB change-feed 131 | } 132 | 133 | const diff = process.hrtime(hook._log.hrtime); 134 | pending += -1; 135 | debug(`timeEnd ${hook._log.route} ${hook.method} ${hook.params.provider}`); 136 | 137 | hook._log.elapsed = (diff[0] * 1e9) + diff[1]; 138 | 139 | if (options.stats !== 'none') { 140 | const entry = cache[hook._log.route][hook.method][hook._log.key]; 141 | const nano = hook._log.elapsed; 142 | 143 | entry.resolvedCount += 1; 144 | entry.nanoTotal += nano; 145 | entry.nanoMin = Math.min(entry.nanoMin, nano); 146 | entry.nanoMax = Math.max(entry.nanoMax, nano); 147 | 148 | if (hook.method === 'find' || hook.method === 'get') { 149 | const items = getItems(hook); 150 | entry.resultItemsCount += Array.isArray(items) ? items.length : 1; 151 | } 152 | } 153 | 154 | if (options.logger) { 155 | options.logger.log(options.logMsg(hook)); 156 | } 157 | 158 | function getItems (hook) { // to reduce complexity for code climate 159 | const result = hook.result; 160 | return result ? result.data || result : result; 161 | } 162 | } 163 | 164 | export function getProfile () { 165 | debug('Get timings'); 166 | 167 | if (options.stats !== 'none') { 168 | Object.keys(cache).forEach(route => { 169 | Object.keys(cache[route]).forEach(method => { 170 | if (options.stats === 'detail') { 171 | const total = Object.assign({}, cacheEntry); 172 | const rM = cache[route][method]; 173 | 174 | Object.keys(rM).forEach(key => { 175 | const rMK = rM[key]; 176 | rMK.avgMs = !rMK.resolvedCount ? 0 : rMK.nanoTotal / rMK.resolvedCount / 1e6; 177 | rMK.pendingAvg = !rMK.resolvedCount ? 0 : rMK.pendingTotal / rMK.resolvedCount; 178 | 179 | total.calledCount += rMK.calledCount; 180 | total.resolvedCount += rMK.resolvedCount; 181 | total.nanoTotal += rMK.nanoTotal; 182 | total.nanoMin = Math.min(total.nanoMin, rMK.nanoMin); 183 | total.nanoMax = Math.max(total.nanoMax, rMK.nanoMax); 184 | total.resultItemsCount += rMK.resultItemsCount; 185 | total.pendingTotal += rMK.pendingTotal; 186 | }); 187 | 188 | total.avgMs = !total.resolvedCount ? 0 : total.nanoTotal / total.resolvedCount / 1e6; 189 | total.pendingAvg = !total.calledCount ? 0 : total.pendingTotal / total.calledCount; 190 | 191 | cache[route][method]._total = total; 192 | } else { 193 | const total2 = cache[route][method]._total; 194 | 195 | total2.avgMs = !total2.resolvedCount ? 0 : total2.nanoTotal / total2.resolvedCount / 1e6; 196 | total2.pendingAvg = !total2.calledCount ? 0 : total2.pendingTotal / total2.calledCount; 197 | 198 | cache[route][method]._total = total2; 199 | } 200 | }); 201 | }); 202 | } 203 | 204 | return cache; 205 | } 206 | 207 | export function getPending () { 208 | debug('getPending', pending); 209 | return pending; 210 | } 211 | 212 | export function clearProfile () { 213 | debug('Clearing cache'); 214 | cache = {}; 215 | } 216 | 217 | export function timestamp () { 218 | const date = new Date(); 219 | const last2 = (numb) => `0${numb}`.slice(-2); 220 | return `${last2(date.getHours())}:${last2(date.getMinutes())}:${last2(date.getSeconds())}`; 221 | } 222 | -------------------------------------------------------------------------------- /test/loggerStats.test.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | 3 | const feathers = require('feathers'); 4 | const hooks = require('feathers-hooks'); 5 | const assert = require('chai').assert; 6 | 7 | const { profiler, getProfile, clearProfile, getPending } = require('../src'); 8 | 9 | const logger = (cache) => ({ log: msg => { cache.push(msg); } }); 10 | 11 | const logMsg = hook => { 12 | hook._log = hook._log || {}; 13 | 14 | return { 15 | route: hook._log.route, 16 | method: hook.method, 17 | provider: hook.params.provider || 'server', 18 | elapsed: Math.round(hook._log.elapsed / 1e5) / 10, 19 | pending: getPending(), 20 | original: null, 21 | error: null 22 | }; 23 | }; 24 | 25 | function services1 () { 26 | this.use('/messages', { 27 | create: data => Promise.resolve(data), 28 | find: params => Promise.resolve([ 29 | { text: 'message a' }, 30 | { text: 'message b' }, 31 | { text: 'message c' } 32 | ]) 33 | }); 34 | 35 | this.use('/users', { 36 | create: data => Promise.resolve(data) 37 | }); 38 | } 39 | 40 | describe('profiler', () => { 41 | describe('Calls are logged & stats created', () => { 42 | let app; 43 | let users; 44 | let messages; 45 | let logCache; 46 | 47 | beforeEach(() => { 48 | logCache = []; 49 | clearProfile(); 50 | 51 | app = feathers() 52 | .configure(hooks()) 53 | .configure(services1) 54 | .configure(profiler({ logger: logger(logCache) })); 55 | 56 | users = app.service('users'); 57 | messages = app.service('messages'); 58 | }); 59 | 60 | it('Handles 1 call', (done) => { 61 | messages.create({ name: 'John Doe' }) 62 | .then(() => { 63 | const stats = getProfile(); 64 | 65 | assert.isArray(logCache, 'log cache is not an array'); 66 | assert.equal(logCache.length, 1, 'log cache is not length 1'); 67 | 68 | assert.equal(stats.messages.create._total.calledCount, 1); 69 | 70 | done(); 71 | }) 72 | .catch(err => { 73 | console.log(err); 74 | assert.fail(false, true, 'unexpected catch'); 75 | done(); 76 | }); 77 | }); 78 | 79 | it('Handles multiple calls on a method', (done) => { 80 | Promise.all([ 81 | messages.create({ text: 'message 1' }), 82 | messages.create({ text: 'message 2' }) 83 | ]) 84 | .then(() => { 85 | const stats = getProfile(); 86 | 87 | assert.isArray(logCache, 'log cache is not an array'); 88 | assert.equal(logCache.length, 2, 'log cache is not length 2'); 89 | 90 | assert.equal(stats.messages.create._total.calledCount, 2); 91 | 92 | done(); 93 | }) 94 | .catch(err => { 95 | console.log(err); 96 | assert.fail(false, true, 'unexpected catch'); 97 | done(); 98 | }); 99 | }); 100 | 101 | it('Handles multiple calls on multiple methods', (done) => { 102 | Promise.all([ 103 | messages.create({ text: 'message 1' }), 104 | messages.create({ text: 'message 2' }), 105 | users.create({ name: 'John Doe' }) 106 | ]) 107 | .then(() => { 108 | const stats = getProfile(); 109 | 110 | assert.isArray(logCache, 'log cache is not an array'); 111 | assert.equal(logCache.length, 3, 'log cache is not length 3'); 112 | 113 | assert.equal(stats.users.create._total.calledCount, 1); 114 | assert.equal(stats.messages.create._total.calledCount, 2); 115 | 116 | done(); 117 | }) 118 | .catch(err => { 119 | console.log(err); 120 | assert.fail(false, true, 'unexpected catch'); 121 | done(); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('Logs and stats are OK', () => { 127 | let app; 128 | let users; 129 | let messages; 130 | let logCache; 131 | 132 | beforeEach(() => { 133 | logCache = []; 134 | clearProfile(); 135 | 136 | app = feathers() 137 | .configure(hooks()) 138 | .configure(services1) 139 | .configure(profiler({ logger: logger(logCache), logMsg, stats: 'detail' })); 140 | 141 | users = app.service('users'); 142 | messages = app.service('messages'); 143 | }); 144 | 145 | it('Handles multiple calls on multiple methods', (done) => { 146 | Promise.all([ 147 | messages.create({ text: 'message 1' }), 148 | messages.create({ text: 'message 2' }), 149 | users.create({ name: 'John Doe' }), 150 | messages.find({ query: { code: 'a' } }), 151 | messages.find({ query: { code: 'b' } }) 152 | ]) 153 | .then(() => { 154 | const stats = getProfile(); 155 | 156 | let logResults = [ 157 | { route: 'messages', 158 | method: 'create', 159 | provider: 'server', 160 | elapsed: 1.4, 161 | pending: 4, 162 | original: null, 163 | error: null }, 164 | { route: 'messages', 165 | method: 'create', 166 | provider: 'server', 167 | elapsed: 1.7, 168 | pending: 3, 169 | original: null, 170 | error: null }, 171 | { route: 'users', 172 | method: 'create', 173 | provider: 'server', 174 | elapsed: 2.2, 175 | pending: 2, 176 | original: null, 177 | error: null }, 178 | { route: 'messages', 179 | method: 'find', 180 | provider: 'server', 181 | elapsed: 2.3, 182 | pending: 1, 183 | original: null, 184 | error: null }, 185 | { route: 'messages', 186 | method: 'find', 187 | provider: 'server', 188 | elapsed: 2.4, 189 | pending: 0, 190 | original: null, 191 | error: null } 192 | ]; 193 | 194 | let statsResults = { 195 | messages: { 196 | create: { 197 | 'id:undefined, params:{}': 198 | { calledCount: 2, 199 | pendingTotal: 1, 200 | pendingAvg: 0.5, 201 | resolvedCount: 2, 202 | nanoTotal: 8744678, 203 | avgMs: 4.372339, 204 | nanoMin: 4213409, 205 | nanoMax: 4531269, 206 | resultItemsCount: 0 }, 207 | _total: 208 | { calledCount: 2, 209 | pendingTotal: 1, 210 | pendingAvg: 0.5, 211 | resolvedCount: 2, 212 | nanoTotal: 8744678, 213 | avgMs: 4.372339, 214 | nanoMin: 4213409, 215 | nanoMax: 4531269, 216 | resultItemsCount: 0 } 217 | }, 218 | find: { 219 | 'id:undefined, params:{"query":{"code":"a"}}': 220 | { calledCount: 1, 221 | pendingTotal: 3, 222 | pendingAvg: 3, 223 | resolvedCount: 1, 224 | nanoTotal: 5058431, 225 | avgMs: 5.058431, 226 | nanoMin: 5058431, 227 | nanoMax: 5058431, 228 | resultItemsCount: 3 }, 229 | 'id:undefined, params:{"query":{"code":"b"}}': 230 | { calledCount: 1, 231 | pendingTotal: 4, 232 | pendingAvg: 4, 233 | resolvedCount: 1, 234 | nanoTotal: 5154055, 235 | avgMs: 5.154055, 236 | nanoMin: 5154055, 237 | nanoMax: 5154055, 238 | resultItemsCount: 3 }, 239 | _total: 240 | { calledCount: 2, 241 | pendingTotal: 7, 242 | pendingAvg: 3.5, 243 | resolvedCount: 2, 244 | nanoTotal: 10212486, 245 | avgMs: 5.106243, 246 | nanoMin: 5058431, 247 | nanoMax: 5154055, 248 | resultItemsCount: 6 } } }, 249 | users: { 250 | create: { 251 | 'id:undefined, params:{}': 252 | { calledCount: 1, 253 | pendingTotal: 2, 254 | pendingAvg: 2, 255 | resolvedCount: 1, 256 | nanoTotal: 4999784, 257 | avgMs: 4.999784, 258 | nanoMin: 4999784, 259 | nanoMax: 4999784, 260 | resultItemsCount: 0 }, 261 | _total: 262 | { calledCount: 1, 263 | pendingTotal: 2, 264 | pendingAvg: 2, 265 | resolvedCount: 1, 266 | nanoTotal: 4999784, 267 | avgMs: 4.999784, 268 | nanoMin: 4999784, 269 | nanoMax: 4999784, 270 | resultItemsCount: 0 } 271 | } 272 | } 273 | }; 274 | 275 | assert.isArray(logCache, 'log cache is not an array'); 276 | assert.deepEqual(sanitizeLogs(logCache), sanitizeLogs(logResults)); 277 | assert.deepEqual(sanitizeStats(stats), sanitizeStats(statsResults)); 278 | 279 | const total = statsResults.messages.find._total; 280 | assert.equal( 281 | total.avgMs, 282 | !total.resolvedCount ? 0 : total.nanoTotal / total.resolvedCount / 1e6 283 | ); 284 | 285 | done(); 286 | }) 287 | .catch(err => { 288 | console.log(err); 289 | assert.fail(false, true, 'unexpected catch'); 290 | done(); 291 | }); 292 | }); 293 | }); 294 | 295 | describe('Stats collection can be turned off', () => { 296 | let app; 297 | let users; 298 | let messages; 299 | let logCache; 300 | 301 | beforeEach(() => { 302 | logCache = []; 303 | clearProfile(); 304 | 305 | app = feathers() 306 | .configure(hooks()) 307 | .configure(services1) 308 | .configure(profiler({ logger: logger(logCache), logMsg, stats: null })); 309 | 310 | users = app.service('users'); 311 | messages = app.service('messages'); 312 | }); 313 | 314 | it('Handles multiple calls on multiple methods', (done) => { 315 | Promise.all([ 316 | messages.create({ text: 'message 1' }), 317 | messages.create({ text: 'message 2' }), 318 | users.create({ name: 'John Doe' }), 319 | messages.find({ query: { code: 'a' } }), 320 | messages.find({ query: { code: 'b' } }) 321 | ]) 322 | .then(() => { 323 | const stats = getProfile(); 324 | 325 | let logResults = [ 326 | { route: 'messages', 327 | method: 'create', 328 | provider: 'server', 329 | elapsed: 1.4, 330 | pending: 4, 331 | original: null, 332 | error: null }, 333 | { route: 'messages', 334 | method: 'create', 335 | provider: 'server', 336 | elapsed: 1.7, 337 | pending: 3, 338 | original: null, 339 | error: null }, 340 | { route: 'users', 341 | method: 'create', 342 | provider: 'server', 343 | elapsed: 2.2, 344 | pending: 2, 345 | original: null, 346 | error: null }, 347 | { route: 'messages', 348 | method: 'find', 349 | provider: 'server', 350 | elapsed: 2.3, 351 | pending: 1, 352 | original: null, 353 | error: null }, 354 | { route: 'messages', 355 | method: 'find', 356 | provider: 'server', 357 | elapsed: 2.4, 358 | pending: 0, 359 | original: null, 360 | error: null } 361 | ]; 362 | 363 | assert.isArray(logCache, 'log cache is not an array'); 364 | assert.deepEqual(sanitizeLogs(logCache), sanitizeLogs(logResults)); 365 | assert.deepEqual(sanitizeStats(stats), {}); 366 | 367 | done(); 368 | }) 369 | .catch(err => { 370 | console.log(err); 371 | assert.fail(false, true, 'unexpected catch'); 372 | done(); 373 | }); 374 | }); 375 | }); 376 | 377 | describe('Stats categories can be customized', () => { 378 | let app; 379 | let users; 380 | let messages; 381 | let logCache; 382 | 383 | const statsDetail = hook => `q`; 384 | 385 | beforeEach(() => { 386 | logCache = []; 387 | clearProfile(); 388 | 389 | app = feathers() 390 | .configure(hooks()) 391 | .configure(services1) 392 | .configure(profiler({ logger: logger(logCache), logMsg, stats: 'detail', statsDetail })); 393 | 394 | users = app.service('users'); 395 | messages = app.service('messages'); 396 | }); 397 | 398 | it('Handles multiple calls on multiple methods', (done) => { 399 | Promise.all([ 400 | messages.create({ text: 'message 1' }), 401 | messages.create({ text: 'message 2' }), 402 | users.create({ name: 'John Doe' }), 403 | messages.find({ query: { code: 'a' } }), 404 | messages.find({ query: { code: 'b' } }) 405 | ]) 406 | .then(() => { 407 | const stats = getProfile(); 408 | 409 | let logResults = [ 410 | { route: 'messages', 411 | method: 'create', 412 | provider: 'server', 413 | elapsed: 1.4, 414 | pending: 4, 415 | original: null, 416 | error: null }, 417 | { route: 'messages', 418 | method: 'create', 419 | provider: 'server', 420 | elapsed: 1.7, 421 | pending: 3, 422 | original: null, 423 | error: null }, 424 | { route: 'users', 425 | method: 'create', 426 | provider: 'server', 427 | elapsed: 2.2, 428 | pending: 2, 429 | original: null, 430 | error: null }, 431 | { route: 'messages', 432 | method: 'find', 433 | provider: 'server', 434 | elapsed: 2.3, 435 | pending: 1, 436 | original: null, 437 | error: null }, 438 | { route: 'messages', 439 | method: 'find', 440 | provider: 'server', 441 | elapsed: 2.4, 442 | pending: 0, 443 | original: null, 444 | error: null } 445 | ]; 446 | 447 | let statsResults = { 448 | messages: { 449 | create: { 450 | q: 451 | { calledCount: 2, 452 | pendingTotal: 1, 453 | pendingAvg: 0.5, 454 | resolvedCount: 2, 455 | nanoTotal: 7385770, 456 | avgMs: 3.692885, 457 | nanoMin: 3679845, 458 | nanoMax: 3705925, 459 | resultItemsCount: 0 }, 460 | _total: 461 | { calledCount: 2, 462 | pendingTotal: 1, 463 | pendingAvg: 0.5, 464 | resolvedCount: 2, 465 | nanoTotal: 7385770, 466 | avgMs: 3.692885, 467 | nanoMin: 3679845, 468 | nanoMax: 3705925, 469 | resultItemsCount: 0 } }, 470 | find: { 471 | q: 472 | { calledCount: 2, 473 | pendingTotal: 7, 474 | pendingAvg: 3.5, 475 | resolvedCount: 2, 476 | nanoTotal: 7358761, 477 | avgMs: 3.6793805, 478 | nanoMin: 3679317, 479 | nanoMax: 3679444, 480 | resultItemsCount: 6 }, 481 | _total: 482 | { calledCount: 2, 483 | pendingTotal: 7, 484 | pendingAvg: 3.5, 485 | resolvedCount: 2, 486 | nanoTotal: 7358761, 487 | avgMs: 3.6793805, 488 | nanoMin: 3679317, 489 | nanoMax: 3679444, 490 | resultItemsCount: 6 491 | } 492 | } 493 | }, 494 | users: { 495 | create: { 496 | q: 497 | { calledCount: 1, 498 | pendingTotal: 2, 499 | pendingAvg: 2, 500 | resolvedCount: 1, 501 | nanoTotal: 3689297, 502 | avgMs: 3.689297, 503 | nanoMin: 3689297, 504 | nanoMax: 3689297, 505 | resultItemsCount: 0 }, 506 | _total: 507 | { calledCount: 1, 508 | pendingTotal: 2, 509 | pendingAvg: 2, 510 | resolvedCount: 1, 511 | nanoTotal: 3689297, 512 | avgMs: 3.689297, 513 | nanoMin: 3689297, 514 | nanoMax: 3689297, 515 | resultItemsCount: 0 516 | } 517 | } 518 | } 519 | }; 520 | 521 | assert.isArray(logCache, 'log cache is not an array'); 522 | assert.deepEqual(sanitizeLogs(logCache), sanitizeLogs(logResults)); 523 | assert.deepEqual(sanitizeStats(stats), sanitizeStats(statsResults)); 524 | 525 | const total = statsResults.messages.find._total; 526 | assert.equal( 527 | total.avgMs, 528 | !total.resolvedCount ? 0 : total.nanoTotal / total.resolvedCount / 1e6 529 | ); 530 | 531 | done(); 532 | }) 533 | .catch(err => { 534 | console.log(err); 535 | assert.fail(false, true, 'unexpected catch'); 536 | done(); 537 | }); 538 | }); 539 | }); 540 | 541 | describe('Does not fail with no options', () => { 542 | let app; 543 | let messages; 544 | 545 | beforeEach(() => { 546 | clearProfile(); 547 | 548 | app = feathers() 549 | .configure(hooks()) 550 | .configure(services1) 551 | .configure(profiler()); 552 | 553 | messages = app.service('messages'); 554 | }); 555 | 556 | it('Handles multiple calls on multiple methods', (done) => { 557 | Promise.all([ 558 | messages.create({ text: 'message 1' }) 559 | ]) 560 | .then(() => { 561 | done(); 562 | }) 563 | .catch(err => { 564 | console.log(err); 565 | assert.fail(false, true, 'unexpected catch'); 566 | done(); 567 | }); 568 | }); 569 | }); 570 | }); 571 | 572 | function sanitizeLogs (logs1) { 573 | const logs = logs1.slice(0); 574 | 575 | logs.forEach(log => { 576 | delete log.elapsed; 577 | }); 578 | 579 | return logs; 580 | } 581 | 582 | function sanitizeStats (stats1) { 583 | const stats = JSON.parse(JSON.stringify(stats1)); 584 | 585 | Object.keys(stats).forEach(service => { 586 | Object.keys(stats[service]).forEach(method => { 587 | Object.keys(stats[service][method]).forEach(key => { 588 | const data = stats[service][method][key]; 589 | 590 | delete data.nanoTotal; 591 | delete data.avgMs; 592 | delete data.nanoMin; 593 | delete data.nanoMax; 594 | }); 595 | }); 596 | }); 597 | 598 | return stats; 599 | } 600 | --------------------------------------------------------------------------------