├── .eslintignore ├── .eslintrc.json ├── .editorconfig ├── changelog.md ├── example ├── app.js └── routes │ └── users.js ├── .gitignore ├── package.json ├── README.md └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | node_modules/* 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "camelcase": 0 5 | }, 6 | "globals": { 7 | "__dirname": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 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 | tab_width = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [Makefile] 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 2018-01-24/2.1.0 2 | 3 | - feat: add `fn` & `filename` when error 4 | 5 | ## 2018-01-23/2.0.2 6 | 7 | - feat: add `name` for log 8 | 9 | ## 2018-01-17/2.0.1 10 | 11 | - fix `take` 12 | 13 | ## 2018-01-14/2.0.0 14 | 15 | - use eslint@standard 16 | - add `async_hooks` support(node engines 4 -> 8) 17 | - update deps 18 | - update example 19 | - update README.md 20 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const koaYieldBreakpoint = require('..')({ 3 | name: 'api', 4 | files: [path.join(__dirname, '**/*.js')] 5 | }) 6 | 7 | const koa = require('koa') 8 | const route = require('koa-route') 9 | const app = koa() 10 | 11 | app.use(koaYieldBreakpoint) 12 | app.use(route.post('/users', require('./routes/users').createUser))// curl -XPOST localhost:3000/users 13 | 14 | app.listen(3000, () => { 15 | console.log('listening on 3000') 16 | }) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Node ### 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 | # all bout databases: 33 | *.sqlite 34 | knexfile.js 35 | 36 | #not ignore app symbolic 37 | !node_modules/base 38 | 39 | internal-errors.txt 40 | docs 41 | doc 42 | .idea 43 | 44 | # heapdump files 45 | heapdump-* 46 | dump.rdb 47 | -------------------------------------------------------------------------------- /example/routes/users.js: -------------------------------------------------------------------------------- 1 | const Mongolass = require('mongolass') 2 | const mongolass = new Mongolass('mongodb://localhost:27017/test') 3 | const User = mongolass.model('User') 4 | const Post = mongolass.model('Post') 5 | const Comment = mongolass.model('Comment') 6 | 7 | exports.createUser = function * () { 8 | const name = this.query.name || 'default' 9 | const age = +this.query.age || 18 10 | yield createUser(name, age) 11 | this.status = 204 12 | } 13 | 14 | function * createUser (name, age) { 15 | const user = (yield User.create({ 16 | name, 17 | age 18 | })).ops[0] 19 | yield createPost(user) 20 | } 21 | 22 | function * createPost (user) { 23 | const post = (yield Post.create({ 24 | uid: user._id, 25 | title: 'post', 26 | content: 'post' 27 | })).ops[0] 28 | 29 | yield createComment(user, post) 30 | } 31 | 32 | function * createComment (user, post) { 33 | yield Comment.create({ 34 | userId: user._id, 35 | postId: post._id, 36 | content: 'comment' 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-yield-breakpoint", 3 | "version": "2.1.0", 4 | "description": "Add breakpoints around `yield` expression especially for koa@1.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "test": "node example/app" 9 | }, 10 | "keywords": [ 11 | "koa", 12 | "yield", 13 | "breakpoint" 14 | ], 15 | "author": { 16 | "name": "nswbmw", 17 | "url": "https://github.com/nswbmw" 18 | }, 19 | "license": "MIT", 20 | "engines": { 21 | "node": ">=8.1" 22 | }, 23 | "dependencies": { 24 | "debug": "3.1.0", 25 | "escodegen": "1.9.0", 26 | "esprima": "4.0.0", 27 | "glob": "7.1.2", 28 | "lodash": "4.17.4", 29 | "node-uuid": "1.4.8", 30 | "shimmer": "1.2.0", 31 | "source-map-support": "0.5.0" 32 | }, 33 | "devDependencies": { 34 | "eslint": "4.15.0", 35 | "eslint-config-standard": "11.0.0-beta.0", 36 | "eslint-plugin-import": "2.8.0", 37 | "eslint-plugin-node": "5.2.1", 38 | "eslint-plugin-promise": "3.6.0", 39 | "eslint-plugin-standard": "3.0.1", 40 | "koa": "1.4.1", 41 | "koa-route": "2.4.2", 42 | "mongolass": "4.1.0" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/nswbmw/koa-yield-breakpoint" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## koa-yield-breakpoint 2 | 3 | Add breakpoints around `yield` expression especially for koa@1. 4 | 5 | ### Install 6 | 7 | ```sh 8 | $ npm i koa-yield-breakpoint --save 9 | ``` 10 | 11 | ### Example 12 | 13 | ```sh 14 | $ DEBUG=koa-yield-breakpoint node example/app 15 | $ curl -XPOST localhost:3000/users 16 | ``` 17 | 18 | ### Usage 19 | 20 | ```js 21 | // Generally, on top of the main file 22 | const koaYieldBreakpoint = require('koa-yield-breakpoint')({ 23 | name: 'api', 24 | files: ['./routes/*.js'], 25 | // store: new require('koa-yield-breakpoint-mongodb')({ 26 | // url: 'mongodb://localhost:27017/test', 27 | // coll: 'koa-yield-breakpoint-loggers' 28 | // }) 29 | }) 30 | 31 | const koa = require('koa') 32 | const routes = require('./routes') 33 | const app = koa() 34 | 35 | // Generally, above other middlewares 36 | app.use(koaYieldBreakpoint) 37 | 38 | routes(app) 39 | 40 | app.listen(3000, () => { 41 | console.log('listening on 3000') 42 | }) 43 | ``` 44 | 45 | **NB**: You'd better put `require('koa-yield-breakpoint')` on the top of the main file, because `koa-yield-breakpoint` rewrite `Module.prototype._compile`. 46 | 47 | koa-yield-breakpoint will wrap `YieldExpression` with: 48 | 49 | ```js 50 | global.logger( 51 | this, 52 | function*(){ 53 | return yield YieldExpression 54 | }, 55 | YieldExpressionString, 56 | filename 57 | ) 58 | ``` 59 | 60 | log like: 61 | 62 | ```json 63 | { 64 | "name": "api", 65 | "requestId": "222f66ec-7259-4d20-930f-2ac035c16e7b", 66 | "timestamp": "2018-01-15T05:02:18.827Z", 67 | "this": { 68 | "state": {}, 69 | "params": {}, 70 | "request": { 71 | "method": "POST", 72 | "path": "/users", 73 | "header": { 74 | "host": "localhost:3000", 75 | "user-agent": "curl/7.54.0", 76 | "accept": "*/*" 77 | }, 78 | "query": {} 79 | }, 80 | "response": { 81 | "status": 404 82 | } 83 | }, 84 | "type": "start", 85 | "step": 1, 86 | "take": 0 87 | } 88 | ``` 89 | 90 | koa-yield-breakpoint will print logs to console by default, if you want to save these logs to db, set `store` option, eg: [koa-yield-breakpoint-mongodb](https://github.com/nswbmw/koa-yield-breakpoint-mongodb). 91 | 92 | **NB:** `type` in `['start', 'beforeYield', 'afterYield', 'error', 'end']`, `take` is ms. 93 | 94 | ### SourceMap 95 | 96 | After v1.1.0, koa-yield-breakpoint support source map: 97 | 98 | **example/routes/users.js** 99 | 100 | ```js 101 | const Mongolass = require('mongolass') 102 | const mongolass = new Mongolass() 103 | mongolass.connect('mongodb://localhost:27017/test') 104 | 105 | exports.getUsers = function* getUsers() { 106 | yield mongolass.model('users').create({ 107 | name: 'xx', 108 | age: 18 109 | }) 110 | 111 | const users = yield mongolass.model('users').find() 112 | 113 | 114 | console.log(haha) 115 | this.body = users 116 | } 117 | ``` 118 | 119 | Will output: 120 | 121 | ```js 122 | ReferenceError: haha is not defined 123 | at Object.getUsers (/Users/nswbmw/node/koa-yield-breakpoint/example/routes/users.js:16:15) 124 | at next (native) 125 | at Object. (/Users/nswbmw/node/koa-yield-breakpoint/node_modules/koa-route/index.js:34:19) 126 | at next (native) 127 | at onFulfilled (/Users/nswbmw/node/koa-yield-breakpoint/node_modules/koa/node_modules/co/index.js:65:19) 128 | ``` 129 | 130 | ### Options 131 | 132 | require('koa-yield-breakpoint')(option) 133 | 134 | - name{String}: service name added to log. 135 | - sourcemap{Boolean}: whether open sourcemap, default: `true`, will **increase** memory usage. 136 | - files{String[]}: files pattern, see [glob](https://github.com/isaacs/node-glob), required. 137 | - exclude_files{String[]}: exclude files pattern, default `[]`. 138 | - store{Object}: backend store instance, see [koa-yield-breakpoint-mongodb](https://github.com/nswbmw/koa-yield-breakpoint-mongodb), default print to console. 139 | - filter{Object}: reserved field in koa's `this`, default: 140 | ``` 141 | { 142 | ctx: ['state', 'params'], 143 | request: ['method', 'path', 'header', 'query', 'body'], 144 | response: ['status', 'body'] 145 | } 146 | ``` 147 | - loggerName{String}: global logger name, default `logger`. 148 | - requestIdPath{String}: requestId path in `this`, default `requestId`. 149 | - yieldCondition{Function}: parameters `(filename, yieldExpression, parsedYieldExpression)`, return a object: 150 | - wrapYield{Boolean}: if `true` return wraped yieldExpression, default `true`. 151 | - deep{Boolean}: if `true` deep wrap yieldExpression, default `true`. 152 | - others: see [glob](https://github.com/isaacs/node-glob#options). 153 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const asyncIdMap = new Map() 2 | const sourceMap = new Map() 3 | require('source-map-support').install({ 4 | retrieveSourceMap: (source) => { 5 | const sourcemap = sourceMap.get(source) 6 | if (sourcemap) { 7 | return { 8 | url: source, 9 | map: sourcemap, 10 | environment: 'node' 11 | } 12 | } 13 | return null 14 | } 15 | }) 16 | 17 | const assert = require('assert') 18 | const Module = require('module') 19 | const async_hooks = require('async_hooks') 20 | 21 | const _ = require('lodash') 22 | const glob = require('glob') 23 | const uuid = require('node-uuid') 24 | const esprima = require('esprima') 25 | const shimmer = require('shimmer') 26 | const escodegen = require('escodegen') 27 | const debug = require('debug')('koa-yield-breakpoint') 28 | 29 | async_hooks.createHook({ 30 | init (asyncId, type, triggerAsyncId) { 31 | const ctx = getCtx(triggerAsyncId) 32 | if (ctx) { 33 | asyncIdMap.set(asyncId, ctx) 34 | } else { 35 | asyncIdMap.set(asyncId, triggerAsyncId) 36 | } 37 | }, 38 | destroy (asyncId) { 39 | asyncIdMap.delete(asyncId) 40 | } 41 | }).enable() 42 | 43 | const defaultOpt = { 44 | sourcemap: true, 45 | nodir: true, 46 | absolute: true, 47 | filter: { 48 | ctx: ['state', 'params'], 49 | request: ['method', 'path', 'header', 'query', 'body'], 50 | response: ['status', 'body'] 51 | }, 52 | loggerName: 'logger', 53 | requestIdPath: 'requestId' 54 | } 55 | 56 | module.exports = function (opt) { 57 | opt = _.defaults(opt || {}, defaultOpt) 58 | opt.filter = opt.filter || {} 59 | opt.filter.ctx = opt.filter.ctx || defaultOpt.filter.ctx 60 | opt.filter.request = opt.filter.request || defaultOpt.filter.request 61 | opt.filter.response = opt.filter.response || defaultOpt.filter.response 62 | debug('options: %j', opt) 63 | 64 | const name = opt.name 65 | const loggerName = opt.loggerName 66 | const requestIdPath = opt.requestIdPath 67 | const files = opt.files 68 | const exclude_files = opt.exclude_files || [] 69 | const store = opt.store || { save: (record) => console.log('%j', record) } 70 | const yieldCondition = opt.yieldCondition 71 | const sourcemap = opt.sourcemap 72 | assert(requestIdPath && _.isString(requestIdPath), '`requestIdPath` option must be string') 73 | assert(files && _.isArray(files), '`files`{array} option required') 74 | assert(_.isArray(exclude_files), '`exclude_files`{array} option required') 75 | assert(store && _.isFunction(store.save), '`store.save`{function} option required, see: koa-yield-breakpoint-mongodb') 76 | if (yieldCondition) { 77 | assert(_.isFunction(yieldCondition), '`yieldCondition` option must be function') 78 | } 79 | 80 | // add global logger 81 | global[loggerName] = function * (ctx, fn, fnStr, filename) { 82 | const originalContext = ctx 83 | let requestId = _getRequestId() 84 | 85 | const asyncId = async_hooks.executionAsyncId() 86 | if (!requestId) { 87 | const _ctx = getCtx(asyncId) 88 | if (_ctx) { 89 | ctx = _ctx 90 | requestId = _getRequestId() 91 | } 92 | } else { 93 | asyncIdMap.set(asyncId, ctx) 94 | } 95 | 96 | let prevRecord 97 | if (requestId) { 98 | prevRecord = _logger('beforeYield') 99 | } 100 | let result 101 | try { 102 | result = yield * fn.call(originalContext) 103 | } catch (e) { 104 | // use innermost error info 105 | e._fn = e._fn || fnStr 106 | e._filename = e._filename || filename 107 | throw e 108 | } 109 | if (requestId) { 110 | _logger('afterYield', result, prevRecord && prevRecord.timestamp) 111 | } 112 | return result 113 | 114 | function _getRequestId () { 115 | return ctx && ctx.app && _.get(ctx, requestIdPath) 116 | } 117 | 118 | function _logger (type, result, prevTimestamp) { 119 | const _this = _.pick(ctx, opt.filter.ctx) 120 | _this.request = _.pick(ctx.request, opt.filter.request) 121 | _this.response = _.pick(ctx.response, opt.filter.response) 122 | 123 | const record = { 124 | name, 125 | requestId, 126 | step: ++ctx.step, 127 | filename, 128 | timestamp: new Date(), 129 | this: _this, 130 | type, 131 | fn: fnStr, 132 | result 133 | } 134 | addTake(ctx, record, prevTimestamp) 135 | debug(record) 136 | 137 | store.save(record, ctx) 138 | return record 139 | } 140 | } 141 | 142 | let filenames = [] 143 | files.forEach(filePattern => { 144 | if (filePattern) { 145 | filenames = filenames.concat(glob.sync(filePattern, opt)) 146 | } 147 | }) 148 | exclude_files.forEach(filePattern => { 149 | if (filePattern) { 150 | _.pullAll(filenames, glob.sync(filePattern, opt)) 151 | } 152 | }) 153 | filenames = _.uniq(filenames) 154 | debug('matched files: %j', filenames) 155 | 156 | // wrap Module.prototype._compile 157 | shimmer.wrap(Module.prototype, '_compile', function (__compile) { 158 | return function koaBreakpointCompile (content, filename) { 159 | if (!_.includes(filenames, filename)) { 160 | try { 161 | return __compile.call(this, content, filename) 162 | } catch (e) { 163 | // `try { require('...') } catch (e) { ... }` will not print compile error message 164 | debug('cannot compile file: %s', filename) 165 | debug(e.stack) 166 | throw e 167 | } 168 | } 169 | 170 | let parsedCodes 171 | try { 172 | parsedCodes = esprima.parse(content, { loc: true }) 173 | } catch (e) { 174 | console.error('cannot parse file: %s', filename) 175 | console.error(e.stack) 176 | process.exit(1) 177 | } 178 | 179 | findYieldAndWrapLogger(parsedCodes) 180 | try { 181 | content = escodegen.generate(parsedCodes, { 182 | format: { indent: { style: ' ' } }, 183 | sourceMap: filename, 184 | sourceMapWithCode: true 185 | }) 186 | } catch (e) { 187 | console.error('cannot generate code for file: %s', filename) 188 | console.error(e.stack) 189 | process.exit(1) 190 | } 191 | debug('file %s regenerate codes:\n%s', filename, content.code) 192 | 193 | // add to sourcemap cache 194 | if (sourcemap) { 195 | sourceMap.set(filename, content.map.toString()) 196 | } 197 | return __compile.call(this, content.code, filename) 198 | 199 | function findYieldAndWrapLogger (node) { 200 | if (!node || typeof node !== 'object') { 201 | return 202 | } 203 | let condition = { 204 | wrapYield: true, 205 | deep: true 206 | } 207 | 208 | if (node.hasOwnProperty('type') && node.type === 'YieldExpression' && !node.__skip) { 209 | const codeLine = node.loc.start 210 | const __argument = node.argument 211 | const __expressionStr = escodegen.generate(__argument) 212 | const expressionStr = ` 213 | global.${loggerName}( 214 | this, 215 | function*(){ 216 | return yield ${__expressionStr} 217 | }, 218 | ${JSON.stringify(__expressionStr)}, 219 | ${JSON.stringify(filename + ':' + codeLine.line + ':' + codeLine.column)} 220 | )` 221 | 222 | if (yieldCondition) { 223 | condition = yieldCondition(filename, __expressionStr, __argument) || condition 224 | assert(typeof condition === 'object', '`yieldCondition` must return a object') 225 | } 226 | if (condition.wrapYield) { 227 | try { 228 | node.argument = esprima.parse(expressionStr, { loc: true }).body[0].expression 229 | node.delegate = true 230 | try { 231 | // skip process this YieldExpression 232 | node.argument.arguments[1].body.body[0].argument.__skip = true 233 | // try correct loc 234 | node.argument.arguments[1].body.body[0].argument.argument = __argument 235 | } catch (e) { /* ignore */ } 236 | } catch (e) { 237 | console.error('cannot parse expression:') 238 | console.error(expressionStr) 239 | console.error(e.stack) 240 | process.exit(1) 241 | } 242 | } 243 | } 244 | if (condition.deep) { 245 | for (const key in node) { 246 | if (node.hasOwnProperty(key)) { 247 | findYieldAndWrapLogger(node[key]) 248 | } 249 | } 250 | } 251 | } 252 | } 253 | }) 254 | 255 | return function * koaYieldBreakpoint (next) { 256 | if (!_.get(this, requestIdPath)) { 257 | _.set(this, requestIdPath, uuid.v4()) 258 | } 259 | this.step = 0 260 | this.timestamps = {} 261 | 262 | _logger(this, 'start') 263 | try { 264 | yield next 265 | } catch (e) { 266 | _logger(this, 'error', e) 267 | throw e 268 | } finally { 269 | _logger(this, 'end') 270 | } 271 | 272 | function _logger (ctx, type, err) { 273 | const _this = _.pick(ctx, opt.filter.ctx) 274 | _this.request = _.pick(ctx.request, opt.filter.request) 275 | _this.response = _.pick(ctx.response, opt.filter.response) 276 | 277 | const record = { 278 | name, 279 | requestId: _.get(ctx, requestIdPath), 280 | timestamp: new Date(), 281 | this: _this, 282 | type, 283 | step: ++ctx.step 284 | } 285 | if (err) { 286 | record.error = err 287 | record.fn = err._fn 288 | record.filename = err._filename 289 | delete err._fn 290 | delete err._filename 291 | } 292 | addTake(ctx, record) 293 | debug(record) 294 | 295 | store.save(record, ctx) 296 | } 297 | } 298 | } 299 | 300 | function addTake (ctx, record, prevTimestamp) { 301 | ctx.timestamps[record.step] = record.timestamp 302 | prevTimestamp = prevTimestamp || ctx.timestamps[record.step - 1] 303 | if (prevTimestamp) { 304 | record.take = record.timestamp - prevTimestamp 305 | } else { 306 | // start default 0 307 | record.take = 0 308 | } 309 | } 310 | 311 | function getCtx (asyncId) { 312 | if (!asyncId) { 313 | return 314 | } 315 | if (typeof asyncId === 'object' && asyncId.app) { 316 | return asyncId 317 | } 318 | return getCtx(asyncIdMap.get(asyncId)) 319 | } 320 | --------------------------------------------------------------------------------