UNPKG

micrologger

Version:

koa microservice application and request logging

404 lines (358 loc) 13.8 kB
'use strict'; const mocha = require('mocha'); const expect = require('chai').expect; const sinon = require('sinon'); const mockery = require('mockery'); const OS = require('os'); const PACKAGE = require('../package.json'); const Micrologger = require('../micrologger'); const Level = require('../levels/level'); const Collector = require('../collectors/collector'); const LEVEL_NAMES = [ 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'informational', 'debug' ]; const LEVEL_KEYWORDS = [ 'emerg', 'alert', 'crit', 'err', 'warning', 'notice', 'info', 'debug' ]; const CTX = () => ({ method : 'get', path : '/', request : { headers : {}, ip : '127.0.0.1' }, response : {}, res : { removeListener : () => {}, once : (evnt, cb) => { if (evnt === 'finish') cb() }, } }); describe('micrologger', () => { describe('constructor', () => { it('should return an itialized logger with default levels and no collectors', () => { let m = new Micrologger(); expect(m.name).to.equal(PACKAGE.name); expect(m.color).to.be.false; expect(m.bold).to.be.false; expect(m.backgroundColor).to.be.false; expect(m.stripAnsi).to.be.true; expect(m.hostname).to.equal(OS.hostname()); expect(m.formatReq).to.be.a('function'); expect(m.formatRes).to.be.a('function'); // Collectors should be empty by default expect(m.collectors).to.be.an('object'); expect(Object.keys(m.collectors)).to.be.length(0); // Header management expect(m.requestIdHeader).to.equal('x-request-id'); expect(m.correlationHeader).to.equal('x-correlation-id'); expect(m.correlationGen).to.be.a('function'); // Levels should default to syslog presets expect(m.levelSeverity).to.equal('ALL'); expect(m.levelValue).to.equal(Infinity); expect(Object.keys(m.levels)).to.have.length(LEVEL_NAMES.length); for (let name of LEVEL_NAMES) { let capitalized = name.charAt(0).toUpperCase() + name.slice(1); let regex = new RegExp(`[Function: ${capitalized}Level]`); expect(m.levels[name]).instanceof(Level); expect(m.levels[name].constructor).match(regex); } }); it('should add log level shortcuts to the base of the logger', () => { let m = new Micrologger(); for (let level of LEVEL_NAMES) { let lvl = m.levels[level]; expect(m[lvl.severity]).to.be.a('function'); expect(m[lvl.severity].isLevel).to.be.true; expect(m[lvl.keyword]).to.be.a('function'); expect(m[lvl.keyword].isLevel).to.be.true; } }); it('should allow settings overrides', () => { let opts = { name: 'NAME', color: 'green', bold: 'BOLD', backgroundColor: 'BKG_COLOR', stripAnsi: false, hostname: 'HOSTNAME', formatReq: () => 'FORMATTED', formatRes: () => 'FORMATTED', } let m = new Micrologger(opts); for (let key of Object.keys(opts)) { expect(m[key]).to.equal(opts[key]); } }); it('should allow setting individual collector', () => { let type = 'my_collector'; class MyCollector extends Collector { constructor () { super(); this.type = type; } } let collector = new MyCollector(); let m = new Micrologger({ collector: collector }); expect(Object.keys(m.collectors)).to.have.length(1); expect(m.collectors[type]).to.equal(collector); }); it('should allow setting map of collectors', () => { let typeOne = 'my_collector_one'; class OneCollector extends Collector { constructor () { super(); this.type = typeOne; } } let typeTwo = 'my_collector_two'; class TwoCollector extends Collector { constructor () { super(); this.type = typeTwo; } } let collectors = {}; collectors[typeOne] = new OneCollector(); collectors[typeTwo] = new TwoCollector(); let m = new Micrologger({ collectors }); expect(Object.keys(m.collectors)).to.have.length(2); expect(m.collectors[typeOne]).to.equal(collectors[typeOne]); expect(m.collectors[typeTwo]).to.equal(collectors[typeTwo]); }); it('should allow settings levels', () => { let typeOne = 'my_level_one'; class OneLevel extends Level { constructor () { super(); this.severity = typeOne; this.keyword = typeOne; this.value = 0; } } let typeTwo = 'my_level_two'; class TwoLevel extends Level { constructor () { super(); this.severity = typeTwo; this.keyword = typeTwo; this.value = 0; } } let levels = {}; levels[typeOne] = new OneLevel (); levels[typeTwo] = new TwoLevel (); let m = new Micrologger({ levels }); expect(Object.keys(m.levels)).to.have.length(2); expect(m.levels[typeOne]).to.equal(levels[typeOne]); expect(m.levels[typeTwo]).to.equal(levels[typeTwo]); }); it('should allow settings overrides', () => { let m = new Micrologger(); // TODO }); }); describe('useCollector', () => { it('should add the predefined collectors to the list of active collectors', () => { let m = new Micrologger(); m.useCollector('zmq', { host : '127.0.0.1', port : 5555, }); expect(m.collectors.zmq).instanceof(Collector); m.collectors.zmq.collect = sinon.spy(); m.collect('test'); expect(m.collectors.zmq.collect.called).to.be.true; }); it('should add the custom collector to the list of active collectors', () => { class MyCollector extends Collector{ constructor () { super(); this.type = 'mine'; this.collect = sinon.spy(); } } let m = new Micrologger(); m.useCollector(new MyCollector()); expect(m.collectors.mine).instanceof(Collector); m.collect('test'); expect(m.collectors.mine.collect.called).to.be.true; }); it('should error if the collector is invalid', () => { let m = new Micrologger(); expect(() => m.useCollector('fail')).throw(Error); expect(Object.keys(m.collectors)).to.have.length(0); }); }); describe('setColor', () => { it('should set all levels to use the color', () => { let m = new Micrologger(); m.setColor('blue'); let levels = Object.keys(m.levels); for (let lvl of levels) expect(m.levels[lvl].color).to.equal('blue'); }); it('error for unsupported colors', () => { let m = new Micrologger(); expect(() => m.setColor('nonsense')).to.throw(Error); }); }); describe('setBold', () => { it('should set all levels to use the bold setting', () => { let m = new Micrologger(); let levels = Object.keys(m.levels); m.setBold(true); for (let lvl of levels) expect(m.levels[lvl].bold).to.be.true; m.setBold(false); for (let lvl of levels) expect(m.levels[lvl].bold).to.be.false; }); }); describe('setStripAnsi', () => { it('should set all levels to use the strip-ansi-encoding setting', () => { let m = new Micrologger(); let levels = Object.keys(m.levels); m.setStripAnsi(true); expect(m.stripAnsi).to.be.true; m.setStripAnsi(false); expect(m.stripAnsi).to.be.false; }); }); describe('log', () => { it('should use the level specified to print the data object message, and collect to registered collectors', () => { let m = new Micrologger(); let level = 'debug'; let data = { message: 'test' }; sinon.spy(m, 'collect'); m.levels[level].log = sinon.spy(); // dont log to console m.log(level, data); // validate call to collector with normalized data expect(m.collect.calledOnce).to.be.true let collectData = m.collect.getCall(0).args[0]; expect(collectData.message).to.equal(data.message); expect(collectData.severity).to.equal('DEBUG'); expect(collectData.level).to.equal('debug'); // validate call to level with normalized data expect(m.levels[level].log.calledOnce).to.be.true let levelData = m.levels[level].log.getCall(0).args[0]; expect(levelData.message).to.equal(data.message); expect(levelData.severity).to.equal('DEBUG'); expect(levelData.level).to.equal('debug'); // Make sure data was not changed expect(data.severity).to.be.undefined; expect(data.level).to.be.undefined; }); it('should accept string instead of an object', () => { let m = new Micrologger(); let level = 'debug'; let data = 'test'; sinon.spy(m, 'collect'); m.levels[level].log = sinon.spy(); // dont log to console m.log(level, 'test'); // validate call to collector with normalized data expect(m.collect.calledOnce).to.be.true let collectData = m.collect.getCall(0).args[0]; expect(collectData.message).to.equal(data); expect(collectData.severity).to.equal('DEBUG'); expect(collectData.level).to.equal('debug'); // validate call to level with normalized data expect(m.levels[level].log.calledOnce).to.be.true let levelData = m.levels[level].log.getCall(0).args[0]; expect(levelData.message).to.equal(data); expect(levelData.severity).to.equal('DEBUG'); expect(levelData.level).to.equal('debug'); }); it('should ignore call if the level is above the logging level set', () => { let m = new Micrologger({ level: 'warning' }); for (let level of Object.keys(m.levels)) m.levels[level].log = () => {}; sinon.spy(m, 'collect'); m.log('debug'); expect(m.collect.called).to.be.false; m.log('informational'); expect(m.collect.called).to.be.false; m.log('notice'); expect(m.collect.called).to.be.false; m.log('warning'); expect(m.collect.callCount).to.equal(1); m.log('error'); expect(m.collect.callCount).to.equal(2); m.log('critical'); expect(m.collect.callCount).to.equal(3); m.log('alert'); expect(m.collect.callCount).to.equal(4); m.log('emergency'); expect(m.collect.callCount).to.equal(5); }); }); describe('collect', () => { it('should execute collect on each collector', () => { let m = new Micrologger(); m.collect(); // should not error if no collectorrs are assigned m.collectors = { one: { collect: sinon.spy() }, two: { collect: sinon.spy() }, }; m.collect('one'); expect(m.collectors.one.collect.calledWith('one')).to.be.true; expect(m.collectors.two.collect.calledWith('one')).to.be.true; m.collect('two'); expect(m.collectors.one.collect.calledWith('two')).to.be.true; expect(m.collectors.two.collect.calledWith('two')).to.be.true; m.collect('three'); expect(m.collectors.one.collect.calledWith('three')).to.be.true; expect(m.collectors.two.collect.calledWith('three')).to.be.true; }); }); describe('middleware', () => { it('should return a middleware function which accepts (ctx, next) and returns a promise', async () => { let m = new Micrologger(); m.log = sinon.spy(); let middleware = m.middleware(); let called = false; await middleware(CTX(), async () => (called = true)); expect(called).to.be.true; }); describe('function', () => { it('should log req and res data', async () => { let m = new Micrologger(); m.levels['informational'].stdout = sinon.spy(); m.levels['error'].stderr = sinon.spy(); sinon.spy(m, 'log'); let middleware = m.middleware(); let ctx = CTX(); await middleware(ctx, async () => {}); let reqCall = m.log.getCall(0).args; let reqLog = reqCall[1]; expect(reqCall[0]).to.equal('informational'); expect(reqLog.request_id).to.be.a('string'); expect(reqLog.correlation_id).to.be.a('string'); expect(reqLog.class).to.equal('client_request'); expect(reqLog.request_time).to.be.a('string'); expect(reqLog.message).to.equal(`<-- ${ctx.method} ${ctx.path}`); expect(reqLog.ident).to.equal(PACKAGE.name); expect(reqLog.host).to.equal(OS.hostname()); expect(reqLog.client).to.equal(ctx.request.ip); expect(reqLog.path).to.equal(ctx.path); expect(reqLog.method).to.equal(ctx.method); expect(reqLog.severity).to.equal('informational'); expect(reqLog.metadata).to.be.an('object'); let resCall = m.log.getCall(1).args; let resLog = resCall[1]; expect(reqCall[0]).to.equal('informational'); expect(resLog.request_id).to.equal(reqLog.request_id); expect(resLog.correlation_id).to.equal(reqLog.correlation_id); expect(resLog.class).to.equal('client_request'); expect(resLog.response_time).to.be.a('string'); expect(resLog.message).to.equal(`--> ${ctx.method} ${ctx.path} ${ctx.status}`); expect(resLog.ident).to.equal(PACKAGE.name); expect(resLog.host).to.equal(OS.hostname()); expect(resLog.client).to.equal(ctx.request.ip); expect(resLog.path).to.equal(ctx.path); expect(resLog.method).to.equal(ctx.method); expect(resLog.severity).to.equal('informational'); expect(resLog.metadata).to.be.an('object'); }); }); }); });