diff --git a/README.md b/README.md index 88e2258..59c85c4 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,10 @@ log.levels('foo', log.WARN) // set level of stream named 'foo' to WARN * `writers`: Map of log level string names to console functions, default is `null`. Use this to customize the functions used when `console` is `true`, see [writers](#writers). +* `pedantic`: A boolean or string which indicates messages should be reformatted to end with a period. If a string, the string is used instead of a period. +* `capitalize`: A boolean which indicates all messages should be reformatted to begin with an uppercase letter. +* `normalize`: A shortcut for `pedantic: true` and `capitalize: true`. +* `formatter`: A custom function which accepts a string and returns a string. Applied to all messages after parameter interpolation. If you specify any unknown properties in the configuration then these are considered *persistent fields* and are added to every log record. This is a convenient way to add labels for sub-components to log records. diff --git a/index.js b/index.js index 4e07847..a7281f1 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,10 @@ var os = require('os'); var path = require('path'), basename = path.basename; var util = require('util'); var circular = require('circular'); -var merge = require('cli-util').merge; +var utils = require('cli-util'); +var merge = utils.merge; +var ucfirst = utils.ucfirst; +var pedantic = utils.pedantic; var pkg = require(path.join(__dirname, 'package.json')); var major = parseInt(pkg.version.split('.')[0]), z; @@ -51,7 +54,11 @@ var defaults = { writers: null, level: null, stream: null, - streams: null + streams: null, + normalize: false, + pedantic: false, + capitalize: false, + formatter: null } /** @@ -409,11 +416,17 @@ Logger.prototype.serialize = function(k, v) { */ Logger.prototype.write = function(level, record, parameters, force) { var i, target, listeners = this.listeners('write'), json, params, event; - var msg = '' + record.msg, prefix; + var msg = '' + record.msg, prefix, conf = this.conf; + var formatter = conf.formatter, pedantic = conf.pedantic, normalize = conf.normalize, + capitalize = conf.capitalize; level = level || record.level; params = parameters.slice(0); params.unshift(record.msg); record.msg = util.format.apply(util, params); + // if we have not just stringified an object or array, try to format the string. + if (!/^[{\[].*[}\]]$/.test(record.msg)) { + record.msg = this._formatMessage(record.msg); + } //console.log('writing %j', record); for(i = 0;i < this.streams.length;i++) { target = this.streams[i]; @@ -463,6 +476,36 @@ Logger.prototype.write = function(level, record, parameters, force) { return (listeners.length === 0 && event !== undefined); } +/** + * Potentially runs one or more of `utils.pedantic()` or `utils.ucfirst()` upon a log message. + * @api private + * @param {string} message Log message to reformat + * @returns {string} + */ +Logger.prototype._formatMessage = function _formatMessage(message) { + var conf = this.conf, + formatter = conf.formatter; + // if option "normalize" is true, the string "like this" should look "Like this." + // if set, it will override options "pedantic" and "ucfirst". + if (conf.normalize) { + message = ucfirst(pedantic(message)); + } + else { + // if option "pedantic" is truthy, end the message with a period, or a user-defined string. + if (conf.pedantic) { + message = pedantic(message, typeof conf.pedantic === 'string' && conf.pedantic); + } + // if option "capitalize" is truthy, capitalize the first letter of the message. + if (conf.capitalize) { + message = ucfirst(message); + } + } + if (formatter && typeof formatter === 'function') { + message = formatter(message); + } + return message; +}; + /** * Log a message. * diff --git a/test/unit/formatters.js b/test/unit/formatters.js new file mode 100644 index 0000000..bda611a --- /dev/null +++ b/test/unit/formatters.js @@ -0,0 +1,136 @@ +var expect = require('chai').expect; +var logger = require('../..'); + +describe('cli-logger:', function () { + it('should write using normalize flag if present', function (done) { + var msg = 'mock write %s'; + var parameters = ['info']; + var expected = 'Mock write info.'; + var name = 'mock-write-logger'; + var conf = {name: name, json: true, streams: [ + {path: 'log/mock-write.log'} + ], normalize: true}; + var log = logger(conf); + log.on('write', function (record) { + expect(record.msg).to.eql(expected); + expect(record.err.message).to.eql('boom'); + done(); + }); + log.info(new Error('boom'), msg, parameters[0]); + }); + it('should prefer normalize flag over custom pedantic string', function (done) { + var msg = 'mock write %s'; + var parameters = ['info']; + var expected = 'Mock write info.'; + var name = 'mock-write-logger'; + var conf = {name: name, json: true, streams: [ + {path: 'log/mock-write.log'} + ], normalize: true, pedantic: '?'}; + var log = logger(conf); + log.on('write', function (record) { + expect(record.msg).to.eql(expected); + expect(record.err.message).to.eql('boom'); + done(); + }); + log.info(new Error('boom'), msg, parameters[0]); + }); + it('should not attempt format a stringified object', function (done) { + var msg = {foo: 'bar'}; + var expected = JSON.stringify(msg); + var name = 'mock-write-logger'; + var conf = {name: name, json: false, streams: [ + {path: 'log/mock-write.log'} + ], normalize: true, pedantic: true, capitalize: true, formatter: function formatter(msg) { + return msg.replace(/"/g, "'"); + }}; + var log = logger(conf); + log.on('write', function (record) { + expect(record.msg).to.eql(expected); + done(); + }); + log.info(msg); + }); + it('should not attempt format a stringified array', function (done) { + var msg = ['bar']; + var expected = JSON.stringify(msg); + var name = 'mock-write-logger'; + var conf = {name: name, json: false, streams: [ + {path: 'log/mock-write.log'} + ], normalize: true, pedantic: true, capitalize: true, formatter: function formatter(msg) { + return msg.replace(/"/g, "'"); + }}; + var log = logger(conf); + log.on('write', function (record) { + expect(record.msg).to.eql(expected); + done(); + }); + log.info(msg); + }); + it('should capitalize the first letter of the message', function (done) { + var msg = 'mock write %s'; + var parameters = ['info']; + var expected = 'Mock write info'; + var name = 'mock-write-logger'; + var conf = {name: name, json: true, streams: [ + {path: 'log/mock-write.log'} + ], capitalize: true}; + var log = logger(conf); + log.on('write', function (record) { + expect(record.msg).to.eql(expected); + expect(record.err.message).to.eql('boom'); + done(); + }); + log.info(new Error('boom'), msg, parameters[0]); + }); + it('should end the string with a period', function (done) { + var msg = 'mock write %s'; + var parameters = ['info']; + var expected = 'mock write info.'; + var name = 'mock-write-logger'; + var conf = {name: name, json: true, streams: [ + {path: 'log/mock-write.log'} + ], pedantic: true}; + var log = logger(conf); + log.on('write', function (record) { + expect(record.msg).to.eql(expected); + expect(record.err.message).to.eql('boom'); + done(); + }); + log.info(new Error('boom'), msg, parameters[0]); + + }); + it('should leverage custom pedantic "period" if present', function (done) { + var msg = 'mock write info'; + var expected = 'mock write info!@!~!~!~!11!1!'; + var name = 'mock-write-logger'; + var conf = {name: name, json: true, streams: [ + {path: 'log/mock-write.log'} + ], pedantic: '!@!~!~!~!11!1!'}; + var log = logger(conf); + log.on('write', function (record) { + expect(record.msg).to.eql(expected); + expect(record.err.message).to.eql('boom'); + done(); + }); + log.info(new Error('boom'), msg); + }); + it('should allow a user-defined formatter', function (done) { + var msg = 'mock write %s'; + var formatter = function formatter(msg) { + return msg.replace('write', 'WRITE'); + }; + var parameters = ['info']; + var expected = 'mock WRITE info'; + var name = 'mock-write-logger'; + var conf = {name: name, json: true, streams: [ + {path: 'log/mock-write.log'} + ], formatter: formatter}; + var log = logger(conf); + log.on('write', function (record) { + expect(record.msg).to.eql(expected); + expect(record.err.message).to.eql('boom'); + done(); + }); + log.info(new Error('boom'), msg, parameters[0]); + }); +});