├── test ├── lumber │ ├── encoders │ │ ├── encoder-test.js │ │ ├── text-test.js │ │ ├── json-test.js │ │ └── xml-test.js │ ├── transports │ │ ├── transport-test.js │ │ ├── console-test.js │ │ ├── file-test.js │ │ └── webservice-test.js │ ├── encoders-test.js │ ├── transports-test.js │ ├── logger-test.js │ └── common-test.js ├── coverage.js └── lumber-test.js ├── .gitignore ├── package.json ├── lib ├── lumber │ ├── encoders.js │ ├── transports.js │ ├── transports │ │ ├── transport.js │ │ ├── console.js │ │ ├── webservice.js │ │ └── file.js │ ├── encoders │ │ ├── encoder.js │ │ ├── json.js │ │ ├── xml.js │ │ └── text.js │ ├── logger.js │ └── common.js └── lumber.js ├── LICENSE └── README.md /test/lumber/encoders/encoder-test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/lumber/transports/transport-test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | coverage.xml 4 | results.xml 5 | jshint.xml 6 | lib-cov/ 7 | *~ 8 | \#*# -------------------------------------------------------------------------------- /test/coverage.js: -------------------------------------------------------------------------------- 1 | // test/coverage.js 2 | var covererageOn = process.argv.some(function(arg) { 3 | return (/^--cover/).test(arg); 4 | }); 5 | 6 | if (covererageOn) { 7 | //console.log('Code coverage on'); 8 | 9 | exports.require = function(path) { 10 | var instrumentedPath = path.replace('/lib', '/lib-cov'); 11 | 12 | try { 13 | require.resolve(instrumentedPath); 14 | return require(instrumentedPath); 15 | } catch (e) { 16 | //console.log('Coverage on, but no instrumented file found at ' 17 | //+ instrumentedPath); 18 | return require(path); 19 | } 20 | }; 21 | } else { 22 | //console.log('Code coverage off'); 23 | exports.require = require; 24 | } -------------------------------------------------------------------------------- /test/lumber/encoders-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * encoders-test.js: Tests to ensure core encoders load properly 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('../coverage'), 14 | encoders = cov.require('../lib/lumber/encoders'); 15 | 16 | vows.describe('Encoders').addBatch({ 17 | 'encoders loader': { 18 | topic: function() { 19 | return null; 20 | }, 21 | 'should have the correct exports': function() { 22 | assert.isObject(encoders); 23 | assert.isFunction(encoders.Text); 24 | assert.isFunction(encoders.Json); 25 | assert.isFunction(encoders.Xml); 26 | } 27 | } 28 | }).export(module); -------------------------------------------------------------------------------- /test/lumber/transports-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * transports-test.js: Tests to ensure core transports load properly 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('../coverage'), 14 | trans = cov.require('../lib/lumber/transports'); 15 | 16 | vows.describe('Transports').addBatch({ 17 | 'transports loader': { 18 | topic: function() { 19 | return null; 20 | }, 21 | 'should have the correct exports': function() { 22 | assert.isObject(trans); 23 | assert.isFunction(trans.Console); 24 | assert.isFunction(trans.File); 25 | assert.isFunction(trans.Webservice); 26 | } 27 | } 28 | }).export(module); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lumber", 3 | "description": "A custom, async, extensible logging library", 4 | "version": "0.0.6", 5 | "author": "Chad Engler ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/englercj/lumber.git" 9 | }, 10 | "bugs": "https://github.com/englercj/lumber/issues", 11 | "keywords": [ "log", "logging", "logger", "tools", "cli" ], 12 | "dependencies": { 13 | "colors": "0.6.x", 14 | "eyes": "0.1.x", 15 | "pkginfo": "0.2.x", 16 | "async": "0.1.x", 17 | "cycle": "1.0.x", 18 | "stack-trace": "0.0.x", 19 | "dateformat": ">= 1" 20 | }, 21 | "devDependencies": { 22 | "vows": "0.6.x" 23 | }, 24 | "main": "./lib/lumber", 25 | "scripts": { 26 | "test": "vows --spec --isolate" 27 | }, 28 | "engines": { 29 | "node": ">= 0.6.0" 30 | } 31 | } -------------------------------------------------------------------------------- /lib/lumber/encoders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * encoders.js: Include for core encoders 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENCE 7 | * 8 | **/ 9 | 10 | var encoders = exports; 11 | 12 | ////////// 13 | // Required Includes 14 | /////////////////////////// 15 | var fs = require('fs'), 16 | path = require('path'), 17 | common = require('./common'); 18 | 19 | ////////// 20 | // Setup getters for encoders 21 | /////////////////////////// 22 | fs.readdirSync(path.join(__dirname, 'encoders')).forEach(function(file) { 23 | //ignore non-js files, and base class 24 | if(file.match(/\.js$/) === null || file == 'encoder.js') return; 25 | 26 | var e = file.replace('.js', ''), 27 | name = common.titleCase(e); 28 | 29 | //ignore base class 30 | encoders.__defineGetter__(name, function() { 31 | return require('./encoders/' + e)[name]; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /lib/lumber/transports.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * transports.js: Include for core transports 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | var transports = exports; 11 | 12 | ////////// 13 | // Required Includes 14 | /////////////////////////// 15 | var fs = require('fs'), 16 | path = require('path'), 17 | common = require('./common'); 18 | 19 | ////////// 20 | // Setup getters for transports 21 | /////////////////////////// 22 | fs.readdirSync(path.join(__dirname, 'transports')).forEach(function(file) { 23 | //ignore non-js files, and base class 24 | if(file.match(/\.js$/) === null || file == 'transport.js') return; 25 | 26 | var t = file.replace('.js', ''), 27 | name = common.titleCase(t); 28 | 29 | //ignore base class 30 | transports.__defineGetter__(name, function() { 31 | return require('./transports/' + t)[name]; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /lib/lumber/transports/transport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * transport.js: Base Transport interface 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | ////////// 11 | // Required Includes 12 | /////////////////////////// 13 | var events = require('events'), 14 | util = require('util'), 15 | lumber = require('../../lumber'); 16 | 17 | /** 18 | * Base Transport 19 | * @interface 20 | */ 21 | var Transport = exports.Transport = function(options) { 22 | events.EventEmitter.call(this); 23 | }; 24 | 25 | ////////// 26 | // Inherits from EventEmitter 27 | /////////////////////////// 28 | util.inherits(Transport, events.EventEmitter); 29 | 30 | ////////// 31 | // Public Methods 32 | /////////////////////////// 33 | /** 34 | * Logs the string via this transport, using 35 | * the encoder specified 36 | * @param {object} args The arguments for the log 37 | */ 38 | Transport.prototype.log = function(args) {}; -------------------------------------------------------------------------------- /lib/lumber/encoders/encoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * encoder.js: Base Encoder interface 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | ////////// 11 | // Required Includes 12 | /////////////////////////// 13 | var events = require('events'), 14 | util = require('util'); 15 | 16 | /** 17 | * Base Encoder 18 | * @interface 19 | */ 20 | var Encoder = exports.Encoder = function(options) { 21 | events.EventEmitter.call(this); 22 | }; 23 | 24 | ////////// 25 | // Inherits from EventEmitter 26 | /////////////////////////// 27 | util.inherits(Encoder, events.EventEmitter); 28 | 29 | ////////// 30 | // Public Methods 31 | /////////////////////////// 32 | /** 33 | * Encodes the passed string into the format 34 | * implemented by this encoder 35 | * @return {string} Encoded string 36 | * @param {string} str String to encode 37 | */ 38 | Encoder.prototype.encode = function(str) { 39 | return str; 40 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Chad Engler 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. -------------------------------------------------------------------------------- /test/lumber/transports/console-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * console-test.js: Tests the console transport 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('../../coverage'), 14 | lumber = cov.require('../lib/lumber'); 15 | 16 | vows.describe('Console').addBatch({ 17 | 'console transport': { 18 | topic: function() { 19 | return new lumber.transports.Console(); 20 | }, 21 | 'has': { 22 | 'the correct defaults': function(con) { 23 | assert.instanceOf(con.encoder, lumber.encoders.Text); 24 | assert.isFunction(con.encoder.encode); 25 | assert.equal(con.level, 'info'); 26 | }, 27 | 'the correct functions': function(con) { 28 | assert.isFunction(con.log); 29 | } 30 | }/*, 31 | 'should': { 32 | topic: function(con) { 33 | process.stdout.on('data', this.callback); 34 | 35 | var logger = new lumber.Logger({ transports: [con] }); 36 | logger.info('Some info data', this.callback); 37 | }, 38 | 'write to the proper stream': function(data) { 39 | assert.isTrue(true); 40 | }, 41 | 'write properly enocoded data': function(data) { 42 | assert.equal(data, 'info: Some info data'); 43 | } 44 | }*/ 45 | } 46 | }).export(module); -------------------------------------------------------------------------------- /lib/lumber.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * lumber.js: Include for lumber 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | var lumber = exports; 11 | 12 | ////////// 13 | // Required Includes 14 | /////////////////////////// 15 | 16 | ////////// 17 | // Expose pkginfo 18 | /////////////////////////// 19 | require('pkginfo')(module, 'version'); 20 | 21 | ////////// 22 | // Include logging transports 23 | /////////////////////////// 24 | lumber.transports = require('./lumber/transports'); 25 | 26 | ////////// 27 | // Include logging transports 28 | /////////////////////////// 29 | lumber.encoders = require('./lumber/encoders'); 30 | 31 | ////////// 32 | // Expose utilities 33 | /////////////////////////// 34 | lumber.util = require('./lumber/common'); 35 | 36 | ////////// 37 | // Expose core 38 | /////////////////////////// 39 | lumber.Logger = require('./lumber/logger').Logger; 40 | lumber.Transport = require('./lumber/transports/transport').Transport; 41 | lumber.Encoder = require('./lumber/encoders/encoder').Encoder; 42 | 43 | ////////// 44 | // Expose defaults 45 | /////////////////////////// 46 | lumber.defaults = { 47 | levels: { 48 | silent: -1, 49 | error: 0, 50 | warn: 1, 51 | info: 2, 52 | verbose: 3, 53 | debug: 4, 54 | silly: 5 55 | }, 56 | colors: { 57 | error: 'red', 58 | warn: 'yellow', 59 | info: 'cyan', 60 | verbose: 'magenta', 61 | debug: 'green', 62 | silly: 'grey' 63 | } 64 | }; -------------------------------------------------------------------------------- /lib/lumber/transports/console.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * console.js: Console transport 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | ////////// 11 | // Required Includes 12 | /////////////////////////// 13 | var util = require('util'), 14 | lumber = require('../../lumber'); 15 | 16 | /** 17 | * Console Transport 18 | * @implements {Transport} 19 | */ 20 | var Console = exports.Console = function(options) { 21 | var self = this; 22 | 23 | lumber.Transport.call(self); 24 | 25 | options = options || {}; 26 | 27 | self.encoder = lumber.util.checkOption(options.encoder, 'text'); 28 | self.level = lumber.util.checkOption(options.level, 'info'); 29 | 30 | self.name = 'console'; 31 | 32 | if(typeof(self.encoder) == 'string') { 33 | var e = lumber.util.titleCase(self.encoder); 34 | 35 | if(lumber.encoders[e]) { 36 | self.encoder = new lumber.encoders[e](); 37 | } else { 38 | throw new Error('Unknown encoder passed: ' + self.encoder); 39 | } 40 | } 41 | }; 42 | 43 | ////////// 44 | // Inherits from EventEmittxer 45 | /////////////////////////// 46 | util.inherits(Console, lumber.Transport); 47 | 48 | ////////// 49 | // Public Methods 50 | /////////////////////////// 51 | /** 52 | * Logs the string via the stdout console 53 | * @param {object} args The arguments for the log 54 | * @param {function} cb The callback to call after logging 55 | */ 56 | Console.prototype.log = function(args, cb) { 57 | var self = this, 58 | msg = self.encoder.encode(args.level, args.msg, args.meta); 59 | 60 | if(args.level === 'error') 61 | console.error(msg); 62 | else 63 | console.log(msg); 64 | 65 | if(cb) cb(null, msg, args.level, self.name); 66 | }; -------------------------------------------------------------------------------- /lib/lumber/encoders/json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * json.js: JSON Encoder 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | ////////// 11 | // Required Includes 12 | /////////////////////////// 13 | var util = require('util'), 14 | dateFormat = require('dateformat'), 15 | common = require('../common'), 16 | Encoder = require('./encoder').Encoder; 17 | 18 | /** 19 | * JSON Encoder 20 | * @constructor 21 | * @implements {Encoder} 22 | */ 23 | var Json = exports.Json = function(options) { 24 | var self = this; 25 | 26 | Encoder.call(self); 27 | 28 | options = options || {}; 29 | 30 | self.colorize = common.checkOption(options.colorize, false); 31 | self.timestamp = common.checkOption(options.timestamp, true); 32 | self.headFormat = common.checkOption(options.headFormat, '%L'); 33 | self.dateFormat = common.checkOption(options.dateFormat, 'isoDateTime'); 34 | 35 | self.contentType = 'application/json'; 36 | self.encoding = 'utf8'; 37 | }; 38 | 39 | ////////// 40 | // Inherits from Encoder 41 | /////////////////////////// 42 | util.inherits(Json, Encoder); 43 | 44 | ////////// 45 | // Public Methods 46 | /////////////////////////// 47 | /** 48 | * Encodes the passed string into JSON format 49 | * @return {string} Encoded string 50 | * @param {string} level The level of this message 51 | * @param {string} msg The message to encode 52 | * @param {object} meta The metadata of this log 53 | */ 54 | Json.prototype.encode = function(level, msg, meta) { 55 | var self = this, 56 | head = self.headFormat.replace('%l', level.toLowerCase()).replace('%L', level.toUpperCase()), 57 | time = dateFormat(new Date(), self.dateFormat), 58 | obj = { 59 | level: level, 60 | head: head, 61 | message: msg 62 | }; 63 | 64 | if(self.timestamp) obj.timestamp = time; 65 | if(meta) obj.meta = meta; 66 | 67 | return JSON.stringify(obj); 68 | }; -------------------------------------------------------------------------------- /test/lumber/transports/file-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * file-test.js: Tests the file transport 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('../../coverage'), 14 | lumber = cov.require('../lib/lumber'); 15 | 16 | vows.describe('File').addBatch({ 17 | 'file transport': { 18 | topic: function() { 19 | return new lumber.transports.File(); 20 | }, 21 | 'has': { 22 | 'the correct defaults': function(trans) { 23 | assert.instanceOf(trans.encoder, lumber.encoders.Json); 24 | assert.isFunction(trans.encoder.encode); 25 | assert.equal(trans.level, 'info'); 26 | assert.equal(trans.filename, path.resolve('app.log')); 27 | }, 28 | 'the correct functions': function(trans) { 29 | assert.isFunction(trans.log); 30 | } 31 | }, 32 | 'should': { 33 | topic: function(trans) { 34 | try { fs.unlinkSync(path.resolve('app.log')); } catch(e) {} 35 | 36 | var logger = new lumber.Logger({ transports: [trans] }); 37 | 38 | logger.log('info', 'A message'); 39 | logger.on('log', this.callback); 40 | }, 41 | 'create the proper file': function(err, msg, level, name, filename) { 42 | var f; 43 | try { f = fs.statSync(path.resolve('app.log')); } catch(e) {} 44 | 45 | assert.isTrue(!err); 46 | assert.isTrue(!!f); 47 | }, 48 | 'pass the correct params': function(err, msg, level, name, filename) { 49 | assert.isTrue(!err); 50 | assert.equal(level, 'info'); 51 | assert.equal(name, 'file'); 52 | assert.equal(filename, path.resolve('app.log')); 53 | }, 54 | 'write properly enocoded data': function(err, msg, level, name, filename) { 55 | assert.isTrue(!err); 56 | assert.equal(msg.trim(), fs.readFileSync(path.resolve('app.log'), 'utf8').trim()); 57 | }, 58 | teardown: function(err) { 59 | try { fs.unlinkSync(path.resolve('app.log')); } catch(e) {} 60 | } 61 | } 62 | } 63 | }).export(module); -------------------------------------------------------------------------------- /test/lumber-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * lumber-test.js: Tests to ensure lumber exports itself correctly 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('./coverage'), 14 | lumber = cov.require('../lib/lumber'); 15 | 16 | vows.describe('Lumber').addBatch({ 17 | 'lumber module': { 18 | topic: function() { 19 | return null; 20 | }, 21 | 'should have the correct exports': function() { 22 | //global export 23 | assert.isObject(lumber); 24 | //transports 25 | assert.isObject(lumber.transports); 26 | assert.isFunction(lumber.transports.Console); 27 | assert.isFunction(lumber.transports.File); 28 | assert.isFunction(lumber.transports.Webservice); 29 | //encoders 30 | assert.isObject(lumber.encoders); 31 | assert.isFunction(lumber.encoders.Json); 32 | assert.isFunction(lumber.encoders.Xml); 33 | assert.isFunction(lumber.encoders.Text); 34 | //utils 35 | assert.isObject(lumber.util); 36 | //core 37 | assert.isFunction(lumber.Logger); 38 | assert.isFunction(lumber.Transport); 39 | assert.isFunction(lumber.Encoder); 40 | //config 41 | assert.isObject(lumber.defaults); 42 | assert.isObject(lumber.defaults.levels); 43 | assert.isObject(lumber.defaults.colors); 44 | 45 | //levels functions, for default logger 46 | /* 47 | Object.keys(lumber.defaults.levels).forEach(function(k) { 48 | assert.isFunction(lumber[k]); 49 | }); 50 | */ 51 | }, 52 | 'should': { 53 | topic: function() { 54 | fs.readFile(path.join(__dirname, '..', 'package.json'), this.callback); 55 | }, 56 | 'have the correct version': function(err, data) { 57 | assert.isNull(err); 58 | var s = JSON.parse(data.toString()); 59 | assert.equal(lumber.version, s.version); 60 | } 61 | } 62 | } 63 | }).export(module); -------------------------------------------------------------------------------- /lib/lumber/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * logger.js: Core logger functionality 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | ////////// 11 | // Required Includes 12 | /////////////////////////// 13 | var events = require('events'), 14 | util = require('util'), 15 | async = require('async'), 16 | lumber = require('../lumber'), 17 | Stream = require('stream').Stream; 18 | 19 | /** 20 | * Core Logger class that does the work of logging 21 | * via one or more transports 22 | * @constructor 23 | * @param {object} options The options for this logger 24 | */ 25 | var Logger = exports.Logger = function(options) { 26 | var self = this; 27 | 28 | events.EventEmitter.call(self); 29 | 30 | options = options || {}; 31 | 32 | self.levels = options.levels || lumber.defaults.levels; 33 | self.colors = options.colors || lumber.defaults.colors; 34 | self.transports = options.transports || [new lumber.transports.Console()]; 35 | self.level = options.level || 'info'; 36 | 37 | //create functions for log levels 38 | Object.keys(self.levels).forEach(function(key) { 39 | self[key] = function() { 40 | var args = Array.prototype.slice.call(arguments); 41 | args.unshift(key); 42 | self.log.apply(self, args); 43 | }; 44 | }); 45 | 46 | //pass alongs 47 | self.transports.forEach(function(trans) { 48 | trans.parent = self; 49 | trans.encoder.colors = self.colors; 50 | }); 51 | }; 52 | 53 | ////////// 54 | // Inherits from EventEmitter 55 | /////////////////////////// 56 | util.inherits(Logger, events.EventEmitter); 57 | 58 | ////////// 59 | // Public Methods 60 | /////////////////////////// 61 | Logger.prototype.log = function() { 62 | var self = this, 63 | args = lumber.util.prepareArgs(Array.prototype.slice.call(arguments)), 64 | cb = args.cb, 65 | done = 0, 66 | errors = []; 67 | 68 | async.forEach(self.transports, function(trans, next) { 69 | //if we aren't a silent level && 70 | //this isn't a silent log && 71 | //this log's level <= this transport's level 72 | if(self.levels[self.level] >= 0 && 73 | self.levels[args.level] >= 0 && 74 | self.levels[args.level] <= self.levels[trans.level]) { 75 | trans.log(args, function() { 76 | var a = Array.prototype.slice.call(arguments); 77 | a.unshift('log'); 78 | self.emit.apply(self, a); 79 | next(); 80 | }); 81 | } else { 82 | next(); 83 | } 84 | }, function(err) { 85 | self.emit('logged', err); 86 | if(cb) cb(err); 87 | }); 88 | }; -------------------------------------------------------------------------------- /lib/lumber/encoders/xml.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * xml.js: JSON Encoder 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | ////////// 11 | // Required Includes 12 | /////////////////////////// 13 | var util = require('util'), 14 | dateFormat = require('dateformat'), 15 | common = require('../common'), 16 | Encoder = require('./encoder').Encoder; 17 | 18 | /** 19 | * XML Encoder 20 | * @constructor 21 | * @implements {Encoder} 22 | */ 23 | var Xml = exports.Xml = function(options) { 24 | var self = this; 25 | 26 | Encoder.call(self); 27 | 28 | options = options || {}; 29 | 30 | self.colorize = common.checkOption(options.colorize, false); 31 | self.timestamp = common.checkOption(options.timestamp, true); 32 | self.headFormat = common.checkOption(options.headFormat, '%L'); 33 | self.dateFormat = common.checkOption(options.dateFormat, 'isoDateTime'); 34 | 35 | self.contentType = 'text/xml'; 36 | self.encoding = 'utf8'; 37 | }; 38 | 39 | ////////// 40 | // Inherits from Encoder 41 | /////////////////////////// 42 | util.inherits(Xml, Encoder); 43 | 44 | ////////// 45 | // Public Methods 46 | /////////////////////////// 47 | /** 48 | * Encodes the passed string into XML format 49 | * @return {string} Encoded string 50 | * @param {string} level The level of this message 51 | * @param {string} msg The message to encode 52 | * @param {object} meta The metadata of this log 53 | */ 54 | Xml.prototype.encode = function(level, msg, meta) { 55 | var self = this, 56 | head = self.headFormat.replace('%l', level.toLowerCase()).replace('%L', level.toUpperCase()), 57 | time = self._encodeObj(new Date()), 58 | log = ''; 59 | 60 | log += '' + head + ''; 61 | log += '' + self._encodeObj(msg) + ''; 62 | 63 | if(meta) 64 | log += '' + self._encodeObj(meta) + ''; 65 | 66 | log += ''; 67 | return log; 68 | }; 69 | 70 | ////////// 71 | // Private Methods 72 | /////////////////////////// 73 | Xml.prototype._encodeObj = function(obj) { 74 | var self = this, nodes = ''; 75 | 76 | if(obj.constructor == Date) { 77 | nodes = dateFormat(obj, self.dateFormat); 78 | } 79 | else if(obj.constructor == Object) { 80 | Object.keys(obj).forEach(function(key) { 81 | nodes += '<' + key + '>' + self._encodeObj(obj[key]) + ''; 82 | }); 83 | } 84 | else if(obj.constructor == Array) { 85 | obj.forEach(function(val) { 86 | nodes += '' + self._encodeObj(val) + ''; 87 | }); 88 | } 89 | else nodes = obj.toString(); 90 | 91 | return nodes; 92 | }; -------------------------------------------------------------------------------- /test/lumber/transports/webservice-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * webservice-test.js: Tests the webservice transport 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | http = require('http'), 12 | vows = require('vows'), 13 | assert = require('assert'), 14 | cov = require('../../coverage'), 15 | lumber = cov.require('../lib/lumber'); 16 | 17 | vows.describe('Webservice').addBatch({ 18 | 'webservice transport': { 19 | topic: function() { 20 | return new lumber.transports.Webservice(); 21 | }, 22 | 'has': { 23 | 'the correct defaults': function(trans) { 24 | assert.instanceOf(trans.encoder, lumber.encoders.Json); 25 | assert.isFunction(trans.encoder.encode); 26 | assert.equal(trans.level, 'info'); 27 | assert.equal(trans.method, 'POST'); 28 | assert.deepEqual(trans.headers, { 'Content-Type': 'application/json' }); 29 | assert.isNull(trans.url); 30 | assert.isNull(trans.auth); 31 | assert.isFalse(trans.secure); 32 | }, 33 | 'the correct functions': function(trans) { 34 | assert.isFunction(trans.log); 35 | } 36 | }, 37 | 'should': { 38 | topic: function(trans) { 39 | var logger = new lumber.Logger({ transports: [new lumber.transports.Webservice({ url: 'http://localhost:91234' })] }), 40 | data, that = this, 41 | server = http.createServer(function(req, res) { 42 | req.on('data', function(chunk) { 43 | if(!data) data = chunk; 44 | else data += chunk; 45 | }); 46 | 47 | req.on('end', function() { 48 | res.writeHead(200, {'Content-Type': 'application/json'}); 49 | res.end(JSON.stringify({ works: 'yeah' })); 50 | }); 51 | }).listen(91234, '127.0.0.1', function() { 52 | logger.log('info', 'A message'); 53 | logger.on('log', function() { 54 | var args = Array.prototype.slice.call(arguments); 55 | args.push(data.toString()); 56 | that.callback.apply(that, args); 57 | }); 58 | }); 59 | }, 60 | 'get the correct response': function(err, msg, level, name, url, statusCode, resData, postData) { 61 | assert.isTrue(!err); 62 | assert.equal(statusCode, 200); 63 | assert.equal(resData, JSON.stringify({ works: 'yeah' })); 64 | }, 65 | 'pass the correct params': function(err, msg, level, name, url, statusCode, resData, postData) { 66 | assert.isTrue(!err); 67 | assert.equal(level, 'info'); 68 | assert.equal(name, 'webservice'); 69 | assert.equal(url, 'http://localhost:91234'); 70 | }, 71 | 'post the properly encoded data': function(err, msg, level, name, url, statusCode, resData, postData) { 72 | assert.isTrue(!err); 73 | assert.equal(msg.trim(), postData.trim()); 74 | } 75 | } 76 | } 77 | }).export(module); -------------------------------------------------------------------------------- /test/lumber/logger-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * logger-test.js: Tests to ensure logger class functions properly 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('../coverage'), 14 | lumber = cov.require('../lib/lumber'); 15 | 16 | vows.describe('Logger').addBatch({ 17 | 'logger module': { 18 | topic: function() { 19 | return new lumber.Logger(); 20 | }, 21 | 'has': { 22 | 'the correct deaults': function(logger) { 23 | assert.isObject(logger.levels); 24 | assert.deepEqual(logger.levels, lumber.defaults.levels); 25 | assert.deepEqual(logger.colors, lumber.defaults.colors); 26 | }, 27 | 'the correct functions': function(logger) { 28 | assert.isFunction(logger.log); 29 | 30 | Object.keys(logger.levels).forEach(function(key) { 31 | assert.isFunction(logger[key]); 32 | }); 33 | } 34 | }, 35 | 'should': { 36 | topic: function() { 37 | var trans = { level: 'info', log: function(){}, encoder: {} }, 38 | logger = new lumber.Logger({ transports: [trans] }); 39 | 40 | return { trans: trans, logger: logger }; 41 | }, 42 | 'not call silent log': function(o) { 43 | //make test fail if called 44 | o.trans.log = function() { assert.isTrue(false); }; 45 | o.logger.log('silent', 'message'); 46 | o.logger.silent('message'); 47 | }, 48 | 'call error log': function(o) { 49 | //make test pass if called 50 | o.trans.log = function() { assert.isTrue(true); }; 51 | o.logger.log('error', 'message'); 52 | o.logger.error('message'); 53 | }, 54 | 'call warn log': function(o) { 55 | //make test pass if called 56 | o.trans.log = function() { assert.isTrue(true); }; 57 | o.logger.log('warn', 'message'); 58 | o.logger.warn('message'); 59 | }, 60 | 'call info log': function(o) { 61 | //make test pass if called 62 | o.trans.log = function() { assert.isTrue(true); }; 63 | o.logger.log('info', 'message'); 64 | o.logger.info('message'); 65 | }, 66 | 'not call verbose log': function(o) { 67 | //make test fail if called 68 | o.trans.log = function() { assert.isTrue(false); }; 69 | o.logger.log('verbose', 'message'); 70 | o.logger.verbose('message'); 71 | }, 72 | 'not call debug log': function(o) { 73 | //make test fail if called 74 | o.trans.log = function() { assert.isTrue(false); }; 75 | o.logger.log('debug', 'message'); 76 | o.logger.debug('message'); 77 | }, 78 | 'not call silly log': function(o) { 79 | //make test fail if called 80 | o.trans.log = function() { assert.isTrue(false); }; 81 | o.logger.log('silly', 'message'); 82 | o.logger.silly('message'); 83 | } 84 | } 85 | } 86 | }).export(module); -------------------------------------------------------------------------------- /test/lumber/encoders/text-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * text-test.js: Tests the text encoder 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('../../coverage'), 14 | inspect = require('eyes').inspector({ stream: null }), 15 | lumber = cov.require('../lib/lumber'); 16 | 17 | vows.describe('Text').addBatch({ 18 | 'text encoder': { 19 | topic: function() { 20 | return new lumber.encoders.Text(); 21 | }, 22 | 'has': { 23 | 'the correct defaults': function(enc) { 24 | assert.isTrue(enc.colorize); 25 | assert.isFalse(enc.timestamp); 26 | assert.equal(enc.headFormat, '%l: '); 27 | assert.equal(enc.dateFormat, 'isoDateTime'); 28 | assert.equal(enc.contentType, 'text/plain'); 29 | assert.isFunction(enc.inspect); 30 | }, 31 | 'the correct functions': function(enc) { 32 | assert.isFunction(enc.encode); 33 | } 34 | }, 35 | 'enocde works': { 36 | 'without timestamp, or meta': function(enc) { 37 | enc.timestamp = false; 38 | assert.equal(enc.encode('info', 'The Message'), 'info: The Message'); 39 | assert.equal(enc.encode('warn', 'The Message'), 'warn: The Message'); 40 | assert.equal(enc.encode('error', 'The Message'), 'error: The Message'); 41 | }, 42 | 'with timestamp, without meta': function(enc) { 43 | enc.timestamp = true; 44 | assert.match(enc.encode('info', 'The Message'), /info: \([\d\-]+T[\d:]+\) The Message/); 45 | assert.match(enc.encode('warn', 'The Message'), /warn: \([\d\-]+T[\d:]+\) The Message/); 46 | assert.match(enc.encode('error', 'The Message'), /error: \([\d\-]+T[\d:]+\) The Message/); 47 | }, 48 | 'without timestamp, with meta': function(enc) { 49 | enc.timestamp = false; 50 | 51 | assert.equal(enc.encode('info', 'The Message', { meta: 'data' }), 52 | 'info: The Message\n\033[36m' + inspect({ meta: 'data' })); 53 | 54 | assert.equal(enc.encode('warn', 'The Message', { meta: 'data' }), 55 | 'warn: The Message\n\033[36m' + inspect({ meta: 'data' })); 56 | 57 | assert.equal(enc.encode('error', 'The Message', { meta: 'data' }), 58 | 'error: The Message\n\033[36m' + inspect({ meta: 'data' })); 59 | }, 60 | 'with timestamp, and meta': function(enc) { 61 | enc.timestamp = true; 62 | 63 | assert.match(enc.encode('info', 'The Message', { meta: 'data' }), 64 | (/^info: \([\d\-]+T[\d:]+\) The Message\n.+\{ .+ \}.+$/)); 65 | 66 | assert.match(enc.encode('warn', 'The Message', { meta: 'data' }), 67 | (/^warn: \([\d\-]+T[\d:]+\) The Message\n.+\{ .+ \}.+$/)); 68 | 69 | assert.match(enc.encode('error', 'The Message', { meta: 'data' }), 70 | (/^error: \([\d\-]+T[\d:]+\) The Message\n.+\{ .+ \}.+$/)); 71 | } 72 | } 73 | } 74 | }).export(module); -------------------------------------------------------------------------------- /lib/lumber/transports/webservice.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * webservice.js: Webservice transport 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | ////////// 11 | // Required Includes 12 | /////////////////////////// 13 | var util = require('util'), 14 | url = require('url'), 15 | http = require('http'), 16 | https = require('https'), 17 | lumber = require('../../lumber'); 18 | 19 | /** 20 | * Webservice Transport 21 | * @implements {Transport} 22 | */ 23 | var Webservice = exports.Webservice = function(options) { 24 | var self = this; 25 | 26 | lumber.Transport.call(self); 27 | 28 | options = options || {}; 29 | 30 | self.encoder = lumber.util.checkOption(options.encoder, 'json'); 31 | self.level = lumber.util.checkOption(options.level, 'info'); 32 | self.url = lumber.util.checkOption(options.url, null); 33 | self.method = lumber.util.checkOption(options.method, 'POST'); 34 | self.headers = lumber.util.checkOption(options.headers, null); 35 | self.secure = lumber.util.checkOption(options.secure, false); 36 | self.auth = lumber.util.checkOption(options.auth, null); 37 | 38 | self.name = 'webservice'; 39 | 40 | if(typeof(self.encoder) == 'string') { 41 | var e = lumber.util.titleCase(self.encoder); 42 | 43 | if(lumber.encoders[e]) { 44 | self.encoder = new lumber.encoders[e](); 45 | } else { 46 | throw new Error('Unknown encoder passed: ' + self.encoder); 47 | } 48 | } 49 | 50 | if(!self.headers) { 51 | self.headers = { 'Content-Type': self.encoder.contentType }; 52 | } 53 | }; 54 | 55 | ////////// 56 | // Inherits from EventEmitter 57 | /////////////////////////// 58 | util.inherits(Webservice, lumber.Transport); 59 | 60 | ////////// 61 | // Public Methods 62 | /////////////////////////// 63 | /** 64 | * Logs the string to the specified webservice 65 | * @param {object} args 66 | * @param {function} cb 67 | */ 68 | Webservice.prototype.log = function(args, cb) { 69 | var self = this, 70 | msg = self.encoder.encode(args.level, args.msg, args.meta), 71 | opts = url.parse(self.url), 72 | req, data, secure = self.secure; 73 | 74 | if(opts.protocol.toLowerCase() == 'https:') secure = true; 75 | 76 | opts.port = opts.port || (secure ? 443 : 80); 77 | opts.method = self.method; 78 | opts.headers = self.headers; 79 | 80 | if(self.auth) opts.auth = self.auth; 81 | 82 | if(self.secure) { 83 | req = https.request(opts); 84 | } else { 85 | req = http.request(opts); 86 | } 87 | 88 | //setup listeners 89 | req.on('response', function(res) { 90 | res.on('data', function(chunk) { 91 | if(!data) data = chunk; 92 | else data += chunk; 93 | }); 94 | 95 | res.on('end', function() { 96 | if(cb) cb(null, msg, args.level, self.name, self.url, res.statusCode, data); 97 | }); 98 | 99 | res.on('close', function(err) { 100 | if(cb) cb(err, msg, args.level, self.name, self.url, res.statusCode, data); 101 | }); 102 | }); 103 | 104 | req.on('error', function(err) { 105 | if(cb) cb(err); 106 | }); 107 | 108 | //write msg to body 109 | req.write(msg); 110 | req.end(); 111 | }; -------------------------------------------------------------------------------- /lib/lumber/encoders/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * text.js: Text Encoder 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | ////////// 11 | // Required Includes 12 | /////////////////////////// 13 | var util = require('util'), 14 | dateFormat = require('dateformat'), 15 | eyes = require('eyes'), 16 | common = require('../common'), 17 | Encoder = require('./encoder').Encoder; 18 | 19 | /** 20 | * Text Encoder 21 | * @constructor 22 | * @implements {Encoder} 23 | */ 24 | var Text = exports.Text = function(options) { 25 | var self = this; 26 | 27 | Encoder.call(self); 28 | options = options || {}; 29 | 30 | self.colorize = common.checkOption(options.colorize, true); 31 | self.timestamp = common.checkOption(options.timestamp, false); 32 | self.headFormat = common.checkOption(options.headFormat, '%l: '); 33 | self.dateFormat = common.checkOption(options.dateFormat, 'isoDateTime'); 34 | 35 | self.inspect = eyes.inspector({ stream: null }); 36 | 37 | self.contentType = 'text/plain'; 38 | self.encoding = 'utf8'; 39 | }; 40 | 41 | ////////// 42 | // Inherits from Encoder 43 | /////////////////////////// 44 | util.inherits(Text, Encoder); 45 | 46 | ////////// 47 | // Public Methods 48 | /////////////////////////// 49 | /** 50 | * Encodes the passed string into CSV Text 51 | * @return {string} Encoded string 52 | * @param {string} level The level of this message 53 | * @param {string} msg The message to encode 54 | * @param {object} meta The metadata of this log 55 | */ 56 | Text.prototype.encode = function(level, msg, meta) { 57 | var self = this, 58 | head = (self.colorize && self.colors ? 59 | self.headFormat 60 | .replace('%l', common.colorize(level.toLowerCase(), level, self.colors)) 61 | .replace('%L', common.colorize(level.toUpperCase(), level, self.colors)) 62 | : 63 | self.headFormat.replace('%l', level.toLowerCase()).replace('%L', level.toUpperCase()) 64 | ), 65 | time = dateFormat(new Date(), self.dateFormat); 66 | 67 | //have to color the meta cyan since that is default 68 | //color for eyes, and there is a glitch that doesn't 69 | //color the entire object on null streams. 70 | //This should really be changed to use w/e the color is set for 71 | //ALL in eyes instead of assuming cyan 72 | return head + (self.timestamp ? '(' + time + ') ' : '') + msg + self._encodeMeta(meta); 73 | }; 74 | 75 | Text.prototype._encodeMeta = function(meta) { 76 | var self = this; 77 | 78 | if(!meta) return ''; 79 | 80 | //special error formatting 81 | if(meta.constructor == Error) { 82 | var c = self.colorize ? self.colors.error || 'red' : null, 83 | msg = [], 84 | props = ['message', 'name', 'type', 'stack', 'arguments'], 85 | temp; 86 | 87 | props.forEach(function(prop) { 88 | //if prop doesnt exist, move on 89 | if(!meta[prop]) return; 90 | 91 | //setup title 92 | if(prop == 'stack') temp = ' Stack Trace'; 93 | else temp = ' Error ' + common.titleCase(prop); 94 | 95 | //color if necessary, and add value 96 | temp = (c ? temp[c] : temp); 97 | temp += ': ' + (prop == 'stack' ? '\n ' : '') + meta[prop]; 98 | 99 | //add to message 100 | msg.push(temp); 101 | }); 102 | 103 | return '\n' + msg.join('\n'); 104 | } 105 | 106 | //if not special case, just inspect with eyes 107 | return '\n\033[36m' + self.inspect(meta); 108 | }; -------------------------------------------------------------------------------- /test/lumber/encoders/json-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * json-test.js: Tests the json encoder 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('../../coverage'), 14 | lumber = cov.require('../lib/lumber'); 15 | 16 | vows.describe('Json').addBatch({ 17 | 'json encoder': { 18 | topic: function() { 19 | return new lumber.encoders.Json(); 20 | }, 21 | 'has': { 22 | 'the correct defaults': function(enc) { 23 | assert.isFalse(enc.colorize); 24 | assert.isTrue(enc.timestamp); 25 | assert.equal(enc.headFormat, '%L'); 26 | assert.equal(enc.dateFormat, 'isoDateTime'); 27 | assert.equal(enc.contentType, 'application/json'); 28 | }, 29 | 'has the correct functions': function(enc) { 30 | assert.isFunction(enc.encode); 31 | } 32 | }, 33 | 'encode works': { 34 | 'without timestamp, or meta': function(enc) { 35 | enc.timestamp = false; 36 | assert.equal(enc.encode('info', 'The Message'), JSON.stringify({ level: 'info', head: 'INFO', message: 'The Message' })); 37 | assert.equal(enc.encode('warn', 'The Message'), JSON.stringify({ level: 'warn', head: 'WARN', message: 'The Message' })); 38 | assert.equal(enc.encode('error', 'The Message'), JSON.stringify({ level: 'error', head: 'ERROR', message: 'The Message' })); 39 | }, 40 | 'with timestamp, without meta': function(enc) { 41 | enc.timestamp = true; 42 | assert.match(enc.encode('info', 'The Message'), re(JSON.stringify({ level: 'info', head: 'INFO', message: 'The Message', timestamp: '[\\d\\-]+T[\\d:]+' }))); 43 | assert.match(enc.encode('warn', 'The Message'), re(JSON.stringify({ level: 'warn', head: 'WARN', message: 'The Message', timestamp: '[\\d\\-]+T[\\d:]+' }))); 44 | assert.match(enc.encode('error', 'The Message'), re(JSON.stringify({ level: 'error', head: 'ERROR', message: 'The Message', timestamp: '[\\d\\-]+T[\\d:]+' }))); 45 | }, 46 | 'without timestamp, with meta': function(enc) { 47 | enc.timestamp = false; 48 | 49 | assert.equal(enc.encode('info', 'The Message', { meta: 'data' }), 50 | JSON.stringify({ level: 'info', head: 'INFO', message: 'The Message', meta: { meta: 'data' } })); 51 | 52 | assert.equal(enc.encode('warn', 'The Message', { meta: 'data' }), 53 | JSON.stringify({ level: 'warn', head: 'WARN', message: 'The Message', meta: { meta: 'data' } })); 54 | 55 | assert.equal(enc.encode('error', 'The Message', { meta: 'data' }), 56 | JSON.stringify({ level: 'error', head: 'ERROR', message: 'The Message', meta: { meta: 'data' } })); 57 | }, 58 | 'with timestamp, and meta': function(enc) { 59 | enc.timestamp = true; 60 | 61 | assert.match(enc.encode('info', 'The Message', { meta: 'data' }), 62 | re(JSON.stringify({ level: 'info', head: 'INFO', message: 'The Message', timestamp: '[\\d\\-]+T[\\d:]+', meta: { meta: 'data' } }))); 63 | 64 | assert.match(enc.encode('warn', 'The Message', { meta: 'data' }), 65 | re(JSON.stringify({ level: 'warn', head: 'WARN', message: 'The Message', timestamp: '[\\d\\-]+T[\\d:]+', meta: { meta: 'data' } }))); 66 | 67 | assert.match(enc.encode('error', 'The Message', { meta: 'data' }), 68 | re(JSON.stringify({ level: 'error', head: 'ERROR', message: 'The Message', timestamp: '[\\d\\-]+T[\\d:]+', meta: { meta: 'data' } }))); 69 | } 70 | } 71 | } 72 | }).export(module); 73 | 74 | function re(str) { 75 | return new RegExp(str.replace(/\\\\(.)/g, '\\$1')); 76 | } -------------------------------------------------------------------------------- /lib/lumber/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * common.js: Common helpers for the entire module 4 | * 5 | * (c) 2012 Panther Development 6 | * MIT LICENSE 7 | * 8 | */ 9 | 10 | var common = module.exports; 11 | 12 | ////////// 13 | // Required Includes 14 | /////////////////////////// 15 | var util = require('util'), 16 | colors = require('colors'), 17 | cycle = require('cycle'); 18 | 19 | /** 20 | * Sets a variable to the default if it is unset 21 | * @return {mixed} The option set 22 | * @param {mixed} opt The option value passed 23 | * @param {mixed} val The default value to set 24 | */ 25 | common.checkOption = function(opt, val) { 26 | return (typeof(opt) == 'undefined' ? val : opt); 27 | }; 28 | 29 | /** 30 | * Title cases a passed string. Changes "help" to "Help" 31 | * @return {string} Title-cased version of passes string 32 | * @param {string} str The string to captialize 33 | */ 34 | common.titleCase = function(str) { 35 | var lines = str.split('\n'); 36 | 37 | lines.forEach(function(line, l) { 38 | var words = line.split(' '); 39 | 40 | words.forEach(function(word, w) { 41 | words[w] = word[0].toUpperCase() + word.slice(1); 42 | }); 43 | 44 | lines[l] = words.join(' '); 45 | }); 46 | 47 | return lines.join('\n'); 48 | }; 49 | 50 | /** 51 | * Generates a random GUID 52 | * @return {string} GUID 53 | */ 54 | common.generateGuid = function() { 55 | /** 56 | * Generates 4 hex characters 57 | * @return {string} 4 character hex string 58 | */ 59 | var S4 = function () { 60 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 61 | }; 62 | 63 | return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4()); 64 | }; 65 | 66 | /** 67 | * Padds a string to the length specified 68 | * @return {string} Padded string 69 | * @param {string} str String to pad 70 | * @param {string} pad What to pad the string with 71 | * @param {number} len The length of final string 72 | */ 73 | common.pad = function(str, pad, len) { 74 | while(str.length < len) 75 | str = pad + str; 76 | 77 | return str; 78 | }; 79 | 80 | /** 81 | * Colorizes a string based on a level 82 | * @return {string} Colorized string 83 | * @param {string} str The string to colorize 84 | * @param {string} level The level the string should be colorized to 85 | */ 86 | common.colorize = function(str, level, colors) { 87 | return str[colors[level]]; 88 | }; 89 | 90 | /** 91 | * Prepares an arguments array for use by a log function 92 | * @return {object} Perpares arguments for a log method 93 | * @param {array} args The arguments to prepare 94 | */ 95 | common.prepareArgs = function(args) { 96 | var obj = { 97 | level: args[0], 98 | meta: args[1], 99 | msg: args[2], 100 | cb: args[args.length - 1] 101 | }, 102 | argStart = 3, fargs, lmsg = args[2]; 103 | 104 | //if meta is a string, we consider it the message 105 | if(typeof(obj.meta) == 'string') { 106 | argStart = 2; 107 | obj.msg = obj.meta; 108 | obj.meta = null; 109 | } 110 | 111 | //if cb is not a func, then all left are format args 112 | if(typeof(obj.cb) != 'function') { 113 | obj.cb = null; 114 | fargs = args.slice(argStart); 115 | } 116 | //if it is, then only upto the last item is format args 117 | else { 118 | fargs = args.slice(argStart, args.length - 1); 119 | } 120 | 121 | //at this point if msg is a function, its the callback 122 | if(typeof(obj.msg) == 'function') { 123 | obj.cb = obj.msg; 124 | obj.msg = ''; 125 | } 126 | 127 | //if we have format args, then lets apply them 128 | if(fargs.length) { 129 | //put msg on and apply to util.format 130 | fargs.unshift(obj.msg); 131 | obj.msg = util.format.apply(null, fargs); 132 | } 133 | 134 | return obj; 135 | }; -------------------------------------------------------------------------------- /test/lumber/encoders/xml-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * xml-test.js: Tests the xml encoder 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('../../coverage'), 14 | lumber = cov.require('../lib/lumber'); 15 | 16 | vows.describe('Xml').addBatch({ 17 | 'xml encoder': { 18 | topic: function() { 19 | return new lumber.encoders.Xml(); 20 | }, 21 | 'has': { 22 | 'the correct defaults': function(enc) { 23 | assert.isFalse(enc.colorize); 24 | assert.isTrue(enc.timestamp); 25 | assert.equal(enc.headFormat, '%L'); 26 | assert.equal(enc.dateFormat, 'isoDateTime'); 27 | assert.equal(enc.contentType, 'text/xml'); 28 | }, 29 | 'the correct functions': function(enc) { 30 | assert.isFunction(enc.encode); 31 | } 32 | }, 33 | 'enocde works': { 34 | 'without timestamp, or meta': function(enc) { 35 | enc.timestamp = false; 36 | assert.equal(enc.encode('info', 'The Message'), 37 | 'INFOThe Message'); 38 | assert.equal(enc.encode('warn', 'The Message'), 39 | 'WARNThe Message'); 40 | assert.equal(enc.encode('error', 'The Message'), 41 | 'ERRORThe Message'); 42 | }, 43 | 'with timestamp, without meta': function(enc) { 44 | enc.timestamp = true; 45 | assert.match(enc.encode('info', 'The Message'), 46 | (/INFO<\/head>The Message<\/message><\/log>/)); 47 | assert.match(enc.encode('warn', 'The Message'), 48 | (/WARN<\/head>The Message<\/message><\/log>/)); 49 | assert.match(enc.encode('error', 'The Message'), 50 | (/ERROR<\/head>The Message<\/message><\/log>/)); 51 | }, 52 | 'without timestamp, with meta': function(enc) { 53 | enc.timestamp = false; 54 | 55 | assert.equal(enc.encode('info', 'The Message', { meta: 'data', ary: ['msg'] }), 56 | 'INFOThe Messagedatamsg'); 57 | 58 | assert.equal(enc.encode('warn', 'The Message', { meta: 'data', ary: ['msg'] }), 59 | 'WARNThe Messagedatamsg'); 60 | 61 | assert.equal(enc.encode('error', 'The Message', { meta: 'data', ary: ['msg'] }), 62 | 'ERRORThe Messagedatamsg'); 63 | }, 64 | 'with timestamp, and meta': function(enc) { 65 | enc.timestamp = true; 66 | 67 | assert.match(enc.encode('info', 'The Message', { meta: 'data', ary: ['msg'] }), 68 | (/INFO<\/head>The Message<\/message>data<\/meta>msg<\/data><\/ary><\/meta><\/log>/)); 69 | 70 | assert.match(enc.encode('warn', 'The Message', { meta: 'data', ary: ['msg'] }), 71 | (/WARN<\/head>The Message<\/message>data<\/meta>msg<\/data><\/ary><\/meta><\/log>/)); 72 | 73 | assert.match(enc.encode('error', 'The Message', { meta: 'data', ary: ['msg'] }), 74 | (/ERROR<\/head>The Message<\/message>data<\/meta>msg<\/data><\/ary><\/meta><\/log>/)); 75 | } 76 | } 77 | } 78 | }).export(module); -------------------------------------------------------------------------------- /test/lumber/common-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * common-test.js: Tests to ensure commong functions work properly 3 | * 4 | * (c) 2012 Panther Development 5 | * MIT LICENCE 6 | * 7 | **/ 8 | 9 | var fs = require('fs'), 10 | path = require('path'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cov = require('../coverage'), 14 | common = cov.require('../lib/lumber/common'); 15 | 16 | vows.describe('Common').addBatch({ 17 | 'common module': { 18 | topic: function() { 19 | return null; 20 | }, 21 | 'should have the correct exports': function() { 22 | assert.isFunction(common.titleCase); 23 | assert.isFunction(common.generateGuid); 24 | assert.isFunction(common.pad); 25 | assert.isFunction(common.colorize); 26 | assert.isFunction(common.prepareArgs); 27 | }, 28 | 'when using titleCase': { 29 | 'single words should uppercase': function() { 30 | assert.equal(common.titleCase('hey'), 'Hey'); 31 | assert.equal(common.titleCase('down'), 'Down'); 32 | }, 33 | 'multiple words should uppercase': function() { 34 | assert.equal(common.titleCase('hey there'), 'Hey There'); 35 | assert.equal(common.titleCase('Hey ho, let\'s Go!\nlawl no, sir!'), 36 | 'Hey Ho, Let\'s Go!\nLawl No, Sir!'); 37 | } 38 | }, 39 | 'when generating a guid': { 40 | topic: function() { 41 | return (/^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/); 42 | }, 43 | 'it should match the guid pattern': function(regex) { 44 | assert.match(common.generateGuid(), regex); 45 | assert.match(common.generateGuid(), regex); 46 | assert.match(common.generateGuid(), regex); 47 | assert.match(common.generateGuid(), regex); 48 | assert.match(common.generateGuid(), regex); 49 | assert.match(common.generateGuid(), regex); 50 | } 51 | }, 52 | 'when padding a string': { 53 | 'it should be the right length': function() { 54 | assert.lengthOf(common.pad('str', ' ', 10), 10); 55 | assert.lengthOf(common.pad('19', '0', 3), 3); 56 | assert.lengthOf(common.pad('dots', '.', 25), 25); 57 | }, 58 | 'it should be the right pad char': function() { 59 | assert.equal(common.pad('str', ' ', 10), ' str'); 60 | assert.equal(common.pad('19', '0', 3), '019'); 61 | assert.equal(common.pad('dots', '.', 25), '.....................dots'); 62 | } 63 | }, 64 | 'when colorizing a string': { 65 | topic: function() { 66 | return { 67 | info: 'cyan', 68 | error: 'red', 69 | warn: 'yellow', 70 | verbose: 'grey' 71 | }; 72 | }, 73 | 'it should be the right color': function(colors) { 74 | assert.equal(common.colorize('str', 'info', colors), '\u001b[36mstr\u001b[39m'); 75 | assert.equal(common.colorize('str', 'error', colors), '\u001b[31mstr\u001b[39m'); 76 | assert.equal(common.colorize('str', 'warn', colors), '\u001b[33mstr\u001b[39m'); 77 | assert.equal(common.colorize('str', 'verbose', colors), '\u001b[90mstr\u001b[39m'); 78 | } 79 | }, 80 | 'when preparing arguments': { 81 | 'calls with no meta, no formats, and no callback should work': function() { 82 | var args = common.prepareArgs(['error', 'This is the message']); 83 | 84 | assert.equal(args.level, 'error'); 85 | assert.isNull(args.meta); 86 | assert.equal(args.msg, 'This is the message'); 87 | assert.isNull(args.cb); 88 | }, 89 | 'calls with meta, no formats, and no callback should work': function() { 90 | var args = common.prepareArgs(['error', { meta: 'data' }, 'This is the message']); 91 | 92 | assert.equal(args.level, 'error'); 93 | assert.deepEqual(args.meta, { meta: 'data' }); 94 | assert.equal(args.msg, 'This is the message'); 95 | assert.isNull(args.cb); 96 | }, 97 | 'calls with no meta, formats, and no callback should work': function() { 98 | var args = common.prepareArgs(['error', 'This is the message %s %d', 'and more', 15]); 99 | 100 | assert.equal(args.level, 'error'); 101 | assert.isNull(args.meta); 102 | assert.equal(args.msg, 'This is the message and more 15'); 103 | assert.isNull(args.cb); 104 | }, 105 | 'calls with meta, formats, and no callback should work': function() { 106 | var args = common.prepareArgs(['error', { meta: 'data' }, 'This is the message %s %d', 'and more', 15]); 107 | 108 | assert.equal(args.level, 'error'); 109 | assert.deepEqual(args.meta, { meta: 'data' }); 110 | assert.equal(args.msg, 'This is the message and more 15'); 111 | assert.isNull(args.cb); 112 | }, 113 | 'calls with no meta, no formats, and a callback should work': function() { 114 | var args = common.prepareArgs(['error', 'This is the message', function() {}]); 115 | 116 | assert.equal(args.level, 'error'); 117 | assert.isNull(args.meta); 118 | assert.equal(args.msg, 'This is the message'); 119 | assert.isFunction(args.cb); 120 | }, 121 | 'calls with meta, no formats, and a callback should work': function() { 122 | var args = common.prepareArgs(['error', { meta: 'data' }, 'This is the message', function() {}]); 123 | 124 | assert.equal(args.level, 'error'); 125 | assert.deepEqual(args.meta, { meta: 'data' }); 126 | assert.equal(args.msg, 'This is the message'); 127 | assert.isFunction(args.cb); 128 | }, 129 | 'calls with no meta, formats, and a callback should work': function() { 130 | var args = common.prepareArgs(['error', 'This is the message %s %d', 'and more', 15, function() {}]); 131 | 132 | assert.equal(args.level, 'error'); 133 | assert.isNull(args.meta); 134 | assert.equal(args.msg, 'This is the message and more 15'); 135 | assert.isFunction(args.cb); 136 | }, 137 | 'calls with meta, formats, and a callback should work': function() { 138 | var args = common.prepareArgs(['error', { meta: 'data' }, 'This is the message %s %d', 'and more', 15, function() {}]); 139 | 140 | assert.equal(args.level, 'error'); 141 | assert.deepEqual(args.meta, { meta: 'data' }); 142 | assert.equal(args.msg, 'This is the message and more 15'); 143 | assert.isFunction(args.cb); 144 | } 145 | } 146 | } 147 | }).export(module); -------------------------------------------------------------------------------- /lib/lumber/transports/file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * file.js: File transport 4 | * 5 | * Liberal inspiration taken from https://github.com/flatiron/winston 6 | * 7 | * (c) 2012 Panther Development 8 | * MIT LICENSE 9 | * 10 | */ 11 | 12 | ////////// 13 | // Required Includes 14 | /////////////////////////// 15 | var util = require('util'), 16 | fs = require('fs'), 17 | path = require('path'), 18 | lumber = require('../../lumber'); 19 | 20 | /** 21 | * File Transport 22 | * @constructor 23 | * @implements {Transport} 24 | */ 25 | var File = exports.File = function(options) { 26 | var self = this; 27 | 28 | lumber.Transport.call(self); 29 | 30 | options = options || {}; 31 | 32 | self.encoder = lumber.util.checkOption(options.encoder, 'json'); 33 | self.level = lumber.util.checkOption(options.level, 'info'); 34 | self.filename = lumber.util.checkOption(options.filename, path.resolve('app.log')); 35 | self.filemode = lumber.util.checkOption(options.filemode, '0666'); 36 | self.maxsize = lumber.util.checkOption(options.maxsize, 52428800); //50MB 37 | self.rotate = lumber.util.checkOption(options.rotate, 10); 38 | 39 | self._size = 0; 40 | self._buffer = []; 41 | 42 | self.name = 'file'; 43 | 44 | if(typeof(self.encoder) == 'string') { 45 | var e = lumber.util.titleCase(self.encoder); 46 | 47 | if(lumber.encoders[e]) { 48 | self.encoder = new lumber.encoders[e](); 49 | } else { 50 | throw new Error('Unknown encoder passed: ' + self.encoder); 51 | } 52 | } 53 | 54 | self.encoding = self.encoder.encoding; 55 | }; 56 | 57 | ////////// 58 | // Inherits from EventEmitter 59 | /////////////////////////// 60 | util.inherits(File, lumber.Transport); 61 | 62 | ////////// 63 | // Public Methods 64 | /////////////////////////// 65 | /** 66 | * Logs the string to the specified file 67 | * @param {object} args The arguments for the log 68 | * @param {function} cb The callback to call when completed 69 | */ 70 | File.prototype.log = function(args, cb) { 71 | var self = this, 72 | msg = self.encoder.encode(args.level, args.msg, args.meta); 73 | 74 | self._open(function(buff) { 75 | if(buff) { 76 | self._buffer.push([msg, args, cb]); 77 | } else { 78 | self._write(msg + '\n', function(err) { 79 | if(cb) cb(err, msg, args.level, self.name, self.filename); 80 | }); 81 | } 82 | }); 83 | }; 84 | 85 | ////////// 86 | // Public Methods 87 | /////////////////////////// 88 | File.prototype._write = function(data, cb) { 89 | var self = this; 90 | 91 | //add size of this new message 92 | self._size += data.length; 93 | 94 | //write to stream 95 | var flushed = self._stream.write(data, self.encoding); 96 | 97 | if(flushed) { 98 | //check if logs need to be rotated 99 | if(self.maxsize && self._size >= self.maxsize) { 100 | self._rotateLogs(function(err) { 101 | self._size = 0; 102 | if(cb) cb(err); 103 | }); 104 | } else { 105 | if(cb) cb(null); 106 | } 107 | } else { 108 | //after msg is drained 109 | self._drain(function() { 110 | //check if logs need to be rotated 111 | if(self.maxsize && self._size >= self.maxsize) { 112 | self._rotateLogs(function(err) { 113 | self._size = 0; 114 | if(cb) cb(err); 115 | }); 116 | } else { 117 | if(cb) cb(null); 118 | } 119 | }); 120 | } 121 | }; 122 | 123 | File.prototype._open = function(cb) { 124 | var self = this; 125 | 126 | if(self._opening) { 127 | if(cb) cb(true); 128 | } else if(self._stream) { 129 | //already have an open stream 130 | if(cb) cb(false); 131 | } else { 132 | //need to open new stream, buffer msg 133 | if(cb) cb(true); 134 | 135 | //check file sizes for rotation 136 | self._checkSize(function(err) { 137 | //after rotation create stream 138 | self._stream = fs.createWriteStream(self.filename, { 139 | flags: 'a', 140 | encoding: self.encoding, 141 | mode: self.fileMode 142 | }); 143 | 144 | self._stream.setMaxListeners(Infinity); 145 | 146 | self.once('flush', function() { 147 | self._opening = false; 148 | self.emit('open', self.filename); 149 | }); 150 | 151 | self._flush(); 152 | }); 153 | } 154 | }; 155 | 156 | File.prototype._close = function(cb) { 157 | var self = this; 158 | 159 | if(self._stream) { 160 | self._stream.end(); 161 | self._stream.destroySoon(); 162 | 163 | self.on('close', function() { 164 | self.emit('closed'); 165 | if(cb) cb(null); 166 | }); 167 | self._stream = null; 168 | } else { 169 | self._stream = null; 170 | if(cb) cb(null); 171 | } 172 | }; 173 | 174 | File.prototype._drain = function(cb) { 175 | var self = this; 176 | 177 | //easy way to handle drain callback 178 | self._stream.once('drain', function() { 179 | self.emit('drain'); 180 | if(cb) cb(); 181 | }); 182 | }; 183 | 184 | File.prototype._flush = function(cb) { 185 | var self = this; 186 | 187 | if(self._buffer.length === 0) { 188 | self.emit('flush'); 189 | if(cb) cb(null); 190 | return; 191 | } 192 | 193 | //start a write for each one 194 | self._buffer.forEach(function(log) { 195 | (function(msg, args, cb) { 196 | process.nextTick(function() { 197 | self._write(msg + '\n', function(err) { 198 | if(cb) cb(err, msg, args.level, self.name, self.filename); 199 | }); 200 | }); 201 | }).apply(self, log); 202 | }); 203 | 204 | //after writes are started clear buffer 205 | self._buffer.length = 0; 206 | 207 | //emit flush after the stream drains 208 | self._drain(function() { 209 | self.emit('flush'); 210 | if(cb) cb(null); 211 | }); 212 | }; 213 | 214 | File.prototype._checkSize = function(cb) { 215 | var self = this; 216 | 217 | //check size of file 218 | fs.stat(self.filename, function(err, stats) { 219 | //if err and error isnt that it doesnt exist 220 | if(err && err.code !== 'ENOENT') { 221 | if(cb) cb(err); 222 | return; 223 | } 224 | 225 | self._size = (stats ? stats.size : 0); 226 | 227 | //if the size is >= maxsize, rotate files 228 | if(self._size >= self.maxsize) { 229 | self._size = 0; 230 | self._rotateLogs(cb); 231 | } else { 232 | cb(null); 233 | } 234 | }); 235 | }; 236 | 237 | File.prototype._rotateLogs = function(cb) { 238 | var self = this, 239 | max = 1, exists = false; 240 | 241 | //keep going until we find max number that doesn't exist 242 | do { 243 | try { 244 | fs.lstatSync(self.filename + '.' + max); 245 | exists = true; 246 | max++; 247 | } catch(e) { 248 | exists = false; 249 | } 250 | } while(exists); 251 | 252 | self._close(function() { 253 | //loop through each file and move their numbers up 254 | self._doLogRotate(max, function(err) { 255 | if(err) { if(cb) cb(err); return; } 256 | 257 | self.emit('rotate'); 258 | 259 | //if the max file is more than how many we keep remove it 260 | if(max > self.rotate) { 261 | fs.unlink(self.filename + '.' + max, function(err) { 262 | if(cb) cb(err); 263 | }); 264 | } else { 265 | if(cb) cb(null); 266 | } 267 | }); 268 | }); 269 | }; 270 | 271 | File.prototype._doLogRotate = function(num, cb) { 272 | var self = this; 273 | 274 | //if we at 0 we are done 275 | if(!num) { if(cb) cb(null); return; } 276 | 277 | //setup filenames to move 278 | var from = self.filename + (num > 1 ? '.' + (num - 1) : ''), 279 | to = self.filename + '.' + num; 280 | 281 | //move files 282 | fs.rename(from, to, function(err) { 283 | if(err) { if(cb) cb(err); return; } 284 | 285 | //move the next one 286 | num--; 287 | self._doLogRotate(num, cb); 288 | }); 289 | }; 290 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lumber (v0.0.6) 2 | 3 | A custom, async, extensible logging library; with true log rotation. 4 | 5 | ## Contents 6 | 7 | - [Overview](#overview) 8 | - [Features](#features) 9 | - [Dependencies](#dependencies) 10 | - [Installation](#installation) 11 | - [Running Tests](#tests) 12 | - [Usage](#usage) 13 | - [Defaults](#defaults) 14 | - [Multiple Transports](#multiple-transports) 15 | - [Different Encoders](#different-encoders) 16 | - [Customer Logging Levels](#custom-logging-levels) 17 | - [Meta Data](#meta-data) 18 | - [Format Params](#format-params) 19 | - [Callbacks](#callbacks) 20 | - [Events](#events) 21 | - [Options](#options) 22 | - [Logger Options](#logger-options) 23 | - [Transport Options](#transport-options) 24 | - [Encoder Options](#encoder-options) 25 | - [TODO](#todo) 26 | 27 | ## Overview 28 | 29 | Lumber is an asynchronous logging library that is geared towards extensibility and providing an all-in-one 30 | solution for logging in Node.js applications. It provides a method of logging to your CLI, a file log, or even 31 | a webservice; each with independent configurable options. 32 | 33 | It is possible with Lumber to have a verbose CLI, an error log, a debug log, and a webservice taking information 34 | logs; each with a different encoder for their data; all with the same logger object. 35 | 36 | ## Features 37 | 38 | - Isolated customizable logging transports 39 | - Customizable Encoders 40 | - Settings changes can be made on-the-fly 41 | 42 | ## Dependencies 43 | 44 | - Node.js (0.6.x) 45 | - Npm (1.x.x) 46 | 47 | ## Installation 48 | 49 | The easiest way to install the Lumber module is with `npm`: 50 | 51 | ```bash 52 | npm install lumber 53 | ``` 54 | 55 | For the bleeding edge build you can clone the repo and install: 56 | 57 | ```bash 58 | git clone git://github.com/englercj/lumber.git && 59 | cd lumber && 60 | npm install 61 | ``` 62 | 63 | ## Tests 64 | 65 | Lumber comes with extensive unit tests written using [vows](http://vowsjs.org/). You can run 66 | the test suite using: 67 | 68 | ```bash 69 | npm test 70 | ``` 71 | 72 | You can also view code coverage, and build statistics at: [ci.pantherdev.com](http://ci.pantherdev.com/job/lumber/). 73 | 74 | ## Usage 75 | 76 | ### Defaults 77 | 78 | By default lumber uses a console transport, with a text encoder. You can use it simply like: 79 | 80 | ```javascript 81 | var lumber = require('lumber'), 82 | logger = new lumber.Logger(); 83 | 84 | logger.log('info', 'Hey there!'); 85 | //OR 86 | logger.info('Hey there!'); 87 | ``` 88 | 89 | ### Multiple Transports 90 | 91 | To use multiple transports, such as a console logger and a file. Simply specify them using the `transports` option: 92 | 93 | ```javascript 94 | var lumber = require('lumber'), 95 | logger = new lumber.Logger({ 96 | transports: [ 97 | new lumber.transports.Console(), 98 | new lumber.transports.File() 99 | ] 100 | }); 101 | 102 | logger.log('info', 'Hey there!'); 103 | //OR 104 | logger.info('Hey there!'); 105 | ``` 106 | 107 | This will print `info: Hey there!` to the console, as well as to the default file `app.log` (though the logfile will be in json). Another example is to have a verbose CLI, error log, and debug log: 108 | 109 | ```javascript 110 | var lumber = require('lumber'), 111 | logger = new lumber.Logger({ 112 | transports: [ 113 | new lumber.transports.Console({ level: 'verbose' }), 114 | new lumber.transports.File({ filename: 'errors.log', level: 'error' }), 115 | new lumber.transports.File({ filename: 'debug.log', level: 'debug' }) 116 | ] 117 | }); 118 | 119 | logger.info('Info message'); //logs to console & debug.log 120 | logger.error('Error message'); //logs to console, debug.log, and errors.log 121 | logger.debug('Debug message'); //logs to debug.log 122 | ``` 123 | 124 | ### Different Encoders 125 | 126 | If you don't like the default encoder chosen by a transport you can easily change it: 127 | 128 | ```javascript 129 | var lumber = require('lumber'), 130 | logger = new lumber.Logger({ 131 | transports: [ 132 | new lumber.transports.File({ 133 | filename: 'app.log', 134 | encoder: 'text' 135 | }) 136 | ] 137 | }); 138 | ``` 139 | 140 | Or if you need to specify options on your own encoder, you can instantiate it instead of passing a string: 141 | 142 | ```javascript 143 | var logger = new lumber.Logger({ 144 | transports: [ 145 | new lumber.transports.File({ 146 | filename: 'app.log', 147 | encoder: new lumber.encoders.Text({ 148 | colorize: false, 149 | headFormat: '[%L] ' 150 | }) 151 | }) 152 | ] 153 | }); 154 | ``` 155 | 156 | ### Custom Logging Levels 157 | 158 | You can pass your own levels and/or colors to a logger instance to override the defaults. Remember that any 159 | negative log level will be considered a "silent" level, which allows for a state that the logger does not log anything: 160 | 161 | ```javascript 162 | var logger = new lumber.Logger({ 163 | levels: { 164 | silent: -1, 165 | error: 0, 166 | yoyo: 1, 167 | please: 2 168 | }, 169 | colors: { 170 | error: 'red', 171 | yoyo: 'rainbow', 172 | please: 'grey' 173 | } 174 | }); 175 | 176 | //now the logger has those levels as convenience functions 177 | logger.yoyo('Yo Yo Yo!'); 178 | logger.please('fork me'); 179 | 180 | //or you can specify them explicitly 181 | logger.log('yoyo', 'some message'); 182 | ``` 183 | 184 | ### Meta Data 185 | 186 | Sometime when you are logging an event (like an error) you may have some meta data that goes along with it (like the 187 | error that was thrown). Lumber allows you to pass in this extra data to be logged along with you message: 188 | 189 | ```javascript 190 | var fs = require('fs'), 191 | lumber = require('lumber'), 192 | logger = new lumber.Logger(); 193 | 194 | try { 195 | fs.statSync('doesnt_exist.file'); 196 | } catch(e) { 197 | logger.error(e, 'File does not exist'); 198 | //or logger.log('error', e, 'File does not exist'); 199 | } 200 | ``` 201 | 202 | **Please Note:** Meta Data must be a non-string object, or you will see unexpected results. 203 | 204 | ### Format Params 205 | 206 | Lumber also allows you to use format params, that is you get the power of [`util.format`](http://nodejs.org/api/util.html#util_util_format) when using lumber: 207 | 208 | ```javascript 209 | var lumber = require('lumber'), 210 | logger = new lumber.Logger(); 211 | 212 | logger.info('You can insert strings: %s, or numbers: %d, or event json: %j', 'like this one', 15, { hi: 'there' }); 213 | //or logger.log('info', 'You can insert strings: %s, or numbers: %d, or event json: %j', 'like this one', 15, { hi: 'there' }); 214 | 215 | //You can still pass meta data if you want to: 216 | logger.info({ meta: 'data' }, 'You can insert strings: %s, or numbers: %d, or event json: %j', 'like this one', 15, { hi: 'there' }); 217 | //or logger.log('info', { meta: 'data' }, 'You can insert strings: %s, or numbers: %d, or event json: %j', 'like this one', 15, { hi: 'there' }); 218 | ``` 219 | 220 | ### Callbacks 221 | 222 | Lumber is an asynchronous logger, so it provides a callback when it has finished logging to all of it's transports: 223 | 224 | ```javascript 225 | var lumber = require('lumber'), 226 | logger = new lumber.Logger({ transports: [new lumber.transports.Console(), new lumber.transports.File()] }); 227 | 228 | logger.info('A message', function(err) { 229 | console.log('Error:', err); 230 | }); 231 | //or logger.log('info', 'A message', function(err) {}); 232 | 233 | //you can still specify meta data if you want 234 | logger.info({ meta: 'data' }, 'A message', function(err) { 235 | console.log('Error:', err); 236 | }); 237 | //or logger.log('info', { meta: 'data' }, 'A message', function(err) {}); 238 | 239 | //you can even continue to use format args 240 | logger.info({ meta: 'data' }, 'A %s message', 'formatted', function(err) { 241 | console.log('Error:', err); 242 | }); 243 | //or logger.log('info', { meta: 'data' }, 'A %s message', 'formatted', function(err) {}); 244 | ``` 245 | 246 | ### Events 247 | 248 | Lumber also is an instance of [`EventEmitter`](http://nodejs.org/api/events.html) and it will emit events as it logs to each transport. 249 | The events you can listen for are: 250 | 251 | - `log`: emitted when finished logging to a transport (for multiple transports this will fire multiple times) 252 | - `logged`: emitted when finished logging to *all* transports. 253 | 254 | When the `logged` event is fired, it passes only an array of errors that occurred or `null` if no errors occurred: 255 | 256 | ```javascript 257 | var lumber = require('lumber'), 258 | logger = new lumber.Logger(); 259 | 260 | logger.info('hey there'); 261 | logger.on('logged', function(errors) { 262 | if(errors) { 263 | errors.forEach(function(err) { 264 | console.log('Error:', err); 265 | }); 266 | } 267 | }); 268 | ``` 269 | 270 | The `log` event is fired after each transport logs, and each transport will send the same base information: 271 | 272 | - `error`: The error if one occurred or `null` if no error occurred 273 | - `msg`: The encoded message that was logged 274 | - `level`: The level of the logged message 275 | - `name`: The transport's name that logged, `'console'` for `lumber.transports.Console`, `'file'` for `lumber.transports.File`, etc. 276 | 277 | Some transports will send extra information as well, here is the extra information for each transport: 278 | 279 | #### Console Transport 280 | 281 | No extra information is sent. 282 | 283 | #### File Transport 284 | 285 | - `filename`: The resolved path to the file that was logged to 286 | 287 | Example: 288 | 289 | ```javascript 290 | var lumber = require('lumber'), 291 | logger = new lumber.Logger({ 292 | transports: [ 293 | lumber.transports.Console(), 294 | lumber.transports.File({ filename: 'errors.log', level: 'error' }), 295 | lumber.transports.File({ filename: 'full.log', level: 'silly' }) 296 | ] 297 | }); 298 | 299 | logger.info('hey there'); 300 | logger.on('log', function(err, msg, level, name, filename) { 301 | //if this is the file transport that finished 302 | if(name == 'file') { 303 | if(filename.indexOf('errors.log') != -1) { 304 | //this is the errors log that finished 305 | } else { 306 | //this is the full log that finished 307 | } 308 | } 309 | }); 310 | ``` 311 | 312 | #### Webservice Transport 313 | 314 | - `url`: The url that the data was sent to 315 | - `statusCode`: The status code of the response from the webservice hit 316 | - `responseBody`: The body of the response from the webservice hit 317 | 318 | ```javascript 319 | var lumber = require('lumber'), 320 | logger = new lumber.Logger({ 321 | transports: [ 322 | lumber.transports.Console(), 323 | lumber.transports.Webservice({ url: 'http://myservice.com/service' }) 324 | ] 325 | }); 326 | 327 | logger.info('hey there'); 328 | logger.on('log', function(err, msg, level, name, url, statusCode, responseBody) { 329 | //if this is the webservice transport that finished 330 | if(name == 'webservice') { 331 | if(statusCode == 200) { 332 | //for this example, lets assume our service returns JSON 333 | var res = JSON.parse(responseBody); 334 | 335 | console.log(res.somethingOrAnotherReturned); 336 | } 337 | } 338 | }); 339 | ``` 340 | 341 | ## Options 342 | 343 | ### Logger Options 344 | 345 | Here are all the options associated with a logger: 346 | 347 | - `levels`: Log levels associated with this logger, defaults to: `{ silent: -1, error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5 }` 348 | - `colors`: Colors of levels in this logger, defaults to: `{ error: 'red', warn: 'yellow', info: 'cyan', verbose: 'magenta', debug: 'green', silly: 'grey' }` 349 | - `transports`: The transports for this logger, instantiated from `lumber.transports`, defaults to: `[new lumber.transports.Console()]` 350 | - `level`: The log level of this logger (can be overridden at the transport level), defaults to: `'info'` 351 | 352 | ### Transport Options 353 | 354 | Here are the options common to all transports: 355 | 356 | - `encoder`: The encoder to use for this transport, defaults vary. 357 | - `level`: The log level of this transport, defaults to: logger level. 358 | 359 | Each Transport has it's own additional options and defaults as well, only differences from the common list are mentioned: 360 | 361 | #### Console Transport 362 | 363 | - `encoder`: defaults to: `'text'` 364 | 365 | #### File Transport 366 | 367 | - `encoder`: defaults to: `'json'` 368 | - `filename`: The filename to log to, defaults to: `'app.log'` 369 | - `filemode`: The file permission to set on the file, defaults to: `'0666'` 370 | - `maxsize`: The max size of the file in byte before it is rotated, defaults to: `52428800` (50MB) 371 | - `rotate`: When rotating files if we have more than this number oldest logs are deleted, defaults to: `10` 372 | 373 | #### Webservice Transport 374 | 375 | - `encoder`: defaults to: `'json'` 376 | - `url`: The URL of the webservice, like `http://domain.com/service` 377 | - `method`: The method of the request to the webservice, defaults to: `'POST'` 378 | - `headers`: The headers to send with the request, defaults to the encoder's content type. 379 | - `secure`: Whether or not to use SSL, must be set for https requests, defaults to: `false` 380 | - `auth`: Authentication for basic auth, in the format `username:password` 381 | 382 | ### Encoder Options 383 | 384 | Here are the options common to all encoders: 385 | 386 | - `colorize`: Whether or not to apply the color scheme, defaults vary. 387 | - `timestamp`: Whether or not to apply a timestamp when encoding the log, defaults vary. 388 | - `headFormat`: The format of the message "head", the head is a formatted way of printing the log level of this message, defaults vary. 389 | - `dateFormat`: The format of the timestamps on logs, uses [node-dateformat](https://github.com/felixge/node-dateformat), defaults to: `'isoDateTime'` 390 | 391 | Each Encoder has it's own additional options and defaults as well, only differences from the common list are mentioned: 392 | 393 | #### Text Encoder 394 | 395 | - `colorize`: defaults to: `true` 396 | - `timestamp`: defaults to: `false` 397 | - `headFormat`: defaults to: `'%l: '` 398 | 399 | #### Json Encoder 400 | 401 | - `colorize`: currently has no effect 402 | - `timestamp`: defaults to: `true` 403 | - `headFormat`: defaults to: `'%L'` 404 | 405 | #### Xml Encoder 406 | 407 | - `colorize`: currently has no effect 408 | - `timestamp`: defaults to: `true` 409 | - `headFormat`: defaults to: `'%L'` 410 | 411 | ## TODO 412 | 413 | - Tests for on-the-fly changes of settings 414 | - Stream log back into memory 415 | - Query log for information 416 | - Docs on writing custom encoders/transports 417 | - MongoDB / Redis transports 418 | --------------------------------------------------------------------------------