├── .travis.yml ├── .gitignore ├── .npmignore ├── .jshintrc ├── package.json ├── LICENSE ├── README.md ├── .jscs.json ├── lib └── index.js └── test └── bunyan.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "6" 5 | - "4" 6 | - "0.12" 7 | - "0.10" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *swp 3 | .DS_Store 4 | coverage 5 | example.js 6 | hack.js 7 | node_modules 8 | npm-shrinkwrap.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | .istanbul.yml 3 | .jscs.json 4 | .jshintrc 5 | .npmignore 6 | .rock.yml 7 | config 8 | coverage 9 | hack.js 10 | npm-shrinkwrap.json 11 | test 12 | tmp 13 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "globals": { 4 | // Mocha 5 | "describe": false, 6 | "it": false, 7 | "before": false, 8 | "beforeEach": false, 9 | "after": false, 10 | "afterEach": false 11 | }, 12 | "camelcase": false, 13 | "eqeqeq": true, 14 | "eqnull": true, 15 | "indent": 2, 16 | "latedef": true, 17 | "newcap": true, 18 | "quotmark": "single", 19 | "trailing": true, 20 | "undef": true, 21 | "unused": true, 22 | "maxlen": 100, 23 | "strict": true, 24 | "expr": true 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-bunyan", 3 | "version": "0.7.0", 4 | "main": "./lib", 5 | "description": "Simple Bunyan logging in Hapi", 6 | "dependencies": { 7 | "lodash": "^3.0.0" 8 | }, 9 | "devDependencies": { 10 | "bunyan": "^1.0.0", 11 | "code": "^1.3.0", 12 | "hapi": "^8.0.0", 13 | "jscs": "^1.5.9", 14 | "jshint": "^2.5.3", 15 | "lab": "^5.2.1" 16 | }, 17 | "scripts": { 18 | "test": "./node_modules/.bin/jshint lib test && ./node_modules/.bin/jscs lib test && ./node_modules/.bin/lab -c -t 100 -l" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/silas/hapi-bunyan.git" 23 | }, 24 | "keywords": [ 25 | "bunyan", 26 | "hapi" 27 | ], 28 | "author": "Silas Sewell ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/silas/hapi-bunyan/issues" 32 | }, 33 | "homepage": "https://github.com/silas/hapi-bunyan" 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Shutterstock, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hapi Bunyan [![Build Status](https://travis-ci.org/silas/hapi-bunyan.png?branch=master)](https://travis-ci.org/silas/hapi-bunyan) 2 | 3 | This a simple [Bunyan][bunyan] plugin for Hapi. 4 | 5 | ## Documentation 6 | 7 | Options 8 | 9 | * logger (Object): Bunyan logger object 10 | * handler (Function, optional): custom event handler, this function can return `true` if it handled the event. 11 | * skipUndefined (Boolean, default: true): don't log events with `undefined` data. 12 | * includeData (Boolean, default: true): include data in log events. 13 | * mergeData (Boolean, default: false): when the event data is an object merge it into the log data. 14 | * includeTags (Boolean, default: false): include tags in log event. 15 | * joinTags (String, optional): join tags using the specified character. 16 | 17 | ## Example 18 | 19 | ``` javascript 20 | var bunyan = require('bunyan'); 21 | var hapi = require('hapi'); 22 | 23 | var server = new hapi.Server(); 24 | server.connection({ port: 8000 }); 25 | 26 | server.route({ 27 | method: 'GET', 28 | path: '/', 29 | handler: function(request, reply) { 30 | request.log.info('just a test'); 31 | 32 | reply({ hello: 'world' }); 33 | }, 34 | }); 35 | 36 | var config = { 37 | register: require('hapi-bunyan'), 38 | options: { 39 | logger: bunyan.createLogger({ name: 'test', level: 'debug' }), 40 | }, 41 | }; 42 | 43 | server.register(config, function(err) { 44 | if (err) throw err; 45 | }); 46 | 47 | server.start(); 48 | ``` 49 | 50 | ## License 51 | 52 | This work is licensed under the MIT License (see the LICENSE file). 53 | 54 | [bunyan]: https://www.npmjs.org/package/bunyan 55 | -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requireSpaceAfterKeywords": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "switch", 9 | "return", 10 | "try", 11 | "catch" 12 | ], 13 | "requireSpacesInFunctionExpression": { 14 | "beforeOpeningCurlyBrace": true 15 | }, 16 | "disallowSpacesInFunctionExpression": { 17 | "beforeOpeningRoundBrace": true 18 | }, 19 | "disallowEmptyBlocks": true, 20 | "requireSpacesInsideObjectBrackets": "all", 21 | "disallowSpacesInsideArrayBrackets": true, 22 | "disallowSpacesInsideParentheses": true, 23 | "disallowQuotedKeysInObjects": "allButReserved", 24 | "disallowSpaceAfterObjectKeys": true, 25 | "requireCommaBeforeLineBreak": true, 26 | "requireOperatorBeforeLineBreak": true, 27 | "requireSpaceAfterBinaryOperators": [ 28 | "?", 29 | "+", 30 | "/", 31 | "*", 32 | ":", 33 | "=", 34 | "==", 35 | "===", 36 | "!=", 37 | "!==", 38 | ">", 39 | ">=", 40 | "<", 41 | "<=" 42 | ], 43 | "requireSpacesInConditionalExpression": true, 44 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 45 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 46 | "requireSpaceBeforeBinaryOperators": [ 47 | "+", 48 | "-", 49 | "/", 50 | "*", 51 | "=", 52 | "==", 53 | "===", 54 | "!=", 55 | "!==" 56 | ], 57 | "requireSpaceAfterBinaryOperators": [ 58 | "+", 59 | "-", 60 | "/", 61 | "*", 62 | "=", 63 | "==", 64 | "===", 65 | "!=", 66 | "!==" 67 | ], 68 | "disallowKeywords": ["with"], 69 | "disallowMultipleLineStrings": true, 70 | "disallowMultipleLineBreaks": true, 71 | "validateLineBreaks": "LF", 72 | "validateQuoteMarks": true, 73 | "disallowTrailingWhitespace": true, 74 | "disallowKeywordsOnNewLine": ["else"], 75 | "requireCapitalizedConstructors": true, 76 | "safeContextKeyword": "self", 77 | "requireDotNotation": true, 78 | "excludeFiles": ["node_modules/**", "test/tmp/**"] 79 | } 80 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var lodash = require('lodash'); 8 | 9 | /** 10 | * Constants. 11 | */ 12 | 13 | var LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; 14 | 15 | /** 16 | * Event logger. 17 | */ 18 | 19 | function logEvent(ctx, data, request) { 20 | if (!data) return; 21 | 22 | var obj = {}; 23 | var msg = ''; 24 | 25 | if (ctx.includeTags && Array.isArray(data.tags)) { 26 | obj.tags = ctx.joinTags ? data.tags.join(ctx.joinTags) : data.tags; 27 | } 28 | 29 | if (request) obj.req_id = request.id; 30 | 31 | if (data instanceof Error) { 32 | ctx.log.child(obj)[ctx.level](data); 33 | return; 34 | } 35 | 36 | var type = typeof data.data; 37 | 38 | if (type === 'string') { 39 | msg = data.data; 40 | } else if (ctx.includeData && data.data !== undefined) { 41 | if (ctx.mergeData && type === 'object' && !Array.isArray(data.data)) { 42 | lodash.assign(obj, data.data); 43 | 44 | if (obj.id === obj.req_id) delete obj.id; 45 | } else { 46 | obj.data = data.data; 47 | } 48 | } else if (ctx.skipUndefined) { 49 | return; 50 | } 51 | 52 | ctx.log[ctx.level](obj, msg); 53 | } 54 | 55 | /** 56 | * Plugin. 57 | */ 58 | 59 | function register(server, options, next) { 60 | if (!options.logger) { 61 | return next(new Error('logger required')); 62 | } 63 | 64 | var log = options.logger; 65 | var handler = options.handler || function() {}; 66 | 67 | options = lodash.omit(options, ['logger', 'handler']); 68 | 69 | options = lodash.defaults(options, { 70 | includeTags: false, 71 | includeData: true, 72 | mergeData: false, 73 | skipUndefined: true, 74 | }); 75 | 76 | var makeCtx = function(tags, level) { 77 | if (tags.fatal) { 78 | level = 'fatal'; 79 | } else if (tags.error) { 80 | level = 'error'; 81 | } else if (tags.warn) { 82 | level = 'warn'; 83 | } else if (tags.info) { 84 | level = 'info'; 85 | } else if (tags.debug) { 86 | level = 'debug'; 87 | } else if (tags.trace) { 88 | level = 'trace'; 89 | } 90 | 91 | return { 92 | level: level, 93 | log: log, 94 | includeTags: options.includeTags, 95 | includeData: options.includeData, 96 | mergeData: options.mergeData, 97 | skipUndefined: options.skipUndefined, 98 | joinTags: options.joinTags, 99 | }; 100 | }; 101 | 102 | server.ext('onRequest', function(request, reply) { 103 | var rlog = request.log; 104 | 105 | request.bunyan = log.child({ req_id: request.id }); 106 | 107 | request.log = function() { 108 | rlog.apply(request, arguments); 109 | }; 110 | 111 | LEVELS.forEach(function(level) { 112 | request.log[level] = function() { 113 | request.bunyan[level].apply(request.bunyan, arguments); 114 | }; 115 | }); 116 | 117 | return reply.continue(); 118 | }); 119 | 120 | server.on('log', function(data, tags) { 121 | var ctx = makeCtx(tags, 'info'); 122 | 123 | if (handler.call(ctx, 'log', data, tags)) { 124 | return; 125 | } 126 | 127 | logEvent(ctx, data); 128 | }); 129 | 130 | server.on('request', function(request, data, tags) { 131 | var ctx = makeCtx(tags, 'info'); 132 | 133 | if (handler.call(ctx, 'request', request, data, tags)) { 134 | return; 135 | } 136 | 137 | logEvent(ctx, data, request); 138 | }); 139 | 140 | server.on('request-internal', function(request, data, tags) { 141 | var ctx = makeCtx(tags, 'debug'); 142 | 143 | if (handler.call(ctx, 'request-internal', request, data, tags)) { 144 | return; 145 | } 146 | 147 | logEvent(ctx, data, request); 148 | }); 149 | 150 | server.on('request-error', function(request, err) { 151 | var tags = {}; 152 | var ctx = makeCtx(tags, 'error'); 153 | 154 | if (handler.call(ctx, 'request-error', request, err, tags)) { 155 | return; 156 | } 157 | 158 | logEvent(ctx, err, request); 159 | }); 160 | 161 | next(); 162 | } 163 | 164 | /** 165 | * Attributes. 166 | */ 167 | 168 | register.attributes = { 169 | pkg: require('../package.json'), 170 | name: 'hapi-bunyan', 171 | }; 172 | 173 | /** 174 | * Module exports. 175 | */ 176 | 177 | exports.log = logEvent; 178 | exports.register = register; 179 | -------------------------------------------------------------------------------- /test/bunyan.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var bunyan = require('bunyan'); 8 | var expect = require('code').expect; 9 | var hapi = require('hapi'); 10 | 11 | /** 12 | * Lab. 13 | */ 14 | 15 | var lab = exports.lab = require('lab').script(); 16 | 17 | /** 18 | * Helpers 19 | */ 20 | 21 | function make() { 22 | var buffer = new bunyan.RingBuffer({ limit: 100 }); 23 | 24 | var logger = bunyan.createLogger({ 25 | name: 'test', 26 | streams: [ 27 | { 28 | level: 'trace', 29 | type: 'raw', 30 | stream: buffer, 31 | }, 32 | ], 33 | }); 34 | 35 | logger.buffer = buffer; 36 | 37 | return logger; 38 | } 39 | 40 | /** 41 | * Plugin. 42 | */ 43 | 44 | lab.experiment('bunyan', function() { 45 | 46 | lab.test('logger requirement', function(done) { 47 | var server = new hapi.Server(); 48 | server.connection(); 49 | 50 | server.register({ register: require('../lib') }, function(err) { 51 | expect(err).to.exist(); 52 | 53 | done(); 54 | }); 55 | }); 56 | 57 | lab.test('logger requirement', function(done) { 58 | var server = new hapi.Server(); 59 | server.connection(); 60 | 61 | server.register({ register: require('../lib') }, function(err) { 62 | expect(err).to.exist(); 63 | 64 | done(); 65 | }); 66 | }); 67 | 68 | lab.test('log event', function(done) { 69 | var logger = make(); 70 | var server = new hapi.Server(); 71 | server.connection(); 72 | 73 | var last; 74 | 75 | var handler = function() { 76 | last = Array.prototype.slice.call(arguments); 77 | }; 78 | 79 | server.register({ 80 | register: require('../lib'), 81 | options: { logger: logger, handler: handler }, 82 | }, function(err) { 83 | expect(err).not.to.exist(); 84 | 85 | server.log(['test'], 'server-test'); 86 | server.log(['error'], 'server-error'); 87 | 88 | expect(last).to.be.an.array(); 89 | expect(last.length).to.equal(3); 90 | expect(last[0]).to.equal('log'); 91 | expect(last[1]).to.be.an.object(); 92 | expect(last[1].data).to.equal('server-error'); 93 | expect(last[2]).to.be.an.object(); 94 | expect(last[2]).to.have.include('error'); 95 | expect(last[2].error).to.equal(true); 96 | 97 | done(); 98 | }); 99 | }); 100 | 101 | lab.test('request event', function(done) { 102 | var logger = make(); 103 | var server = new hapi.Server(); 104 | server.connection(); 105 | 106 | server.route({ 107 | method: 'GET', 108 | path: '/', 109 | handler: function(request, reply) { 110 | request.log(['tester'], 'hello world'); 111 | request.log.trace('test-trace'); 112 | request.log.error('test-error'); 113 | 114 | reply({ hello: 'world' }); 115 | }, 116 | }); 117 | 118 | server.register({ 119 | register: require('../lib'), 120 | options: { logger: logger }, 121 | }, function(err) { 122 | expect(err).not.to.exist(); 123 | 124 | server.inject('/', function() { 125 | var records = logger.buffer.records; 126 | 127 | expect(records[0].level).to.equal(20); 128 | expect(records[0].data).to.include('method'); 129 | expect(records[0].data).to.include('url'); 130 | expect(records[0].data).to.include('agent'); 131 | expect(records[0].msg).to.equal(''); 132 | 133 | expect(records[1].level).to.equal(30); 134 | expect(records[1].msg).to.equal('hello world'); 135 | 136 | expect(records[2].level).to.equal(10); 137 | expect(records[2].msg).to.equal('test-trace'); 138 | 139 | expect(records[3].level).to.equal(50); 140 | expect(records[3].msg).to.equal('test-error'); 141 | 142 | expect(records[4].level).to.equal(20); 143 | expect(records[4].data).to.include('msec'); 144 | expect(records[4].msg).to.equal(''); 145 | 146 | done(); 147 | }); 148 | }); 149 | }); 150 | 151 | lab.test('request error', function(done) { 152 | var logger = make(); 153 | var server = new hapi.Server({ debug: false }); 154 | server.connection(); 155 | 156 | server.route({ 157 | method: 'GET', 158 | path: '/', 159 | handler: function() { 160 | throw new Error('fail'); 161 | }, 162 | }); 163 | 164 | server.register({ 165 | register: require('../lib'), 166 | options: { logger: logger }, 167 | }, function(err) { 168 | expect(err).not.to.exist(); 169 | 170 | server.inject('/', function() { 171 | var records = logger.buffer.records; 172 | 173 | expect(records[1].level).to.equal(50); 174 | 175 | expect(records[1]).to.include('err'); 176 | expect(records[1].err.message).to.equal('Uncaught error: fail'); 177 | expect(records[1].err.name).to.include('Error'); 178 | expect(records[1].err).to.include('stack'); 179 | expect(records[1].msg).to.equal('Uncaught error: fail'); 180 | 181 | done(); 182 | }); 183 | }); 184 | }); 185 | 186 | lab.test('handle bad data', function(done) { 187 | var logger = make(); 188 | var server = new hapi.Server(); 189 | server.connection(); 190 | 191 | server.route({ 192 | method: 'GET', 193 | path: '/', 194 | handler: function(request, reply) { 195 | request.connection.emit('request-internal', request, null, true); 196 | 197 | reply({ hello: 'world' }); 198 | }, 199 | }); 200 | 201 | server.register({ 202 | register: require('../lib'), 203 | options: { logger: logger }, 204 | }, function(err) { 205 | expect(err).not.to.exist(); 206 | 207 | server.inject('/', function() { 208 | done(); 209 | }); 210 | }); 211 | }); 212 | 213 | lab.test('handle ctx options', function(done) { 214 | var logger = make(); 215 | var server = new hapi.Server(); 216 | server.connection(); 217 | 218 | server.route({ 219 | method: 'GET', 220 | path: '/', 221 | handler: function(request, reply) { 222 | request.log(['trace'], { test: 'includeTags' }); 223 | request.log(['debug'], { test: 'joinTags' }); 224 | request.log(['info'], { test: 'includeData' }); 225 | request.log(['warn'], { test: 'mergeData' }); 226 | request.log(['error'], { test: 'mergeData delete id' }); 227 | request.log(['fatal'], { test: 'mergeData not object' }); 228 | request.log(['fatal'], { test: 'mergeData array' }); 229 | request.log(['fatal'], { test: 'skipUndefined' }); 230 | 231 | reply({ hello: 'world' }); 232 | }, 233 | }); 234 | 235 | var handler = function(type, request, data) { 236 | if (type === 'request' && data.data.test) { 237 | switch (data.data.test) { 238 | case 'includeTags': 239 | this.includeTags = true; 240 | this.joinTags = false; 241 | break; 242 | case 'joinTags': 243 | this.includeTags = true; 244 | this.joinTags = true; 245 | break; 246 | case 'includeData': 247 | this.includeData = false; 248 | break; 249 | case 'mergeData': 250 | this.mergeData = true; 251 | break; 252 | case 'mergeData delete id': 253 | this.mergeData = true; 254 | data.data.id = request.id; 255 | break; 256 | case 'mergeData not object': 257 | this.mergeData = true; 258 | data.data = 123; 259 | break; 260 | case 'mergeData array': 261 | this.mergeData = true; 262 | data.data = [1, 2, 3]; 263 | break; 264 | case 'skipUndefined': 265 | this.skipUndefined = false; 266 | data.data = undefined; 267 | break; 268 | } 269 | } 270 | }; 271 | 272 | server.register({ 273 | register: require('../lib'), 274 | options: { logger: logger, handler: handler }, 275 | }, function(err) { 276 | expect(err).not.to.exist(); 277 | 278 | server.inject('/', function() { 279 | done(); 280 | }); 281 | }); 282 | }); 283 | 284 | lab.test('skip log handling', function(done) { 285 | var logger = make(); 286 | var server = new hapi.Server(); 287 | server.connection(); 288 | 289 | server.route({ 290 | method: 'GET', 291 | path: '/', 292 | handler: function(request, reply) { 293 | var tags = {}; 294 | 295 | request.connection.emit('log', 'skip', tags); 296 | request.connection.emit('request', request, 'skip', tags); 297 | request.connection.emit('request-internal', request, 'skip', tags); 298 | request.connection.emit('request-error', request, 'skip', tags); 299 | 300 | reply({ hello: 'world' }); 301 | }, 302 | }); 303 | 304 | var handler = function() { 305 | for (var i = 0; i < arguments.length; i++) { 306 | if (arguments[i] === 'skip') { 307 | return true; 308 | } 309 | } 310 | }; 311 | 312 | server.register({ 313 | register: require('../lib'), 314 | options: { logger: logger, handler: handler }, 315 | }, function(err) { 316 | expect(err).not.to.exist(); 317 | 318 | server.inject('/', function() { 319 | var records = logger.buffer.records; 320 | 321 | expect(records.length).to.equal(2); 322 | 323 | done(); 324 | }); 325 | }); 326 | }); 327 | }); 328 | --------------------------------------------------------------------------------