├── .gitignore ├── package.json ├── LICENSE ├── index.js ├── README.md └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "morgan-json", 3 | "version": "1.1.0", 4 | "description": "A variant of `morgan.compile` that provides format functions that output JSON", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "istanbul cover _mocha test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/indexzero/morgan-json.git" 12 | }, 13 | "keywords": [ 14 | "morgan", 15 | "json", 16 | "logging" 17 | ], 18 | "author": "Charlie Robbins ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/indexzero/morgan-json/issues" 22 | }, 23 | "homepage": "https://github.com/indexzero/morgan-json#readme", 24 | "devDependencies": { 25 | "assume": "^1.4.1", 26 | "istanbul": "^0.4.5", 27 | "mocha": "^3.1.2", 28 | "morgan": "^1.7.0" 29 | }, 30 | "dependencies": { 31 | "diagnostics": "^1.0.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Charlie Robbins 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 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('diagnostics')('morgan-json'); 4 | 5 | /** 6 | * Compile a `morgan` format string into a `morgan` format function 7 | * that returns JSON. 8 | * 9 | * Adopted from `morgan.compile` from `morgan` under MIT. 10 | * 11 | * @param {string|Object} format 12 | * @param {Object} opts Options for how things are returned. 13 | * - 'stringify': (default: true) If false returns an object literal 14 | * @return {function} 15 | * @public 16 | */ 17 | module.exports = function compile (format, opts) { 18 | if (format === '') { 19 | throw new Error('argument format string must not be empty'); 20 | } 21 | 22 | if (typeof format !== 'string') { 23 | return compileObject(format, opts); 24 | } 25 | 26 | opts = opts || {}; 27 | 28 | var fmt = format.replace(/"/g, '\\"'); 29 | var stringify = opts.stringify !== false ? 'JSON.stringify' : ''; 30 | var js = ' "use strict"\n return ' + stringify + '({' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?([^:]+)?/g, function (_, name, arg, trail, offset, str) { 31 | var tokenName = String(JSON.stringify(name)); 32 | var tokenArguments = 'req, res'; 33 | var tokenFunction = 'tokens[' + tokenName + ']'; 34 | var trailer = (trail || '').trimRight(); 35 | 36 | if (arg !== undefined) { 37 | tokenArguments += ', ' + String(JSON.stringify(arg)); 38 | } 39 | 40 | return '\n ' + tokenName + ': (' + tokenFunction + '(' + tokenArguments + ') || "-") + ' + JSON.stringify(trailer) + ',' 41 | }) + '\n })'; 42 | 43 | debug('\n%s', js); 44 | 45 | // eslint-disable-next-line no-new-func 46 | return new Function('tokens, req, res', js); 47 | } 48 | 49 | /** 50 | * Compile an Object with keys as `morgan` format strings into a `morgan` format function 51 | * that returns JSON. The JSON returned will have the same keys as the format Object. 52 | * 53 | * Adopted from `morgan.compile` from `morgan` under MIT. 54 | * 55 | * @param {string|Object} format 56 | * @param {Object} opts Options for how things are returned. 57 | * - 'stringify': (default: true) If false returns an object literal 58 | * @return {function} 59 | * @public 60 | */ 61 | function compileObject (format, opts) { 62 | if (!format || typeof format !== 'object') { 63 | throw new Error('argument format must be a string or an object'); 64 | } 65 | 66 | opts = opts || {}; 67 | 68 | var keys = Object.keys(format); 69 | var stringify = opts.stringify !== false ? 'JSON.stringify' : ''; 70 | var js = ' "use strict"\n return ' + stringify + '({' + keys.map(function (key, i) { 71 | var assignment = '\n "' + key + '": "' + format[key].replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function (_, name, arg) { 72 | var tokenArguments = 'req, res'; 73 | var tokenFunction = 'tokens[' + String(JSON.stringify(name)) + ']'; 74 | 75 | if (arg !== undefined) { 76 | tokenArguments += ', ' + String(JSON.stringify(arg)); 77 | } 78 | 79 | return '" + (' + tokenFunction + '(' + tokenArguments + ') || "-") + "'; 80 | }) + '"'; 81 | 82 | return assignment; 83 | }) + '\n })'; 84 | 85 | debug('\n%s', js); 86 | 87 | // eslint-disable-next-line no-new-func 88 | return new Function('tokens, req, res', js); 89 | }; 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # morgan-json 2 | 3 | A variant of `morgan.compile` that provides format functions that output JSON 4 | 5 | ## Usage 6 | 7 | ``` js 8 | const json = require('morgan-json'); 9 | // json(string, opts); 10 | // json(object, opts); 11 | ``` 12 | 13 | To put that into a real world example: 14 | 15 | ``` js 16 | const morgan = require('morgan'); 17 | const express = require('express'); 18 | const json = require('morgan-json'); 19 | 20 | const app = express() 21 | const format = json({ 22 | short: ':method :url :status', 23 | length: ':res[content-length]', 24 | 'response-time': ':response-time ms' 25 | }); 26 | 27 | app.use(morgan(format)); 28 | app.get('/', function (req, res) { 29 | res.send('hello, world!') 30 | }); 31 | ``` 32 | 33 | When requests to this `express` application come in `morgan` will output a JSON object that looks 34 | like: 35 | 36 | ``` 37 | {"short":"GET / 200","length":200,"response-time":"2 ms"} 38 | ``` 39 | 40 | ### Format objects 41 | 42 | When provided with an object, `morgan-json` returns a function that will output JSON with keys 43 | for each of the keys in that object. The value for each key will be the result of evaluating each 44 | format string in the object provided. For example: 45 | 46 | ``` js 47 | const morgan = require('morgan'); 48 | const json = require('morgan-json'); 49 | 50 | const format = json({ 51 | short: ':method :url :status', 52 | length: ':res[content-length]', 53 | 'response-time': ':response-time ms' 54 | }); 55 | 56 | app.use(morgan(format)); 57 | ``` 58 | 59 | Will output a JSON object that has keys `short`, `length` and `response-time`: 60 | 61 | ``` 62 | {"short":"GET / 200","length":200,"response-time":"2 ms"} 63 | ``` 64 | 65 | ### Format strings 66 | 67 | When provided with a format string, `morgan-json` returns a function that outputs JSON with keys 68 | for each of the named tokens within the string provided. Any characters trailing after a token 69 | will be included in the value for that key in JSON. For example: 70 | 71 | ``` js 72 | const morgan = require('morgan'); 73 | const json = require('morgan-json'); 74 | 75 | const format = json(':method :url :status :res[content-length] bytes :response-time ms'); 76 | 77 | app.use(morgan(format)); 78 | ``` 79 | 80 | Will output a JSON object that has keys `method`, `url`, `status`, `res` and `response-time`: 81 | 82 | ``` 83 | {"method":"GET","url":"/","status":"200","res":"10 bytes","response-time":"2 ms"} 84 | ``` 85 | 86 | ### Returning strings vs. Objects 87 | 88 | By default functions returned by `morgan-json` will return strings from `JSON.stringify`. In some 89 | cases you may want object literals (e.g. if you perform stringification in another layer of your logger). In this case just provide `{ stringify: false }`: 90 | 91 | ``` js 92 | ``` js 93 | const morgan = require('morgan'); 94 | const winston = require('winston'); 95 | const json = require('morgan-json'); 96 | 97 | const format = json(':method :url :status', { stringify: false }); 98 | 99 | app.use(morgan(format, { 100 | stream: { 101 | write: function (obj) { 102 | winston.info(obj); 103 | } 104 | } 105 | })); 106 | ``` 107 | 108 | Will output a JSON object that has keys 109 | ``` 110 | 111 | ## Tests 112 | 113 | ``` 114 | npm test 115 | ``` 116 | 117 | ##### LICENSE: MIT 118 | ##### AUTHOR: [Charlie Robbins](https://github.com/indexzero) 119 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assume = require('assume'); 4 | var morgan = require('morgan'); 5 | var json = require('./'); 6 | 7 | // 8 | // A simple mock "morgan" object which returns deterministic 9 | // output from the defined functions. 10 | // 11 | var mock = { 12 | method: function () { return 'method' }, 13 | url: function () { return 'url' }, 14 | status: function () { return 'status' }, 15 | res: function (req, res, arg) { return ['res', arg].join(' ') }, 16 | 'response-time': function () { return 'response-time' }, 17 | }; 18 | 19 | // 20 | // Invalid argument message that morgan-json outputs. 21 | // 22 | var invalidMsg = 'argument format must be a string or an object'; 23 | 24 | describe('morgan-json', function () { 25 | it('format string of all tokens', function () { 26 | var compiled = json(':method :url :status :res[content-length] :response-time'); 27 | var output = compiled(mock); 28 | 29 | assume(output).deep.equals(JSON.stringify({ 30 | method: 'method', 31 | url: 'url', 32 | status: 'status', 33 | res: 'res content-length', 34 | 'response-time': 'response-time' 35 | })); 36 | }); 37 | 38 | it('format string of all tokens (with trailers)', function () { 39 | var compiled = json(':method :url :status :res[content-length] bytes :response-time ms'); 40 | var output = compiled(mock); 41 | 42 | assume(output).deep.equals(JSON.stringify({ 43 | method: 'method', 44 | url: 'url', 45 | status: 'status', 46 | res: 'res content-length bytes', 47 | 'response-time': 'response-time ms' 48 | })); 49 | }); 50 | 51 | it('format object of all single tokens (no trailers)', function () { 52 | var compiled = json({ 53 | method: ':method', 54 | url: ':url', 55 | status: ':status', 56 | 'response-time': ':response-time', 57 | length: ':res[content-length]' 58 | }); 59 | 60 | var output = compiled(mock); 61 | assume(output).deep.equals(JSON.stringify({ 62 | method: 'method', 63 | url: 'url', 64 | status: 'status', 65 | 'response-time': 'response-time', 66 | length: 'res content-length' 67 | })) 68 | }); 69 | 70 | it('format object with multiple tokens', function () { 71 | var compiled = json({ 72 | short: ':method :url :status', 73 | 'response-time': ':response-time', 74 | length: ':res[content-length]' 75 | }); 76 | 77 | var output = compiled(mock); 78 | assume(output).deep.equals(JSON.stringify({ 79 | short: 'method url status', 80 | 'response-time': 'response-time', 81 | length: 'res content-length' 82 | })); 83 | }); 84 | 85 | it('format object of all tokens (with trailers)', function () { 86 | var compiled = json({ 87 | method: 'GET :method', 88 | url: '-> /:url', 89 | status: 'Code :status', 90 | 'response-time': ':response-time ms', 91 | length: ':res[content-length]' 92 | }); 93 | 94 | var output = compiled(mock); 95 | assume(output).deep.equals(JSON.stringify({ 96 | method: 'GET method', 97 | url: '-> /url', 98 | status: 'Code status', 99 | 'response-time': 'response-time ms', 100 | length: 'res content-length' 101 | })); 102 | }); 103 | 104 | describe('{ stringify: false }', function () { 105 | it('format object returns an object', function () { 106 | var compiled = json({ 107 | short: ':method :url :status', 108 | 'response-time': ':response-time', 109 | length: ':res[content-length]' 110 | }, { stringify: false }); 111 | 112 | var output = compiled(mock); 113 | assume(output).is.an('object'); 114 | assume(output).deep.equals({ 115 | short: 'method url status', 116 | 'response-time': 'response-time', 117 | length: 'res content-length' 118 | }); 119 | }); 120 | 121 | it('format string returns an object', function () { 122 | var compiled = json(':method :url :status', { stringify: false }); 123 | 124 | var output = compiled(mock); 125 | assume(output).is.an('object'); 126 | assume(output).deep.equals({ 127 | method: 'method', 128 | url: 'url', 129 | status: 'status' 130 | }); 131 | }); 132 | }); 133 | 134 | describe('Invalid arguments', function () { 135 | it('throws with null', function () { 136 | assume(function () { json(null); }).throws(invalidMsg); 137 | }); 138 | 139 | it('throws with Boolean', function () { 140 | assume(function () { json(false); }).throws(invalidMsg); 141 | assume(function () { json(true); }).throws(invalidMsg); 142 | }); 143 | 144 | it('throws with Number', function () { 145 | assume(function () { json(0); }).throws(invalidMsg); 146 | assume(function () { json(1); }).throws(invalidMsg); 147 | assume(function () { json(Number.MAX_VALUE); }).throws(invalidMsg); 148 | assume(function () { json(Number.POSITIVE_INFINITY); }).throws(invalidMsg); 149 | }); 150 | 151 | it('throws with empty string', function () { 152 | assume(function () { json(''); }).throws('argument format string must not be empty'); 153 | }); 154 | }); 155 | }); 156 | --------------------------------------------------------------------------------