├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin.js ├── example.js ├── index.js ├── package.json └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 8 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Irina Shestak 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 | # pino-gris 🍇 2 | 3 | [![npm version][1]][2] [![build status][3]][4] 4 | [![downloads][5]][6] [![js-standard-style][7]][8] 5 | 6 | A verbose [ndjson](http://ndjson.org) log formatter for [pino](https://github.com/pinojs/pino). 7 | 8 | ![screenshot](./screenshot.png) 9 | 10 | [1]: https://img.shields.io/npm/v/pino-gris.svg?style=flat-square 11 | [2]: https://npmjs.org/package/pino-gris 12 | [3]: https://img.shields.io/travis/ungoldman/pino-gris/master.svg?style=flat-square 13 | [4]: https://travis-ci.org/ungoldman/pino-gris 14 | [5]: http://img.shields.io/npm/dm/pino-gris.svg?style=flat-square 15 | [6]: https://npmjs.org/package/pino-gris 16 | [7]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 17 | [8]: https://github.com/feross/standard 18 | 19 | **Note:** this is a fork of [`pino-colada`][pino-colada]. 20 | The main difference is that `pino-gris` does more verbose logging of objects. 21 | 22 | ## Install 23 | 24 | ``` 25 | npm install pino-gris 26 | ``` 27 | 28 | ## Usage 29 | 30 | Pipe any `pino` output into `pino-gris` for logging. 31 | 32 | ```bash 33 | node server.js | pino-gris 34 | ``` 35 | 36 | After parsing input from `server.js`, `pino-gris` returns a stream and pipes it 37 | over to `process.stdout`. It will output a timestamp, a log level in the form of 38 | an emoji, a message, and any extra data supplied in the first argument. 39 | 40 | ``` 41 | > log.fatal(new Error('Aaaaaauugh'), 'Someone is dead!') 42 | 43 | 13:14:32 💀 test Someone is dead! 44 | 45 | type: Error 46 | stack: Error: Aaaaaauugh 47 | at Object. (/Users/ng/dev/modules/pino-gris/example.js:8:11) 48 | at Module._compile (module.js:652:30) 49 | at Object.Module._extensions..js (module.js:663:10) 50 | at Module.load (module.js:565:32) 51 | at tryModuleLoad (module.js:505:12) 52 | at Function.Module._load (module.js:497:3) 53 | at Function.Module.runMain (module.js:693:10) 54 | at startup (bootstrap_node.js:188:16) 55 | at bootstrap_node.js:609:3 56 | ``` 57 | 58 | The main difference between this and [`pino-colada`][pino-colada] is that it will output _any_ key attached to the `pino` log object that isn't included in the following list: 59 | 60 | ```js 61 | const pinoKeys = [ 62 | 'level', 63 | 'time', 64 | 'msg', 65 | 'message', 66 | 'pid', 67 | 'hostname', 68 | 'name', 69 | 'ns', 70 | 'v', 71 | 'req', 72 | 'res', 73 | 'statusCode', 74 | 'responseTime', 75 | 'elapsed', 76 | 'method', 77 | 'contentLength', 78 | 'url' 79 | ] 80 | ``` 81 | 82 | These are all the keys that were already being processed by `pino-colada`. So anything that falls out of this list will also get printed. 83 | 84 | This means error stack traces, objects, arrays, or anything else will get logged. 85 | 86 | ### Example Output 87 | 88 | For live sample output, try running `npm start` in this repo. 89 | 90 | ``` 91 | > node example.js | ./bin.js 92 | 93 | 13:14:32 💀 test Someone is dead! 94 | 95 | type: Error 96 | stack: Error: Aaaaaauugh 97 | at Object. (/Users/ng/dev/modules/pino-gris/example.js:8:11) 98 | at Module._compile (module.js:652:30) 99 | at Object.Module._extensions..js (module.js:663:10) 100 | at Module.load (module.js:565:32) 101 | at tryModuleLoad (module.js:505:12) 102 | at Function.Module._load (module.js:497:3) 103 | at Function.Module.runMain (module.js:693:10) 104 | at startup (bootstrap_node.js:188:16) 105 | at bootstrap_node.js:609:3 106 | 107 | 13:14:32 🚨 test What really happened? 108 | 109 | type: Error 110 | stack: Error: Perhaps we'll never know 111 | at Object. (/Users/ng/dev/modules/pino-gris/example.js:9:11) 112 | at Module._compile (module.js:652:30) 113 | at Object.Module._extensions..js (module.js:663:10) 114 | at Module.load (module.js:565:32) 115 | at tryModuleLoad (module.js:505:12) 116 | at Function.Module._load (module.js:497:3) 117 | at Function.Module.runMain (module.js:693:10) 118 | at startup (bootstrap_node.js:188:16) 119 | at bootstrap_node.js:609:3 120 | 121 | 13:14:32 🔍 test Interrogating suspects 122 | 123 | 0: Colonel Mustard 124 | 1: Miss Scarlet 125 | 2: Mr. Green 126 | 3: Mrs. Peacock 127 | 4: Mrs. White 128 | 5: Professor Plum 129 | 130 | 13:14:32 ⚠️ test Gathering evidence 131 | 132 | evidence: { 133 | "weapon": "Candlestick", 134 | "location": "Library", 135 | "suspect": "Colonel Mustard" 136 | } 137 | 138 | 13:14:32 ✨ test Justice is served 139 | 140 | justice: true 141 | 142 | ``` 143 | 144 | ### Verbose Mode 145 | 146 | For _extremely verbose_ formatted output, you can use the `-v` flag. This will print _all_ properties of the log object. 147 | 148 | A line like this... 149 | 150 | ```js 151 | log.info({ justice: true }, 'Justice is served') 152 | ``` 153 | 154 | Whose raw output looks like this... 155 | 156 | ``` 157 | {"level":30,"time":1563400958346,"msg":"Justice is served","pid":3902,"hostname":"quant.hsd1.or.comcast.net","name":"test","justice":true,"v":1} 158 | ``` 159 | 160 | When fed to `pino-gris` like this... 161 | 162 | ```sh 163 | output | pino-gris -v 164 | ``` 165 | 166 | Will be formatted like this: 167 | 168 | ``` 169 | 14:59:23 ✨ test Justice is served 170 | 171 | level: info 172 | time: 1563400763379 173 | msg: Justice is served 174 | pid: 3737 175 | hostname: quant.hsd1.or.comcast.net 176 | name: test 177 | justice: true 178 | v: 1 179 | message: Justice is served 180 | ns: 181 | ``` 182 | 183 | ### Nota Bene 184 | 185 | Be careful how you use `pino`! It will do very different things depending on the order of arguments. 186 | 187 | Example: 188 | 189 | ``` 190 | > log.error(new Error('error text'), 'message text') 191 | 192 | {"level":50,"time":1523650090921,"msg":"message text","pid":63152,"hostname":"quant.local","name":"test","type":"Error","stack":"Error: error text\n at repl:1:9\n at ContextifyScript.Script.runInThisContext (vm.js:50:33)\n at REPLServer.defaultEval (repl.js:240:29)\n at bound (domain.js:301:14)\n at REPLServer.runBound [as eval] (domain.js:314:12)\n at REPLServer.onLine (repl.js:468:10)\n at emitOne (events.js:121:20)\n at REPLServer.emit (events.js:211:7)\n at REPLServer.Interface._onLine (readline.js:282:10)\n at REPLServer.Interface._line (readline.js:631:8)","v":1} 193 | 194 | > log.error('message text', new Error('error text')) 195 | 196 | {"level":50,"time":1523650105577,"msg":"message text {}","pid":63152,"hostname":"quant.local","name":"test","v":1} 197 | ``` 198 | 199 | In the first case above, the error's stack trace is merged onto the object, but the error's message is only visible in `stack`. 200 | 201 | In the second case above, the error's stack and message properties are completely lost! 202 | 203 | So if you want to preserve any important information from an object, always pass it first. 204 | 205 | Also note `pino` will do weird things with key collisions, like so: 206 | 207 | ``` 208 | > log.trace({ v: 2 }) 209 | {"level":10,"time":1523650319601,"pid":63152,"hostname":"quant.local","name":"test","v":2,"v":1} 210 | ``` 211 | 212 | Notice there are two `v` keys above now! 🤔 213 | 214 | ## Related content 215 | - [pino-colada][pino-colada] 216 | - [pino](https://github.com/pinojs/pino) 217 | - [merry](https://github.com/shipharbor/merry) 218 | - [garnish](https://github.com/mattdesl/garnish) 219 | - [@studio/log](https://github.com/javascript-studio/studio-log) 220 | - [pino-http](https://github.com/pinojs/pino-http) 221 | - [hapi-pino](https://github.com/pinojs/hapi-pino) 222 | 223 | [pino-colada]: https://github.com/lrlna/pino-colada 224 | 225 | ## License 226 | [MIT](https://tldrlegal.com/license/mit-license) 227 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | var pinoGris = require('./')() 3 | var input = process.stdin 4 | var output = process.stdout 5 | 6 | input 7 | .pipe(pinoGris) 8 | .pipe(output) 9 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var pino = require('pino') 2 | 3 | var log = pino({ 4 | name: 'test', 5 | level: 'trace' 6 | }) 7 | 8 | log.fatal(new Error('Aaaaaauugh'), 'Someone is dead!') 9 | log.trace({ 10 | stack: (() => { 11 | let arr = [] 12 | while (arr.length < 250) { 13 | arr.push('😱') 14 | } 15 | return arr.join('\n') 16 | })() 17 | }, 'Endless screaming detected') 18 | log.error(new Error('Perhaps we\'ll never know'), 'What really happened?') 19 | log.trace([ 20 | 'Colonel Mustard', 21 | 'Miss Scarlet', 22 | 'Mr. Green', 23 | 'Mrs. Peacock', 24 | 'Mrs. White', 25 | 'Professor Plum' 26 | ], 'Interrogating suspects') 27 | log.warn({ 28 | evidence: { 29 | weapon: 'Candlestick', 30 | location: 'Library', 31 | suspect: 'Colonel Mustard' 32 | } 33 | }, 'Gathering evidence') 34 | log.info({ justice: true }, 'Justice is served') 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var prettyBytes = require('prettier-bytes') 2 | var jsonParse = require('fast-json-parse') 3 | var prettyMs = require('pretty-ms') 4 | var padLeft = require('pad-left') 5 | var indent = require('indent') 6 | var split = require('split2') 7 | var chalk = require('chalk') 8 | 9 | var nl = '\n' 10 | var emojiLog = { 11 | warn: '⚠️', 12 | info: '✨', 13 | error: '🚨', 14 | debug: '🐛', 15 | fatal: '💀', 16 | trace: '🔍' 17 | } 18 | var lineLimit = 100 19 | var pinoKeys = [ 20 | 'level', 21 | 'time', 22 | 'msg', 23 | 'message', 24 | 'pid', 25 | 'hostname', 26 | 'name', 27 | 'ns', 28 | 'v', 29 | 'req', 30 | 'res', 31 | 'statusCode', 32 | 'responseTime', 33 | 'elapsed', 34 | 'method', 35 | 'contentLength', 36 | 'url' 37 | ] 38 | var verbose = process.argv.includes('-v') 39 | 40 | module.exports = PinoGris 41 | 42 | function PinoGris () { 43 | return split(parse) 44 | } 45 | 46 | function parse (line) { 47 | var obj = jsonParse(line) 48 | if (!obj.value || obj.err) return line + nl 49 | obj = obj.value 50 | 51 | if (!obj.level) return line + nl 52 | if (typeof obj.level === 'number') convertLogNumber(obj) 53 | 54 | return output(obj) + nl 55 | } 56 | 57 | function convertLogNumber (obj) { 58 | if (obj.level === 10) obj.level = 'trace' 59 | if (obj.level === 20) obj.level = 'debug' 60 | if (obj.level === 30) obj.level = 'info' 61 | if (obj.level === 40) obj.level = 'warn' 62 | if (obj.level === 50) obj.level = 'error' 63 | if (obj.level === 60) obj.level = 'fatal' 64 | } 65 | 66 | function output (obj) { 67 | var output = [] 68 | 69 | if (!obj.message) obj.message = obj.msg 70 | if (!obj.level) obj.level = 'userlvl' 71 | if (!obj.name) obj.name = '' 72 | if (!obj.ns) obj.ns = '' 73 | 74 | output.push(formatDate()) 75 | output.push(formatLevel(obj.level)) 76 | output.push(formatNs(obj.ns)) 77 | output.push(formatName(obj.name)) 78 | output.push(formatMessage(obj)) 79 | 80 | var req = obj.req 81 | var res = obj.res 82 | var statusCode = (res) ? res.statusCode : obj.statusCode 83 | var responseTime = obj.responseTime || obj.elapsed 84 | var method = (req) ? req.method : obj.method 85 | var contentLength = obj.contentLength 86 | var url = (req) ? req.url : obj.url 87 | 88 | if (method != null) { 89 | output.push(formatMethod(method)) 90 | output.push(formatStatusCode(statusCode)) 91 | } 92 | if (url != null) output.push(formatUrl(url)) 93 | if (contentLength != null) output.push(formatBundleSize(contentLength)) 94 | if (responseTime != null) output.push(formatLoadTime(responseTime)) 95 | output.push(formatExtra(obj)) 96 | 97 | return output.filter(noEmpty).join(' ') 98 | } 99 | 100 | function formatDate () { 101 | var date = new Date() 102 | var hours = padLeft(date.getHours().toString(), 2, '0') 103 | var minutes = padLeft(date.getMinutes().toString(), 2, '0') 104 | var seconds = padLeft(date.getSeconds().toString(), 2, '0') 105 | var prettyDate = hours + ':' + minutes + ':' + seconds 106 | return chalk.gray(prettyDate) 107 | } 108 | 109 | function formatLevel (level) { 110 | const emoji = emojiLog[level] 111 | const padding = isWideEmoji(emoji) ? '' : ' ' 112 | return emoji + padding 113 | } 114 | 115 | function formatNs (name) { 116 | return chalk.cyan(name) 117 | } 118 | 119 | function formatName (name) { 120 | return chalk.blue(name) 121 | } 122 | 123 | function formatMessage (obj) { 124 | var msg = formatMessageName(obj.message) 125 | if (obj.level === 'error') return chalk.red(msg) 126 | if (obj.level === 'trace') return chalk.white(msg) 127 | if (obj.level === 'warn') return chalk.magenta(msg) 128 | if (obj.level === 'debug') return chalk.yellow(msg) 129 | if (obj.level === 'info' || obj.level === 'userlvl') return chalk.green(msg) 130 | if (obj.level === 'fatal') return chalk.white.bgRed(msg) 131 | } 132 | 133 | function formatUrl (url) { 134 | return chalk.white(url) 135 | } 136 | 137 | function formatMethod (method) { 138 | return chalk.white(method) 139 | } 140 | 141 | function formatStatusCode (statusCode) { 142 | statusCode = statusCode || 'xxx' 143 | return chalk.white(statusCode) 144 | } 145 | 146 | function formatLoadTime (elapsedTime) { 147 | var elapsed = parseInt(elapsedTime, 10) 148 | var time = prettyMs(elapsed) 149 | return chalk.gray(time) 150 | } 151 | 152 | function formatBundleSize (bundle) { 153 | var bytes = parseInt(bundle, 10) 154 | var size = prettyBytes(bytes).replace(/ /, '') 155 | return chalk.gray(size) 156 | } 157 | 158 | function formatMessageName (message) { 159 | if (message === 'request') return '<--' 160 | if (message === 'response') return '-->' 161 | return message 162 | } 163 | 164 | function formatExtra (obj) { 165 | const extra = Object.keys(obj) 166 | .filter(key => { 167 | if (verbose) return true 168 | return !pinoKeys.includes(key) 169 | }) 170 | .reduce((acc, key) => { 171 | acc[key] = obj[key] 172 | return acc 173 | }, {}) 174 | 175 | const extraKeys = Object.keys(extra) 176 | 177 | if (extraKeys.length === 0) return '' 178 | 179 | const content = nl + nl + extraKeys.map(key => { 180 | let val = extra[key] 181 | if (isObject(val)) val = JSON.stringify(val, null, 2) 182 | 183 | // limit very long string values 184 | if (!verbose && typeof val === 'string' && val.split('\n').length > lineLimit) { 185 | let arr = val.split('\n').slice(0, lineLimit) 186 | arr.push(`(truncated at ${lineLimit} lines)`) 187 | val = arr.join('\n') 188 | } 189 | return chalk.gray(`${key}: ${val}`) 190 | }).join(nl) + nl 191 | 192 | return indent(content, 2) 193 | } 194 | 195 | function isObject (val) { 196 | return val != null && typeof val === 'object' && Array.isArray(val) === false 197 | } 198 | 199 | function isWideEmoji (character) { 200 | return character !== '⚠️' 201 | } 202 | 203 | function noEmpty (val) { 204 | return !!val 205 | } 206 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pino-gris", 3 | "description": "verbose ndjson log formatter for pino", 4 | "version": "1.3.1", 5 | "author": "Nate Goldman (https://ungoldman.com/)", 6 | "bin": { 7 | "pino-gris": "./bin.js" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/ungoldman/pino-gris/issues" 11 | }, 12 | "contributors": [ 13 | "Irina Shestak ", 14 | "Yoshua Wuyts ", 15 | "Nate Goldman " 16 | ], 17 | "dependencies": { 18 | "chalk": "^2.0.1", 19 | "fast-json-parse": "^1.0.2", 20 | "indent": "0.0.2", 21 | "pad-left": "^2.1.0", 22 | "pad-right": "^0.2.2", 23 | "prettier-bytes": "^1.0.3", 24 | "pretty-ms": "^2.1.0", 25 | "split2": "^2.1.1" 26 | }, 27 | "devDependencies": { 28 | "pino": "^4.16.1", 29 | "standard": "^8.6.0" 30 | }, 31 | "files": [ 32 | "index.js", 33 | "bin.js" 34 | ], 35 | "homepage": "https://github.com/ungoldman/pino-gris#readme", 36 | "keywords": [ 37 | "logger", 38 | "ndjson", 39 | "pino", 40 | "pretty-printer" 41 | ], 42 | "license": "MIT", 43 | "main": "index.js", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/ungoldman/pino-gris.git" 47 | }, 48 | "scripts": { 49 | "start": "node example.js | ./bin.js", 50 | "test": "standard" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungoldman/pino-gris/c1131bca4298dac0c4db645f66b9c41e90ec6d99/screenshot.png --------------------------------------------------------------------------------