From 95721becde6213dd251eab1a291c82b3d4635eca Mon Sep 17 00:00:00 2001 From: Darren Hurley Date: Thu, 17 Nov 2016 22:34:44 +0000 Subject: [PATCH] Remove new lines from logged values --- package.json | 3 +- src/lib/format.js | 44 ++--- src/lib/formatter.js | 13 +- src/lib/logger.js | 103 ++++++++++++ src/lib/utils.js | 3 + src/main.js | 86 +--------- test/lib/agent.test.js | 2 +- test/lib/format.test.js | 51 +++--- test/lib/formatter.test.js | 2 +- test/lib/logger.test.js | 261 +++++++++++++++++++++++++++++ test/lib/transports/splunk.test.js | 4 +- test/logger.test.js | 156 ----------------- 12 files changed, 423 insertions(+), 305 deletions(-) create mode 100644 src/lib/logger.js create mode 100644 src/lib/utils.js create mode 100644 test/lib/logger.test.js delete mode 100644 test/logger.test.js diff --git a/package.json b/package.json index 82c7b21..f64af59 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "eslint": "^3.0.1", "lintspaces-cli": "^0.3.0", "mocha": "^2.2.1", - "sinon": "^1.17.0" + "sinon": "^1.17.0", + "sinon-chai": "^2.8.0" }, "engines": { "node": ">=4.4.7" diff --git a/src/lib/format.js b/src/lib/format.js index 62468b7..22d9d7d 100644 --- a/src/lib/format.js +++ b/src/lib/format.js @@ -1,39 +1,25 @@ -const extractErrorDetails = err => { - const deets = { - error_message: err.message, - error_name: err.name - }; - if (err.stack) { - // logs need to all be on one line, so remove newlines from the stacktrace - deets.error_stack = err.stack.replace(/\n/g, '; '); - } - - return deets; -}; +const sanitise = value => (typeof value === 'string') ? value.replace(/"/g, '\'').replace(/\n/g, '; ') : value; -const deQuote = value => (typeof value === 'string') ? value.replace(/"/g, '\'') : value; +const formatMessage = message => sanitise(message); -const formatMessage = message => deQuote(message); - -const formatError = obj => (obj instanceof Error) ? extractErrorDetails(obj) : obj; +const formatValue = value => { + if (Array.isArray(value)) { + return `"${value.map(sanitise).join(',')}"`; + } else { + // wrap in quotes, if it contains a space + const sanitisedValue = sanitise(value); + return (/\s/.test(sanitisedValue)) ? `"${sanitisedValue}"` : sanitisedValue; + } +}; const formatFields = fields => { const formattedFields = Object.keys(fields) - .map(key => { - let value = formatValue(fields[key]); - return `${key}=${value}`; + .map(fieldName => { + const fieldValue = formatValue(fields[fieldName]); + return `${fieldName}=${fieldValue}`; }); return formattedFields.join(' '); }; -const formatValue = value => { - if (Array.isArray(value)) { - return `"${value.join(',')}"`; - } else { - // wrap in quotes, if it contains a space - return (/\s/.test(value)) ? `"${deQuote(value)}"` : deQuote(value); - } -}; - -export { formatError, formatMessage, formatFields, formatValue } +export { formatMessage as message, formatFields as fields } diff --git a/src/lib/formatter.js b/src/lib/formatter.js index ee0e28a..d79021d 100644 --- a/src/lib/formatter.js +++ b/src/lib/formatter.js @@ -1,10 +1,11 @@ -import { formatMessage, formatFields } from './format'; - -const nonEmpty = item => item; +import * as format from './format'; +import * as utils from './utils'; export default ({ level, message = '', meta = {} }) => { - const fields = level ? Object.assign({ level }, meta) : meta; - return [formatMessage(message), formatFields(fields)] - .filter(nonEmpty) + if (level) { + meta.level = level + } + return [format.message(message), format.fields(meta)] + .filter(utils.identity) .join(' '); } diff --git a/src/lib/logger.js b/src/lib/logger.js new file mode 100644 index 0000000..8d8db85 --- /dev/null +++ b/src/lib/logger.js @@ -0,0 +1,103 @@ +import winston from 'winston'; + +import Splunk from './transports/splunk'; +import formatter from './formatter'; +import * as utils from './utils'; + +const extractErrorDetails = obj => { + if (obj instanceof Error) { + const deets = { + error_message: obj.message, + error_name: obj.name + }; + if ('stack' in obj) { + deets.error_stack = obj.stack; + } + + return deets; + } else { + return obj; + } +}; + +const loggerArgs = (level, message, ...metas) => { + const args = [level]; + // if not a string, assume it's a meta object + if (typeof message === 'string') { + args.push(message); + } else { + metas.unshift(message) + } + if (metas.length) { + args.push( + metas.reduceRight((currentFormattedMetas, meta) => + Object.assign({}, currentFormattedMetas, extractErrorDetails(meta)), + {}) + ); + } + + return args; +}; + +class Logger { + + constructor (deps = {}) { + this.deps = Object.assign({ winston, formatter, Splunk }, deps); + this.logger = new (this.deps.winston.Logger)(); + // create logging methods + Object.keys(this.logger.levels).forEach(level => + this[level] = (...args) => this.log(level, ...args) + ); + } + + log (level, message, ...metas) { + const args = loggerArgs(level, message, ...metas).filter(utils.identity); + this.logger.log.apply(this.logger, args); + } + + addConsole (level = 'info', opts = {}) { + if (this.logger.transports.console) { + return; + } + this.logger.add( + this.deps.winston.transports.Console, + Object.assign({ level, formatter: this.deps.formatter }, opts) + ); + } + + removeConsole () { + if (!this.logger.transports.console) { + return; + } + this.logger.remove('console'); + } + + addSplunk (splunkUrl, level = 'info', opts = {}) { + if (this.logger.transports.splunk) { + return; + } + if (!splunkUrl) { + this.warn('No `splunkUrl` supplied'); + return false; + } + this.logger.add( + this.deps.Splunk, + Object.assign({ level, splunkUrl }, opts) + ); + } + + removeSplunk () { + if (!this.logger.transports.splunk) { + return; + } + this.logger.remove('splunk'); + } + + clearLoggers () { + Object.keys(this.logger.transports) + .forEach(logger => this.logger.remove(logger)); + } + +} + +export default Logger; diff --git a/src/lib/utils.js b/src/lib/utils.js new file mode 100644 index 0000000..319b926 --- /dev/null +++ b/src/lib/utils.js @@ -0,0 +1,3 @@ +const identity = item => item; + +export { identity } diff --git a/src/main.js b/src/main.js index 76601aa..41a0a69 100644 --- a/src/main.js +++ b/src/main.js @@ -1,88 +1,4 @@ -import winston from 'winston'; -import Splunk from './lib/transports/splunk'; -import formatter from './lib/formatter'; -import { formatError } from './lib/format'; - -const loggerArgs = (level, message, ...metas) => { - const args = [level]; - // if not a string, assume it's a meta object - if (typeof message === 'string') { - args.push(message); - } else { - metas.unshift(message) - } - if (metas.length) { - args.push( - metas.reduceRight((currentFormattedMetas, meta) => - Object.assign({}, currentFormattedMetas, formatError(meta)), - {}) - ); - } - - return args; -}; - -const empty = item => item; - -class Logger { - - constructor () { - this.logger = new (winston.Logger)(); - // create logging methods - Object.keys(this.logger.levels).forEach(level => - this[level] = (...args) => this.log(level, ...args) - ); - } - - log (level, message, ...metas) { - const args = loggerArgs(level, message, ...metas).filter(empty); - this.logger.log.apply(this.logger, args); - } - - addConsole (level = 'info', opts = {}) { - if (this.logger.transports.console) { - return; - } - this.logger.add( - winston.transports.Console, - Object.assign({ level, formatter }, opts) - ); - } - - removeConsole () { - if (!this.logger.transports.console) { - return; - } - this.logger.remove('console'); - } - - addSplunk (splunkUrl, level = 'info', opts = {}) { - if (this.logger.transports.splunk) { - return; - } - if (!splunkUrl) { - this.logger.warn('No `splunkUrl` supplied'); - return false; - } - this.logger.add( - Splunk, - Object.assign({ level, splunkUrl }, opts) - ); - } - - removeSplunk () { - if (!this.logger.transports.splunk) { - return; - } - this.logger.remove('splunk'); - } - - clearLoggers () { - Object.keys(this.logger.transports) - .forEach(logger => this.logger.remove(logger)); - } - -} +import Logger from './lib/logger'; const logger = new Logger(); diff --git a/test/lib/agent.test.js b/test/lib/agent.test.js index 6fdeab2..29b4b3b 100644 --- a/test/lib/agent.test.js +++ b/test/lib/agent.test.js @@ -26,7 +26,7 @@ describe('Agent', () => { }); server.listen(port); }); - agent = fork(path.resolve(__dirname, '..', '..', 'build/lib/agent.js'), [`${host}/${appName}`]); + agent = fork(path.resolve(__dirname, '..', '..', 'build', 'lib', 'agent.js'), [`${host}/${appName}`]); }); afterEach(done => { diff --git a/test/lib/format.test.js b/test/lib/format.test.js index a07eaf9..241e707 100644 --- a/test/lib/format.test.js +++ b/test/lib/format.test.js @@ -1,27 +1,24 @@ import chai from 'chai'; chai.should(); -import { formatError, formatMessage, formatFields, formatValue } from '../../build/lib/format'; +import * as format from '../../build/lib/format'; describe('Format', () => { - describe('Error', () => { - - it('should exist', () => { - formatError.should.exist; - }); - - }); - describe('Message', () => { it('should exist', () => { - formatMessage.should.exist; + format.message.should.exist; }); it('should replace double-quotes with single-quotes', () => { const message = 'an "error" occurred'; - formatMessage(message).should.equal('an \'error\' occurred'); + format.message(message).should.equal('an \'error\' occurred'); + }); + + it('should convert new lines to semicolon-space', () => { + const message = 'an error\nover \nmultiple lines'; + format.message(message).should.equal('an error; over ; multiple lines'); }); }); @@ -29,14 +26,14 @@ describe('Format', () => { describe('Fields', () => { it('should exist', () => { - formatFields.should.exist; + format.fields.should.exist; }); it('should format fields', () => { const meta = { app: 'ft-next-front-page' }; - formatFields(meta).should.equal('app=ft-next-front-page'); + format.fields(meta).should.equal('app=ft-next-front-page'); }); it('should format multiple fields', () => { @@ -44,21 +41,28 @@ describe('Format', () => { app: 'ft-next-front-page', level: 'error' }; - formatFields(meta).should.equal('app=ft-next-front-page level=error'); + format.fields(meta).should.equal('app=ft-next-front-page level=error'); }); it('should wrap values with spaces in double quotes', () => { const meta = { msg: 'Bad response' }; - formatFields(meta).should.equal('msg="Bad response"'); + format.fields(meta).should.equal('msg="Bad response"'); }); it('should convert values with double quotes to singles', () => { const meta = { msg: 'Server responded with "Bad Request", 400' }; - formatFields(meta).should.equal('msg="Server responded with \'Bad Request\', 400"'); + format.fields(meta).should.equal('msg="Server responded with \'Bad Request\', 400"'); + }); + + it('should convert new lines to semicolon-space', () => { + const meta = { + msg: 'an error\nover \nmultiple lines' + }; + format.fields(meta).should.equal('msg="an error; over ; multiple lines"'); }); it('should handle non-string values', () => { @@ -66,22 +70,21 @@ describe('Format', () => { msg: 'Bad response', status: 400 }; - formatFields(meta).should.equal('msg="Bad response" status=400'); + format.fields(meta).should.equal('msg="Bad response" status=400'); }); it('should convert arrays to multi-valued fields', () => { const meta = { msg: ['value-one', 'value-two'] }; - formatFields(meta).should.equal('msg="value-one,value-two"'); + format.fields(meta).should.equal('msg="value-one,value-two"'); }); - }); - - describe('Value', () => { - - it('should exist', () => { - formatValue.should.exist; + it('should sanitise multi-valued fields', () => { + const meta = { + msg: ['value one', 'value "two"', 'value \nthree'] + }; + format.fields(meta).should.equal('msg="value one,value \'two\',value ; three"'); }); }); diff --git a/test/lib/formatter.test.js b/test/lib/formatter.test.js index 6e6014c..9f7e175 100644 --- a/test/lib/formatter.test.js +++ b/test/lib/formatter.test.js @@ -35,7 +35,7 @@ describe('Formatter', () => { it('should format all options', () => { const options = { message: 'a message', level: 'error', meta: { foo: 'bar' }}; - formatter(options).should.equal('a message level=error foo=bar'); + formatter(options).should.equal('a message foo=bar level=error'); }); }); diff --git a/test/lib/logger.test.js b/test/lib/logger.test.js new file mode 100644 index 0000000..b1cefaf --- /dev/null +++ b/test/lib/logger.test.js @@ -0,0 +1,261 @@ +import sinon from 'sinon'; +import chai from 'chai'; +import chaiString from 'chai-string'; +import sinonChai from 'sinon-chai'; + +import Logger from '../../build/lib/logger'; + +chai.should(); +chai.use(chaiString); +chai.use(sinonChai); + +const winstonStub = ({ + log = () => {}, + add = () => {}, + remove = () => {}, + transports = {}, + levels = { info: true, warn: true, error: true }, + Console = () => {} +}) => { + return { + Logger: function () { + this.log = log; + this.add = add; + this.remove = remove; + this.levels = levels; + this.transports = transports; + }, + transports: { Console } + }; +}; + +describe('Logger', () => { + + it('should exist', () => { + Logger.should.exist; + }); + + it('should be able to instantiate', () => { + const logger = new Logger(); + logger.should.be.defined; + }); + + describe('#log', () => { + + it('should be able to log a message', () => { + const logSpy = sinon.spy(); + const winston = winstonStub({ log: logSpy }); + const logger = new Logger({ winston }); + logger.log('info', 'a message'); + logSpy.should.always.have.been.calledWithExactly('info', 'a message'); + }); + + it('should be able to use shorthand methods', () => { + const logSpy = sinon.spy(); + const winston = winstonStub({ log: logSpy }); + const logger = new Logger({ winston }); + logger.info('a message'); + logSpy.should.always.have.been.calledWithExactly('info', 'a message'); + }); + + it('should pass message and meta through', () => { + const logSpy = sinon.spy(); + const winston = winstonStub({ log: logSpy }); + const logger = new Logger({ winston }); + logger.log('info', 'a message', { foo: 'foo' }); + logSpy.should.always.have.been.calledWithExactly('info', 'a message', { foo: 'foo' }); + }); + + it('should pass meta only through', () => { + const logSpy = sinon.spy(); + const winston = winstonStub({ log: logSpy }); + const logger = new Logger({ winston }); + logger.log('info', { foo: 'foo' }); + logSpy.should.always.have.been.calledWithExactly('info', { foo: 'foo' }); + }); + + it('should convert Error message to meta', () => { + class MyError extends Error { }; + const logSpy = sinon.spy(); + const winston = winstonStub({ log: logSpy }); + const logger = new Logger({ winston }); + logger.log('info', new MyError('whoops!')); + logSpy.lastCall.args[1].should.have.property('error_message', 'whoops!'); + logSpy.lastCall.args[1].should.have.property('error_name', 'Error'); + logSpy.lastCall.args[1].error_stack.should.startWith('Error: whoops!'); + }); + + it('should combine Error message meta', () => { + class MyError extends Error { }; + const logSpy = sinon.spy(); + const winston = winstonStub({ log: logSpy }); + const logger = new Logger({ winston }); + logger.log('info', new MyError('whoops!'), { foo: 'foo' }); + logSpy.lastCall.args[1].should.have.property('error_message', 'whoops!'); + logSpy.lastCall.args[1].should.have.property('foo', 'foo'); + }); + + it('should handle message and Error meta', () => { + class MyError extends Error { }; + const logSpy = sinon.spy(); + const winston = winstonStub({ log: logSpy }); + const logger = new Logger({ winston }); + logger.log('info', 'a message', new MyError('whoops!')); + logSpy.lastCall.args[1].should.equal('a message'); + logSpy.lastCall.args[2].should.have.property('error_message', 'whoops!'); + }); + + it('should be able to send message, error and meta', () => { + class MyError extends Error { }; + const logSpy = sinon.spy(); + const winston = winstonStub({ log: logSpy }); + const logger = new Logger({ winston }); + logger.log('info', 'a message', new MyError('whoops!'), { extra: 'foo' }); + logSpy.lastCall.args[1].should.equal('a message'); + logSpy.lastCall.args[2].should.have.property('error_message', 'whoops!'); + logSpy.lastCall.args[2].should.have.property('extra', 'foo'); + }); + + it('should be able to send multiple metas, earlier arguments taking priority', () => { + const logSpy = sinon.spy(); + const winston = winstonStub({ log: logSpy }); + const logger = new Logger({ winston }); + logger.log('info', { extra: 'foo' }, { anotherExtra: 'bar' }, { extra: 'baz' }); + logSpy.should.always.have.been.calledWithExactly('info', { extra: 'foo', anotherExtra: 'bar' }); + }); + + }); + + describe('#addConsole', () => { + + it('should be able to add a console logger', () => { + const formatter = () => {}; + const addSpy = sinon.spy(); + const ConsoleSpy = sinon.spy(); + const winston = winstonStub({ add: addSpy, Console: ConsoleSpy }); + const logger = new Logger({ winston, formatter }); + logger.addConsole(); + addSpy.should.always.have.been.calledWithExactly(ConsoleSpy, { level: 'info', formatter }); + }); + + it('should be able to set the console logger\'s level', () => { + const addSpy = sinon.spy(); + const winston = winstonStub({ add: addSpy }); + const logger = new Logger({ winston }); + logger.addConsole('warn'); + addSpy.lastCall.args[1].should.have.property('level', 'warn'); + }); + + it('should be able to pass opts', () => { + const addSpy = sinon.spy(); + const winston = winstonStub({ add: addSpy }); + const logger = new Logger({ winston }); + logger.addConsole('info', { option: 'one' }); + addSpy.lastCall.args[1].should.have.property('option', 'one'); + }); + + it('should not be able to add if already added', () => { + const addSpy = sinon.spy(function () { this.transports.console = true; }); + const winston = winstonStub({ add: addSpy }); + const logger = new Logger({ winston }); + logger.addConsole(); + logger.addConsole(); + addSpy.should.have.been.calledOnce; + }); + + }); + + describe('#removeConsole', () => { + + it('should be able to remove', () => { + const removeSpy = sinon.spy(); + const winston = winstonStub({ remove: removeSpy, transports: { console: true } }); + const logger = new Logger({ winston }); + logger.removeConsole(); + removeSpy.should.always.have.been.calledWithExactly('console'); + }); + + it('should not be able to remove if not added', () => { + const removeSpy = sinon.spy(); + const winston = winstonStub({ remove: removeSpy }); + const logger = new Logger({ winston }); + logger.removeConsole(); + removeSpy.should.not.have.been.called; + }); + + }); + + describe('#addSplunk', () => { + + it('should be able to add a splunk logger', () => { + const addSpy = sinon.spy(); + const SplunkSpy = sinon.spy(); + const winston = winstonStub({ add: addSpy }); + const logger = new Logger({ winston, Splunk: SplunkSpy }); + logger.addSplunk('http://splunk.ft.com'); + addSpy.should.always.have.been.calledWithExactly(SplunkSpy, { level: 'info', splunkUrl: 'http://splunk.ft.com' }); + }); + + it('should be able to set the console logger\'s level', () => { + const addSpy = sinon.spy(); + const SplunkSpy = sinon.spy(); + const winston = winstonStub({ add: addSpy }); + const logger = new Logger({ winston, Splunk: SplunkSpy }); + logger.addSplunk('http://splunk.ft.com', 'warn'); + addSpy.should.always.have.been.calledWithExactly(SplunkSpy, { level: 'warn', splunkUrl: 'http://splunk.ft.com' }); + }); + + it('should return false if no `splunkUrl` supplied', () => { + const addSpy = sinon.spy(); + const SplunkSpy = sinon.spy(); + const winston = winstonStub({ add: addSpy }); + const logger = new Logger({ winston, Splunk: SplunkSpy }); + logger.addSplunk().should.be.false; + addSpy.should.not.have.been.called; + }); + + it('should not be able to add if already added', () => { + const addSpy = sinon.spy(function () { this.transports.splunk = true; }); + const winston = winstonStub({ add: addSpy }); + const logger = new Logger({ winston }); + logger.addSplunk('http://splunk.ft.com'); + logger.addSplunk('http://splunk.ft.com'); + addSpy.should.have.been.calledOnce; + }); + + }); + + describe('#removeSplunk', () => { + + it('should be able to remove', () => { + const removeSpy = sinon.spy(); + const winston = winstonStub({ remove: removeSpy, transports: { splunk: true } }); + const logger = new Logger({ winston }); + logger.removeSplunk(); + removeSpy.should.always.have.been.calledWithExactly('splunk'); + }); + + it('should not be able to remove if not added', () => { + const removeSpy = sinon.spy(); + const winston = winstonStub({ remove: removeSpy }); + const logger = new Logger({ winston }); + logger.removeSplunk(); + removeSpy.should.not.have.been.called; + }); + + }); + + describe('#clearLoggers', () => { + + it('should be able to clear loggers', () => { + const removeSpy = sinon.spy(); + const winston = winstonStub({ remove: removeSpy, transports: { console: true, splunk: true } }); + const logger = new Logger({ winston }); + logger.clearLoggers(); + removeSpy.should.have.been.calledWith('console'); + removeSpy.should.have.been.calledWith('splunk'); + }); + + }); + +}); diff --git a/test/lib/transports/splunk.test.js b/test/lib/transports/splunk.test.js index 09534e2..8353e69 100644 --- a/test/lib/transports/splunk.test.js +++ b/test/lib/transports/splunk.test.js @@ -24,7 +24,7 @@ describe('Splunk', () => { mockAgent.url.should.equal('http://splunk.ft.com/ft-next-front-page'); splunkTransport.log('error', 'a message', { field: 'value'}); mockAgent.send.called.should.be.true; - mockAgent.send.calledWith('a message level=error field=value').should.be.true; + mockAgent.send.calledWith('a message field=value level=error').should.be.true; }); it('should handle no message', () => { @@ -32,7 +32,7 @@ describe('Splunk', () => { send: sinon.spy() }; const splunkTransport = new Splunk({ splunkUrl: 'http://splunk.ft.com/ft-next-front-page', agent: mockAgent }); - splunkTransport.log('error', ''); + splunkTransport.log('error'); mockAgent.send.calledWith('level=error').should.be.true; }); diff --git a/test/logger.test.js b/test/logger.test.js deleted file mode 100644 index 30cf854..0000000 --- a/test/logger.test.js +++ /dev/null @@ -1,156 +0,0 @@ -import sinon from 'sinon'; -import chai from 'chai'; -import chaiString from 'chai-string'; -chai.should(); -chai.use(chaiString); - -import logger from '../build/main'; - -describe('Logger', () => { - - afterEach(() => { - logger.clearLoggers(); - }); - - it('should exist', () => { - logger.should.exist; - }); - - it('should be able to clear loggers', () => { - logger.addConsole(); - logger.clearLoggers(); - logger.logger.transports.should.be.empty; - }); - - describe('Logging', () => { - - let logSpy; - - beforeEach(() => { - logSpy = sinon.spy(logger.logger, 'log'); - }); - - afterEach(() => { - logSpy.restore(); - }); - - it('should be able to log a message', () => { - logger.log('info', 'a message'); - logSpy.calledWithExactly('info', 'a message').should.be.true; - }); - - it('should be able to use shorthand methods', () => { - logger.info('a message'); - logSpy.calledWithExactly('info', 'a message').should.be.true; - }); - - it('should pass message and meta through', () => { - logger.log('info', 'a message', { foo: 'foo' }); - logSpy.calledWithExactly('info', 'a message', { foo: 'foo' }).should.be.true; - }); - - it('should pass meta only through', () => { - logger.log('info', { foo: 'foo' }); - logSpy.calledWithExactly('info', { foo: 'foo' }).should.be.true; - }); - - it('should convert Error message to meta', () => { - class MyError extends Error { }; - logger.log('info', new MyError('whoops!')); - logSpy.lastCall.args[1].should.have.property('error_message', 'whoops!'); - logSpy.lastCall.args[1].should.have.property('error_name', 'Error'); - logSpy.lastCall.args[1].error_stack.should.startWith('Error: whoops!; at'); - }); - - it('should combine Error message meta', () => { - class MyError extends Error { }; - logger.log('info', new MyError('whoops!'), { foo: 'foo' }); - logSpy.lastCall.args[1].should.have.property('error_message', 'whoops!'); - logSpy.lastCall.args[1].should.have.property('foo', 'foo'); - }); - - it('should handle message and Error meta', () => { - class MyError extends Error { }; - logger.log('info', 'a message', new MyError('whoops!')); - logSpy.lastCall.args[1].should.equal('a message'); - logSpy.lastCall.args[2].should.have.property('error_message', 'whoops!'); - }); - - it('should be able to send message, error and meta', () => { - class MyError extends Error { }; - logger.log('info', 'a message', new MyError('whoops!'), { extra: 'foo' }); - logSpy.lastCall.args[1].should.equal('a message'); - logSpy.lastCall.args[2].should.have.property('error_message', 'whoops!'); - logSpy.lastCall.args[2].should.have.property('extra', 'foo'); - }); - - it('should be able to send multiple metas', () => { - logger.log('info', { extra: 'foo' }, { anotherExtra: 'bar' }, { extra: 'baz' }); - logSpy.calledWithExactly('info', { extra: 'foo', anotherExtra: 'bar' }).should.be.true; - }); - - }) - - describe('Console', () => { - - it('should be able to add', () => { - logger.addConsole('warn'); - logger.logger.transports.console.level.should.equal('warn'); - }); - - it('should have "info" level by default', () => { - logger.addConsole(); - logger.logger.transports.console.level.should.equal('info'); - }); - - it('should not be able to add if already added', () => { - logger.addConsole(); - (() => logger.addConsole()).should.not.throw(Error); - }); - - it('should be able to remove', () => { - logger.addConsole(); - logger.removeConsole(); - logger.logger.transports.should.be.empty; - }); - - it('should not be able to remove if not added', () => { - (() => logger.removeConsole()).should.not.throw(Error); - }); - - }); - - describe('Splunk', () => { - - it('should be able to add', () => { - logger.addSplunk('http://splunk.ft.com', 'warn'); - logger.logger.transports.splunk.level.should.equal('warn'); - }); - - it('should have "info" level by default', () => { - logger.addSplunk('http://splunk.ft.com'); - logger.logger.transports.splunk.level.should.equal('info'); - }); - - it('should return false if no `splunkUrl` supplied', () => { - logger.addSplunk().should.equal(false); - }); - - it('should not be able to add if already added', () => { - logger.addSplunk('http://splunk.ft.com'); - (() => logger.addSplunk('http://splunk.ft.com')).should.not.throw(Error); - }); - - it('should be able to remove', () => { - logger.addSplunk('http://splunk.ft.com'); - logger.removeSplunk(); - logger.logger.transports.should.be.empty; - }); - - it('should not be able to remove if not added', () => { - (() => logger.removeSplunk()).should.not.throw(Error); - }); - - }); - -});