├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── .xo-config.json ├── History.md ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── test ├── index.test.js └── test-server.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | ci: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: 13 | - 18 14 | - 20 15 | - 22 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | - name: Install dependencies 21 | run: yarn install 22 | - name: Check linter 23 | run: yarn lint 24 | - name: Run tests 25 | run: yarn test-ci -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS # 2 | ################### 3 | .DS_Store 4 | .idea 5 | Thumbs.db 6 | tmp/ 7 | temp/ 8 | 9 | 10 | # Node.js # 11 | ################### 12 | node_modules 13 | package-lock.json 14 | yarn.lock 15 | npm-debug.log 16 | yarn-debug.log 17 | yarn-error.log 18 | 19 | # Build # 20 | ################### 21 | dist 22 | build 23 | 24 | # NYC # 25 | ################### 26 | coverage 27 | *.lcov 28 | .nyc_output 29 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /.xo-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier": true, 3 | "space": true, 4 | "extends": ["xo-lass"], 5 | "rules": { 6 | "max-params": "off", 7 | "no-bitwise": "off", 8 | "unicorn/prefer-math-trunc": "off", 9 | "n/no-unsupported-features/node-builtins": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 3.2.0 / 2018-03-13 3 | ================== 4 | 5 | * add a transporter to logger opts param (#65) 6 | 7 | 3.1.0 / 2017-09-26 8 | ================== 9 | 10 | * Adds Boom compatibility (#64) 11 | 12 | 3.0.1 / 2017-06-30 13 | ================== 14 | 15 | * Fixes error 'chalk[color] does not return function' when using custom (#61) 16 | 17 | 3.0.0 / 2017-05-16 18 | ================== 19 | 20 | * use async function 21 | * update bytes to 2.x 22 | 23 | 2.0.1 / 2017-01-12 24 | ================== 25 | 26 | * add color for code 0 27 | * bump deps 28 | 29 | 2.0.0 / 2015-11-19 30 | ================== 31 | 32 | * add: support for koa 2 33 | * remove: support for koa 1 34 | 35 | 1.2.2 / 2014-07-05 36 | ================== 37 | 38 | * fix: stop using octal literals for strict mode 39 | 40 | 1.2.1 / 2014-05-13 41 | ================== 42 | 43 | * bump bytes 44 | 45 | 1.2.0 / 2014-02-12 46 | ================== 47 | 48 | * add: add colors and symbols for error and close events 49 | * remove: only make the method bold 50 | 51 | 1.1.1 / 2014-01-15 52 | ================== 53 | 54 | * fix: use `ctx.originalUrl` instead of `ctx.url` 55 | 56 | 1.1.0 / 2013-12-21 57 | ================== 58 | 59 | * add: log the response length of streams 60 | * add: humanize the response time 61 | * add: log when response is finished/closed, not caught upstream 62 | 63 | 1.0.1 / 2013-08-22 64 | ================== 65 | 66 | * fix exception handling 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Koa.js contributors 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # koa-logger 3 | 4 | [![npm version][npm-image]][npm-url] 5 | 6 | Development style logger middleware for [koa](https://github.com/koajs/koa). Compatible with [request-received](https://github.com/cabinjs/request-received). 7 | 8 | ``` 9 | <-- GET / 10 | --> GET / 200 835ms 746b 11 | <-- GET / 12 | --> GET / 200 960ms 1.9kb 13 | <-- GET /users 14 | --> GET /users 200 357ms 922b 15 | <-- GET /users?page=2 16 | --> GET /users?page=2 200 466ms 4.66kb 17 | ``` 18 | 19 | ## Installation 20 | 21 | ```js 22 | $ npm install koa-logger 23 | ``` 24 | 25 | ## Example 26 | 27 | ```js 28 | const logger = require('koa-logger') 29 | const Koa = require('koa') 30 | 31 | const app = new Koa() 32 | app.use(logger()) 33 | ``` 34 | 35 | ## Notes 36 | 37 | Recommended that you `.use()` this middleware near the top 38 | to "wrap" all subsequent middleware. 39 | 40 | ## Use Custom Transporter 41 | 42 | ```js 43 | const logger = require('koa-logger') 44 | const Koa = require('koa') 45 | 46 | const app = new Koa() 47 | app.use(logger((str, args) => { 48 | // redirect koa logger to other output pipe 49 | // default is process.stdout(by console.log function) 50 | })) 51 | ``` 52 | or 53 | ```js 54 | app.use(logger({ 55 | transporter: (str, args) => { 56 | // ... 57 | } 58 | })) 59 | ``` 60 | 61 | Param `str` is output string with ANSI Color, and you can get pure text with other modules like `strip-ansi` 62 | Param `args` is a array by `[format, method, url, status, time, length]` 63 | 64 | ## License 65 | 66 | MIT 67 | 68 | [npm-image]: https://img.shields.io/npm/v/koa-logger.svg?style=flat-square 69 | [npm-url]: https://www.npmjs.com/package/koa-logger 70 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const { process } = require('node:process'); 2 | const Koa = require('koa'); 3 | const compress = require('koa-compress')(); 4 | const logger = require('.'); 5 | 6 | const app = new Koa(); 7 | 8 | // wrap subsequent middleware in a logger 9 | app.use(logger()); 10 | 11 | // 204 12 | app.use((ctx, next) => { 13 | if (ctx.path === '/204') ctx.status = 204; 14 | else return next(); 15 | }); 16 | 17 | // 404 18 | app.use((ctx, next) => { 19 | if (ctx.path === '/404') return; 20 | return next(); 21 | }); 22 | 23 | // destroy 24 | app.use((ctx, next) => { 25 | if (ctx.path === '/close') return ctx.req.destroy(); 26 | return next(); 27 | }); 28 | 29 | // compress the response 1/2 the time to calculate the stream length 30 | app.use((ctx, next) => { 31 | if (Math.random() > 0.5) { 32 | return next(); 33 | } 34 | 35 | return compress(ctx, next); 36 | }); 37 | 38 | // response middleware 39 | app.use(async (ctx, next) => { 40 | // yield control downstream 41 | await next(); 42 | 43 | // sleep for 0-2s 44 | await sleep((Math.random() * 2000) | 0); 45 | 46 | // error 47 | if (Math.random() > 0.75) { 48 | const err = new Error('boom'); 49 | err.status = 500; 50 | throw err; 51 | } 52 | 53 | // random body 54 | const body = Array.from({ length: (Math.random() * 5 * 1024) | 9 }).join('a'); 55 | ctx.status = 200; 56 | ctx.body = body; 57 | }); 58 | 59 | const port = process.env.PORT || 3000; 60 | app.listen(port); 61 | console.log('listening on port ' + port); 62 | 63 | // sleep helper 64 | function sleep(ms) { 65 | return new Promise(function (resolve) { 66 | setTimeout(resolve, ms); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('node:util'); 4 | const onFinished = require('on-finished'); 5 | const Counter = require('passthrough-counter'); 6 | const humanize = require('humanize-number'); 7 | const bytes = require('bytes'); 8 | const chalk = require('chalk'); 9 | 10 | // color map. 11 | const colorCodes = { 12 | 7: 'magenta', 13 | 5: 'red', 14 | 4: 'yellow', 15 | 3: 'cyan', 16 | 2: 'green', 17 | 1: 'green', 18 | 0: 'yellow' 19 | }; 20 | 21 | // Log helper. 22 | function log(print, ctx, start, length, err, event) { 23 | // get the status code of the response 24 | const status = err 25 | ? err.isBoom 26 | ? err.output.statusCode 27 | : err.status || 500 28 | : ctx.status || 404; 29 | 30 | // set the color of the status code; 31 | const s = (status / 100) | 0; 32 | const color = colorCodes[s > 7 ? 0 : s]; 33 | 34 | // get the human readable response length 35 | const formattedLength = [204, 205, 304].includes(status) 36 | ? '' 37 | : length 38 | ? bytes(length).toLowerCase() 39 | : '-'; 40 | 41 | const upstream = err 42 | ? chalk.red('xxx') 43 | : event === 'close' 44 | ? chalk.yellow('-x-') 45 | : chalk.gray('-->'); 46 | 47 | print( 48 | ' ' + 49 | upstream + 50 | ' ' + 51 | chalk.bold('%s') + 52 | ' ' + 53 | chalk.gray('%s') + 54 | ' ' + 55 | chalk[color]('%s') + 56 | ' ' + 57 | chalk.gray('%s') + 58 | ' ' + 59 | chalk.gray('%s'), 60 | ctx.method, 61 | ctx.originalUrl, 62 | status, 63 | time(start), 64 | formattedLength 65 | ); 66 | } 67 | 68 | /** 69 | * Show the response time in a human readable format. 70 | * In milliseconds if less than 10 seconds, 71 | * in seconds otherwise. 72 | */ 73 | function time(start) { 74 | const delta = Date.now() - start; 75 | return humanize( 76 | delta < 10000 ? delta + 'ms' : Math.round(delta / 1000) + 's' 77 | ); 78 | } 79 | 80 | function koaLogger(options) { 81 | // print to console helper. 82 | const print = (function () { 83 | let transporter; 84 | if (typeof options === 'function') { 85 | transporter = options; 86 | } else if (options && options.transporter) { 87 | transporter = options.transporter; 88 | } 89 | 90 | // eslint-disable-next-line func-names 91 | return function printFunc(...args) { 92 | const string = util.format(...args); 93 | if (transporter) transporter(string, args); 94 | else console.log(...args); 95 | }; 96 | })(); 97 | 98 | // eslint-disable-next-line func-names 99 | return async function logger(ctx, next) { 100 | // request 101 | const start = (() => { 102 | // see: https://github.com/koajs/logger/pull/79 103 | const injectedRequestRecivedStartTime = 104 | ctx[Symbol.for('request-received.startTime')]; 105 | if (!injectedRequestRecivedStartTime) return Date.now(); 106 | // TOD-O: remove cabinjs/request-received on our next major release (ts) 107 | console.warn( 108 | '[DEPRECATION WARNING]: Support for `request-received` will be removed in the next major release.' 109 | ); 110 | 111 | // support request-received v0.0.1 112 | if ('getTime' in injectedRequestRecivedStartTime) 113 | return injectedRequestRecivedStartTime.getTime(); 114 | // support request-received v0.2.0 + v0.3.0 115 | return injectedRequestRecivedStartTime; 116 | })(); 117 | 118 | print( 119 | ' ' + 120 | chalk.gray('<--') + 121 | ' ' + 122 | chalk.bold('%s') + 123 | ' ' + 124 | chalk.gray('%s'), 125 | ctx.method, 126 | ctx.originalUrl 127 | ); 128 | 129 | try { 130 | await next(); 131 | } catch (err) { 132 | // log uncaught downstream errors 133 | log(print, ctx, start, null, err); 134 | throw err; 135 | } 136 | 137 | // calculate the length of a streaming response 138 | // by intercepting the stream with a counter. 139 | // only necessary if a content-length header is currently not set. 140 | 141 | let counter; 142 | if (ctx.response.length === 0 && ctx.body && ctx.body.readable) 143 | counter = ctx.body.pipe(new Counter()).on('error', ctx.onerror); 144 | 145 | // log when the response is finished or closed, 146 | // whichever happens first. 147 | onFinished(ctx.res, (error) => { 148 | const event = error ? 'close' : 'finish'; 149 | const length = counter ? counter.length : ctx.response.length; 150 | log(print, ctx, start, length, null, event); 151 | }); 152 | }; 153 | } 154 | 155 | module.exports = koaLogger; 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-logger", 3 | "description": "Logging middleware for koa", 4 | "version": "3.2.1", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "scripts": { 10 | "lint": "xo", 11 | "lint:fix": "xo --fix", 12 | "test": "node --test", 13 | "precoverage": "rimraf .nyc_output coverage", 14 | "test-ci": "c8 npm run test" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/koajs/logger" 19 | }, 20 | "keywords": [ 21 | "koa", 22 | "middleware", 23 | "logger", 24 | "log" 25 | ], 26 | "author": "Christoffer Hallas ", 27 | "contributors": [ 28 | { 29 | "name": "Imed Jaberi", 30 | "email": "imed-jebari@outlook.com", 31 | "url": "https://github.com/3imed-jaberi" 32 | } 33 | ], 34 | "license": "MIT", 35 | "dependencies": { 36 | "bytes": "^3.1.0", 37 | "chalk": "^4.1.2", 38 | "humanize-number": "^0.0.2", 39 | "on-finished": "^2.4.1", 40 | "passthrough-counter": "^1.0.0" 41 | }, 42 | "devDependencies": { 43 | "boom": "^7.3.0", 44 | "c8": "^10.1.3", 45 | "eslint-config-xo-lass": "^2.0.1", 46 | "koa": "^3.0.0", 47 | "koa-compress": "^5.1.1", 48 | "koa-route": "^4.0.1", 49 | "sinon": "^20.0.0", 50 | "supertest": "^7.1.1", 51 | "xo": "^0.60.0" 52 | }, 53 | "engines": { 54 | "node": ">= 18" 55 | }, 56 | "homepage": "https://github.com/koajs/logger", 57 | "bugs": { 58 | "url": "https://github.com/koajs/logger/issues" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // test tools 4 | const { describe, it, before, beforeEach, afterEach } = require('node:test'); 5 | const assert = require('node:assert'); 6 | const sinon = require('sinon'); 7 | const request = require('supertest'); 8 | // test subjects 9 | const chalk = require('chalk'); 10 | 11 | let log; 12 | let sandbox; 13 | let transporter; 14 | let app; 15 | const transporterFunc = { 16 | blankFunc(_, __) { 17 | // blankFunc 18 | } 19 | }; 20 | 21 | describe('koa-logger', () => { 22 | before(() => { 23 | app = require('./test-server')(); 24 | }); 25 | 26 | beforeEach(() => { 27 | sandbox = sinon.createSandbox(); 28 | log = sandbox.spy(console, 'log'); 29 | }); 30 | 31 | afterEach(() => { 32 | sandbox.restore(); 33 | }); 34 | 35 | it('should log a request', async () => { 36 | const response = await request(app.callback()).get('/200'); 37 | 38 | assert.strictEqual(response.status, 200); 39 | assert.strictEqual(response.text, 'hello world'); 40 | assert.strictEqual(log.called, true); 41 | }); 42 | 43 | it('should log a request with correct method and url', async () => { 44 | const response = await request(app.callback()).head('/200'); 45 | 46 | assert.strictEqual(response.status, 200); 47 | assert.ok( 48 | log.calledWith( 49 | ' ' + 50 | chalk.gray('<--') + 51 | ' ' + 52 | chalk.bold('%s') + 53 | ' ' + 54 | chalk.gray('%s'), 55 | 'HEAD', 56 | '/200' 57 | ), 58 | true 59 | ); 60 | }); 61 | 62 | it('should log a response', async () => { 63 | const response = await request(app.callback()).get('/200'); 64 | assert.strictEqual(response.status, 200); 65 | assert.strictEqual(log.callCount, 2); 66 | }); 67 | 68 | it('should log a 200 response', async () => { 69 | const response = await request(app.callback()).get('/200'); 70 | 71 | assert.strictEqual(response.status, 200); 72 | assert.ok( 73 | log.calledWith( 74 | ' ' + 75 | chalk.gray('-->') + 76 | ' ' + 77 | chalk.bold('%s') + 78 | ' ' + 79 | chalk.gray('%s') + 80 | ' ' + 81 | chalk.green('%s') + 82 | ' ' + 83 | chalk.gray('%s') + 84 | ' ' + 85 | chalk.gray('%s'), 86 | 'GET', 87 | '/200', 88 | 200, 89 | sinon.match.any, 90 | '11b' 91 | ), 92 | 2 93 | ); 94 | }); 95 | 96 | it('should log a 200 response for stream', async () => { 97 | const response = await request(app.callback()).get('/200-stream'); 98 | assert.strictEqual(response.status, 200); 99 | 100 | assert.ok( 101 | log.calledWith( 102 | ' ' + 103 | chalk.gray('-->') + 104 | ' ' + 105 | chalk.bold('%s') + 106 | ' ' + 107 | chalk.gray('%s') + 108 | ' ' + 109 | chalk.green('%s') + 110 | ' ' + 111 | chalk.gray('%s') + 112 | ' ' + 113 | chalk.gray('%s'), 114 | 'GET', 115 | '/200-stream', 116 | 200, 117 | sinon.match.any, 118 | sinon.match.string 119 | ) 120 | ); 121 | }); 122 | 123 | it('should log a 301 response', async () => { 124 | const response = await request(app.callback()).get('/301'); 125 | 126 | assert.strictEqual(response.status, 301); 127 | assert.ok( 128 | log.calledWith( 129 | ' ' + 130 | chalk.gray('-->') + 131 | ' ' + 132 | chalk.bold('%s') + 133 | ' ' + 134 | chalk.gray('%s') + 135 | ' ' + 136 | chalk.cyan('%s') + 137 | ' ' + 138 | chalk.gray('%s') + 139 | ' ' + 140 | chalk.gray('%s'), 141 | 'GET', 142 | '/301', 143 | 301, 144 | sinon.match.any, 145 | sinon.match.string 146 | ) 147 | ); 148 | }); 149 | 150 | it('should log a 304 response', async () => { 151 | const response = await request(app.callback()).get('/304'); 152 | 153 | assert.strictEqual(response.status, 304); 154 | assert.ok( 155 | log.calledWith( 156 | ' ' + 157 | chalk.gray('-->') + 158 | ' ' + 159 | chalk.bold('%s') + 160 | ' ' + 161 | chalk.gray('%s') + 162 | ' ' + 163 | chalk.cyan('%s') + 164 | ' ' + 165 | chalk.gray('%s') + 166 | ' ' + 167 | chalk.gray('%s'), 168 | 'GET', 169 | '/304', 170 | 304, 171 | sinon.match.any, 172 | '' 173 | ), 174 | 301 175 | ); 176 | }); 177 | 178 | it('should log a 404 response', async () => { 179 | const response = await request(app.callback()).get('/404'); 180 | assert.strictEqual(response.status, 404); 181 | assert.ok( 182 | log.calledWith( 183 | ' ' + 184 | chalk.gray('-->') + 185 | ' ' + 186 | chalk.bold('%s') + 187 | ' ' + 188 | chalk.gray('%s') + 189 | ' ' + 190 | chalk.yellow('%s') + 191 | ' ' + 192 | chalk.gray('%s') + 193 | ' ' + 194 | chalk.gray('%s'), 195 | 'GET', 196 | '/404', 197 | 404, 198 | sinon.match.any, 199 | '9b' 200 | ), 201 | 404 202 | ); 203 | }); 204 | 205 | it('should log a 500 response', async () => { 206 | const response = await request(app.callback()).get('/500'); 207 | 208 | assert.strictEqual(response.status, 500); 209 | assert.ok( 210 | log.calledWith( 211 | ' ' + 212 | chalk.gray('-->') + 213 | ' ' + 214 | chalk.bold('%s') + 215 | ' ' + 216 | chalk.gray('%s') + 217 | ' ' + 218 | chalk.red('%s') + 219 | ' ' + 220 | chalk.gray('%s') + 221 | ' ' + 222 | chalk.gray('%s'), 223 | 'GET', 224 | '/500', 225 | 500, 226 | sinon.match.any, 227 | '12b' 228 | ), 229 | 500 230 | ); 231 | }); 232 | 233 | it('should log middleware error', async () => { 234 | const response = await request(app.callback()).get('/error'); 235 | 236 | assert.strictEqual(response.status, 500); 237 | assert.ok( 238 | log.calledWith( 239 | ' ' + 240 | chalk.red('xxx') + 241 | ' ' + 242 | chalk.bold('%s') + 243 | ' ' + 244 | chalk.gray('%s') + 245 | ' ' + 246 | chalk.red('%s') + 247 | ' ' + 248 | chalk.gray('%s') + 249 | ' ' + 250 | chalk.gray('%s'), 251 | 'GET', 252 | '/error', 253 | 500, 254 | sinon.match.any, 255 | '-' 256 | ) 257 | ); 258 | }); 259 | 260 | it('should log a 500 response with boom', async () => { 261 | const response = await request(app.callback()).get('/500-boom'); 262 | 263 | assert.strictEqual(response.status, 500); 264 | assert.ok( 265 | log.calledWith( 266 | ' ' + 267 | chalk.red('xxx') + 268 | ' ' + 269 | chalk.bold('%s') + 270 | ' ' + 271 | chalk.gray('%s') + 272 | ' ' + 273 | chalk.red('%s') + 274 | ' ' + 275 | chalk.gray('%s') + 276 | ' ' + 277 | chalk.gray('%s'), 278 | 'GET', 279 | '/500-boom', 280 | 500, 281 | sinon.match.any, 282 | '-' 283 | ) 284 | ); 285 | }); 286 | }); 287 | 288 | describe('koa-logger-transporter-direct', () => { 289 | before(() => { 290 | transporter = function (string, args) { 291 | transporterFunc.blankFunc(string, args); 292 | }; 293 | 294 | app = require('./test-server')(transporter); 295 | }); 296 | 297 | beforeEach(() => { 298 | sandbox = sinon.createSandbox(); 299 | log = sandbox.spy(transporterFunc, 'blankFunc'); 300 | }); 301 | 302 | afterEach(() => { 303 | sandbox.restore(); 304 | }); 305 | 306 | it('should log a request', async () => { 307 | const response = await request(app.callback()).get('/200'); 308 | 309 | assert.strictEqual(response.status, 200); 310 | assert.strictEqual(response.text, 'hello world'); 311 | assert.ok(log.called); 312 | }); 313 | 314 | it('should log a request with correct method and url', async () => { 315 | const response = await request(app.callback()).head('/200'); 316 | 317 | assert.strictEqual(response.status, 200); 318 | assert.ok( 319 | log.calledWith(sinon.match.string, [ 320 | ' ' + 321 | chalk.gray('<--') + 322 | ' ' + 323 | chalk.bold('%s') + 324 | ' ' + 325 | chalk.gray('%s'), 326 | 'HEAD', 327 | '/200' 328 | ]), 329 | 200 330 | ); 331 | }); 332 | 333 | it('should log a response', async () => { 334 | const response = await request(app.callback()).get('/200'); 335 | 336 | assert.strictEqual(response.status, 200); 337 | assert.strictEqual(log.callCount, 2); 338 | }); 339 | 340 | it('should log a 200 response', async () => { 341 | const response = await request(app.callback()).get('/200'); 342 | 343 | assert.strictEqual(response.status, 200); 344 | 345 | assert.ok( 346 | log.calledWith(sinon.match.string, [ 347 | ' ' + 348 | chalk.gray('-->') + 349 | ' ' + 350 | chalk.bold('%s') + 351 | ' ' + 352 | chalk.gray('%s') + 353 | ' ' + 354 | chalk.green('%s') + 355 | ' ' + 356 | chalk.gray('%s') + 357 | ' ' + 358 | chalk.gray('%s'), 359 | 'GET', 360 | '/200', 361 | 200, 362 | sinon.match.any, 363 | '11b' 364 | ]) 365 | ); 366 | }); 367 | 368 | it('should log a 301 response', async () => { 369 | const response = await request(app.callback()).get('/301'); 370 | 371 | assert.strictEqual(response.status, 301); 372 | assert.ok( 373 | log.calledWith(sinon.match.string, [ 374 | ' ' + 375 | chalk.gray('-->') + 376 | ' ' + 377 | chalk.bold('%s') + 378 | ' ' + 379 | chalk.gray('%s') + 380 | ' ' + 381 | chalk.cyan('%s') + 382 | ' ' + 383 | chalk.gray('%s') + 384 | ' ' + 385 | chalk.gray('%s'), 386 | 'GET', 387 | '/301', 388 | 301, 389 | sinon.match.any, 390 | sinon.match.string 391 | ]) 392 | ); 393 | }); 394 | 395 | it('should log a 304 response', async () => { 396 | const response = await request(app.callback()).get('/304'); 397 | 398 | assert.strictEqual(response.status, 304); 399 | assert.ok( 400 | log.calledWith(sinon.match.string, [ 401 | ' ' + 402 | chalk.gray('-->') + 403 | ' ' + 404 | chalk.bold('%s') + 405 | ' ' + 406 | chalk.gray('%s') + 407 | ' ' + 408 | chalk.cyan('%s') + 409 | ' ' + 410 | chalk.gray('%s') + 411 | ' ' + 412 | chalk.gray('%s'), 413 | 'GET', 414 | '/304', 415 | 304, 416 | sinon.match.any, 417 | '' 418 | ]) 419 | ); 420 | }); 421 | 422 | it('should log a 404 response', async () => { 423 | const response = await request(app.callback()).get('/404'); 424 | assert.strictEqual(response.status, 404); 425 | 426 | assert.ok( 427 | log.calledWith(sinon.match.string, [ 428 | ' ' + 429 | chalk.gray('-->') + 430 | ' ' + 431 | chalk.bold('%s') + 432 | ' ' + 433 | chalk.gray('%s') + 434 | ' ' + 435 | chalk.yellow('%s') + 436 | ' ' + 437 | chalk.gray('%s') + 438 | ' ' + 439 | chalk.gray('%s'), 440 | 'GET', 441 | '/404', 442 | 404, 443 | sinon.match.any, 444 | '9b' 445 | ]) 446 | ); 447 | }); 448 | 449 | it('should log a 500 response', async () => { 450 | const response = await request(app.callback()).get('/500'); 451 | 452 | assert.strictEqual(response.status, 500); 453 | 454 | assert.ok( 455 | log.calledWith(sinon.match.string, [ 456 | ' ' + 457 | chalk.gray('-->') + 458 | ' ' + 459 | chalk.bold('%s') + 460 | ' ' + 461 | chalk.gray('%s') + 462 | ' ' + 463 | chalk.red('%s') + 464 | ' ' + 465 | chalk.gray('%s') + 466 | ' ' + 467 | chalk.gray('%s'), 468 | 'GET', 469 | '/500', 470 | 500, 471 | sinon.match.any, 472 | '12b' 473 | ]) 474 | ); 475 | }); 476 | 477 | it('should log middleware error', async () => { 478 | const response = await request(app.callback()).get('/error'); 479 | 480 | assert.strictEqual(response.status, 500); 481 | assert.ok( 482 | log.calledWith(sinon.match.string, [ 483 | ' ' + 484 | chalk.red('xxx') + 485 | ' ' + 486 | chalk.bold('%s') + 487 | ' ' + 488 | chalk.gray('%s') + 489 | ' ' + 490 | chalk.red('%s') + 491 | ' ' + 492 | chalk.gray('%s') + 493 | ' ' + 494 | chalk.gray('%s'), 495 | 'GET', 496 | '/error', 497 | 500, 498 | sinon.match.any, 499 | '-' 500 | ]) 501 | ); 502 | }); 503 | 504 | it('should log a 500 response with boom', async () => { 505 | const response = await request(app.callback()).get('/500-boom'); 506 | 507 | assert.strictEqual(response.status, 500); 508 | 509 | assert.ok( 510 | log.calledWith(sinon.match.string, [ 511 | ' ' + 512 | chalk.red('xxx') + 513 | ' ' + 514 | chalk.bold('%s') + 515 | ' ' + 516 | chalk.gray('%s') + 517 | ' ' + 518 | chalk.red('%s') + 519 | ' ' + 520 | chalk.gray('%s') + 521 | ' ' + 522 | chalk.gray('%s'), 523 | 'GET', 524 | '/500-boom', 525 | 500, 526 | sinon.match.any, 527 | '-' 528 | ]) 529 | ); 530 | }); 531 | }); 532 | 533 | describe('koa-logger-transporter-opts', () => { 534 | before(() => { 535 | transporter = function (string, args) { 536 | transporterFunc.blankFunc(string, args); 537 | }; 538 | 539 | app = require('./test-server')({ transporter }); 540 | }); 541 | 542 | beforeEach(() => { 543 | sandbox = sinon.createSandbox(); 544 | log = sandbox.spy(transporterFunc, 'blankFunc'); 545 | }); 546 | 547 | afterEach(() => { 548 | sandbox.restore(); 549 | }); 550 | 551 | it('should log a request', async () => { 552 | const response = await request(app.callback()).get('/200'); 553 | 554 | assert.strictEqual(response.status, 200); 555 | assert.strictEqual(response.text, 'hello world'); 556 | 557 | assert.strictEqual(log.called, true); 558 | }); 559 | 560 | it('should log a request with correct method and url', async () => { 561 | const response = await request(app.callback()).head('/200'); 562 | 563 | assert.strictEqual(response.status, 200); 564 | 565 | assert.ok( 566 | log.calledWith(sinon.match.string, [ 567 | ' ' + 568 | chalk.gray('<--') + 569 | ' ' + 570 | chalk.bold('%s') + 571 | ' ' + 572 | chalk.gray('%s'), 573 | 'HEAD', 574 | '/200' 575 | ]) 576 | ); 577 | }); 578 | 579 | it('should log a response', async () => { 580 | const response = await request(app.callback()).get('/200'); 581 | assert.strictEqual(response.status, 200); 582 | 583 | assert.strictEqual(log.callCount, 2); 584 | }); 585 | 586 | it('should log a 200 response', async () => { 587 | const response = await request(app.callback()).get('/200'); 588 | assert.strictEqual(response.status, 200); 589 | 590 | assert.ok( 591 | log.calledWith(sinon.match.string, [ 592 | ' ' + 593 | chalk.gray('-->') + 594 | ' ' + 595 | chalk.bold('%s') + 596 | ' ' + 597 | chalk.gray('%s') + 598 | ' ' + 599 | chalk.green('%s') + 600 | ' ' + 601 | chalk.gray('%s') + 602 | ' ' + 603 | chalk.gray('%s'), 604 | 'GET', 605 | '/200', 606 | 200, 607 | sinon.match.any, 608 | '11b' 609 | ]) 610 | ); 611 | }); 612 | 613 | it('should log a 301 response', async () => { 614 | const response = await request(app.callback()).get('/301'); 615 | 616 | assert.strictEqual(response.status, 301); 617 | assert.ok( 618 | log.calledWith(sinon.match.string, [ 619 | ' ' + 620 | chalk.gray('-->') + 621 | ' ' + 622 | chalk.bold('%s') + 623 | ' ' + 624 | chalk.gray('%s') + 625 | ' ' + 626 | chalk.cyan('%s') + 627 | ' ' + 628 | chalk.gray('%s') + 629 | ' ' + 630 | chalk.gray('%s'), 631 | 'GET', 632 | '/301', 633 | 301, 634 | sinon.match.any, 635 | sinon.match.string 636 | ]) 637 | ); 638 | }); 639 | 640 | it('should log a 304 response', async () => { 641 | const response = await request(app.callback()).get('/304'); 642 | 643 | assert.strictEqual(response.status, 304); 644 | assert.ok( 645 | log.calledWith(sinon.match.string, [ 646 | ' ' + 647 | chalk.gray('-->') + 648 | ' ' + 649 | chalk.bold('%s') + 650 | ' ' + 651 | chalk.gray('%s') + 652 | ' ' + 653 | chalk.cyan('%s') + 654 | ' ' + 655 | chalk.gray('%s') + 656 | ' ' + 657 | chalk.gray('%s'), 658 | 'GET', 659 | '/304', 660 | 304, 661 | sinon.match.any, 662 | '' 663 | ]) 664 | ); 665 | }); 666 | 667 | it('should log a 404 response', async () => { 668 | const response = await request(app.callback()).get('/404'); 669 | 670 | assert.strictEqual(response.status, 404); 671 | 672 | assert.ok( 673 | log.calledWith(sinon.match.string, [ 674 | ' ' + 675 | chalk.gray('-->') + 676 | ' ' + 677 | chalk.bold('%s') + 678 | ' ' + 679 | chalk.gray('%s') + 680 | ' ' + 681 | chalk.yellow('%s') + 682 | ' ' + 683 | chalk.gray('%s') + 684 | ' ' + 685 | chalk.gray('%s'), 686 | 'GET', 687 | '/404', 688 | 404, 689 | sinon.match.any, 690 | '9b' 691 | ]) 692 | ); 693 | }); 694 | 695 | it('should log a 500 response', async () => { 696 | const response = await request(app.callback()).get('/500'); 697 | 698 | assert.strictEqual(response.status, 500); 699 | 700 | assert.ok( 701 | log.calledWith(sinon.match.string, [ 702 | ' ' + 703 | chalk.gray('-->') + 704 | ' ' + 705 | chalk.bold('%s') + 706 | ' ' + 707 | chalk.gray('%s') + 708 | ' ' + 709 | chalk.red('%s') + 710 | ' ' + 711 | chalk.gray('%s') + 712 | ' ' + 713 | chalk.gray('%s'), 714 | 'GET', 715 | '/500', 716 | 500, 717 | sinon.match.any, 718 | '12b' 719 | ]) 720 | ); 721 | }); 722 | 723 | it('should log middleware error', async () => { 724 | const response = await request(app.callback()).get('/error'); 725 | 726 | assert.strictEqual(response.status, 500); 727 | assert.ok( 728 | log.calledWith(sinon.match.string, [ 729 | ' ' + 730 | chalk.red('xxx') + 731 | ' ' + 732 | chalk.bold('%s') + 733 | ' ' + 734 | chalk.gray('%s') + 735 | ' ' + 736 | chalk.red('%s') + 737 | ' ' + 738 | chalk.gray('%s') + 739 | ' ' + 740 | chalk.gray('%s'), 741 | 'GET', 742 | '/error', 743 | 500, 744 | sinon.match.any, 745 | '-' 746 | ]) 747 | ); 748 | }); 749 | 750 | it('should log a 500 response with boom', async () => { 751 | const response = await request(app.callback()).get('/500-boom'); 752 | assert.strictEqual(response.status, 500); 753 | 754 | assert.ok( 755 | log.calledWith(sinon.match.string, [ 756 | ' ' + 757 | chalk.red('xxx') + 758 | ' ' + 759 | chalk.bold('%s') + 760 | ' ' + 761 | chalk.gray('%s') + 762 | ' ' + 763 | chalk.red('%s') + 764 | ' ' + 765 | chalk.gray('%s') + 766 | ' ' + 767 | chalk.gray('%s'), 768 | 'GET', 769 | '/500-boom', 770 | 500, 771 | sinon.match.any, 772 | '-' 773 | ]) 774 | ); 775 | }); 776 | }); 777 | -------------------------------------------------------------------------------- /test/test-server.js: -------------------------------------------------------------------------------- 1 | const { Readable } = require('node:stream'); 2 | const Koa = require('koa'); 3 | const Boom = require('boom'); 4 | const _ = require('koa-route'); 5 | const logger = require('..'); 6 | 7 | function testServer(options) { 8 | const app = new Koa(); 9 | app.use(logger(options)); 10 | 11 | app.use( 12 | _.get('/200', (ctx) => { 13 | ctx.status = 200; 14 | ctx.body = 'hello world'; 15 | }) 16 | ); 17 | 18 | app.use( 19 | _.get('/200-stream', (ctx) => { 20 | ctx.status = 200; 21 | ctx.body = Readable.from(['hello world']); 22 | }) 23 | ); 24 | 25 | app.use( 26 | _.get('/301', (ctx) => { 27 | ctx.status = 301; 28 | }) 29 | ); 30 | 31 | app.use( 32 | _.get('/304', (ctx) => { 33 | ctx.status = 304; 34 | }) 35 | ); 36 | 37 | app.use( 38 | _.get('/404', (ctx) => { 39 | ctx.status = 404; 40 | ctx.body = 'not found'; 41 | }) 42 | ); 43 | 44 | app.use( 45 | _.get('/500', (ctx) => { 46 | ctx.status = 500; 47 | ctx.body = 'server error'; 48 | }) 49 | ); 50 | 51 | app.use( 52 | _.get('/500-boom', (ctx) => { 53 | ctx.throw(Boom.badImplementation('terrible implementation')); 54 | }) 55 | ); 56 | 57 | app.use( 58 | _.get('/error', () => { 59 | throw new Error('oh no'); 60 | }) 61 | ); 62 | 63 | return app; 64 | } 65 | 66 | module.exports = testServer; 67 | --------------------------------------------------------------------------------