From fdb9114218ca6d39b794f2cdfec43e33bf20bad7 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Mon, 30 Jan 2012 14:28:02 -0800 Subject: [PATCH] multiple streams support at different levels; add 'file' stream type --- .gitignore | 1 + TODO.md | 26 +++++--- hi.js => examples/hi.js | 12 +++- examples/multi.js | 21 +++++++ lib/bunyan.js | 130 ++++++++++++++++++++++++++++++++++------ 5 files changed, 162 insertions(+), 28 deletions(-) create mode 100644 .gitignore rename hi.js => examples/hi.js (65%) create mode 100644 examples/multi.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ceeb05b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/tmp diff --git a/TODO.md b/TODO.md index e983a90..9212458 100644 --- a/TODO.md +++ b/TODO.md @@ -1,17 +1,25 @@ -- mark wants pretty output for debug output - - not sure if 'bunyan --pretty' or whatever would suffice -- mark suggested a list of streams. This is what ring could be. - `bunyan` cli -- expand set of fields +- renderer support (i.e. repr of a restify request obj) +- expand set of fields: from dap + time, hostname require: facility and hostname -- renderer support (i.e. repr of a restify request obj) - docs -- feel out usage -- not sure about `log.info()` for is-enabled. Perhaps `log.isInfo()` because - can then use that for `log.isInfo(true)` for 'ring' argument. Separate level - and ringLevel. +- mark wants pretty output for debug output + - not sure if 'bunyan --pretty' or whatever would suffice +- ringBuffer stream +- syslog: Josh uses https://github.com/chrisdew/node-syslog + streams: [ + ... + { + level: "warn", + type: "syslog", + syslog_facility: "LOG_LOCAL1", // one of the syslog facility defines + syslog_pid: true, // syslog logopt "LOG_PID" + syslog_cons: false // syslog logopt "LOG_CONS" + } - Logger.set to mutate config or `this.fields` - Logger.del to remove a field +- "canWrite" handling for full streams. Need to buffer a la log4js - test suite diff --git a/hi.js b/examples/hi.js similarity index 65% rename from hi.js rename to examples/hi.js index 93af3de..1c9b5d6 100644 --- a/hi.js +++ b/examples/hi.js @@ -1,14 +1,22 @@ -var Logger = require('./lib/bunyan'); +var Logger = require('../lib/bunyan'); +// Basic usage. var log = new Logger({facility: "myapp", level: "info"}); + +// isInfoEnabled replacement console.log("log.info() is:", log.info()) + +// `util.format`-based printf handling log.info("hi"); log.info("hi", "trent"); log.info("hi %s there", true); + +// First arg as an object adds fields to the log record. log.info({foo:"bar"}, "hi %d", 1, "two", 3); -console.log("\n--\n") +// Shows `log.clone(...)` to specialize a logger for a sub-component. +console.log("\n\n") function Wuzzle(options) { this.log = options.log; diff --git a/examples/multi.js b/examples/multi.js new file mode 100644 index 0000000..cbb3c3f --- /dev/null +++ b/examples/multi.js @@ -0,0 +1,21 @@ +var Logger = require('../lib/bunyan'); +log = new Logger({ + service: "amon", + streams: [ + { + level: "info", + stream: process.stdout, + }, + { + level: "error", + path: "tmp/error.log" + // when to close file stream? + } + ] +}); + + +log.debug("hi nobody on debug"); +log.info("hi stdout on info"); +log.error("hi both on error"); +log.fatal("hi both on fatal"); diff --git a/lib/bunyan.js b/lib/bunyan.js index 0e4f87d..4c6497a 100644 --- a/lib/bunyan.js +++ b/lib/bunyan.js @@ -97,6 +97,12 @@ var nameFromLevel = { FATAL: 'fatal' }; +function getLevel(nameOrNum) { + return (typeof(nameOrNum) === 'string' + ? levelFromName[nameOrNum] + : nameOrNum); +} + //---- Logger class @@ -105,8 +111,13 @@ function Logger(options) { if (! this instanceof Logger) { return new Logger(options); } + + var self = this; if (!options) { - throw new TypeError("options (object) is required"); + throw new TypeError('options (object) is required'); + } + if (options.stream && options.streams) { + throw new TypeError('can only have one of "stream" or "streams"'); } // These are the default fields for log records (minus the attributes @@ -115,25 +126,105 @@ function Logger(options) { // any changes. this.fields = objCopy(options); - if (options.stream) { - this.stream = options.stream; - delete this.fields.stream; - } else { - this.stream = process.stdout; - } + // Extract and setup the configuration options (the remaining ones are + // log record fields). + var lowestLevel = Number.POSITIVE_INFINITY; + var level; if (options.level) { - this.level = (typeof(options.level) === 'string' - ? levelFromName[options.level] - : options.level); - if (! (DEBUG <= this.level && this.level <= FATAL)) { + level = getLevel(options.level); + if (! (DEBUG <= level && level <= FATAL)) { throw new Error('invalid level: ' + options.level); } delete this.fields.level; } else { - this.level = 2; // INFO is default level. + level = INFO; } + this.streams = []; + if (options.stream) { + this.streams.push({ + type: "stream", + stream: options.stream, + closeOnExit: false, + level: level + }); + if (level < lowestLevel) { + lowestLevel = level; + } + delete this.fields.stream; + } else if (options.streams) { + options.streams.forEach(function (s) { + s = objCopy(s); + + // Implicit 'type' from other args. + type = s.type; + if (!s.type) { + if (s.stream) { + s.type = "stream"; + } else if (s.path) { + s.type = "file" + } + } + + if (s.level) { + s.level = getLevel(s.level); + } else { + s.level = level; + } + if (s.level < lowestLevel) { + lowestLevel = s.level; + } + + switch (s.type) { + case "stream": + if (!s.closeOnExit) { + s.closeOnExit = false; + } + break; + case "file": + if (!s.stream) { + s.stream = fs.createWriteStream(s.path, + {flags: 'a', encoding: 'utf8'}); + if (!s.closeOnExit) { + s.closeOnExit = true; + } + } else { + if (!s.closeOnExit) { + s.closeOnExit = false; + } + } + break; + default: + throw new TypeError('unknown stream type "' + s.type + '"'); + } + + self.streams.push(s); + }); + delete this.fields.streams; + } else { + this.streams.push({ + type: "stream", + stream: process.stdout, + closeOnExit: false, + level: level + }); + if (level < lowestLevel) { + lowestLevel = level; + } + } + this.level = lowestLevel; + + paul("Logger: ", self) //XXX Non-core fields should go in 'x' sub-object. + + //process.on('exit', function () { + // self.streams.forEach(function (s) { + // if (s.closeOnExit) { + // paul("closing stream s:", s); + // s.stream.end(); + // } + // }); + //}); } @@ -154,10 +245,8 @@ function Logger(options) { * Supports the same set of options as the constructor. */ Logger.prototype.clone = function (options) { - paul("keys", Object.keys(this)) var cloneOptions = objCopy(this.fields); - cloneOptions.level = this.level; - cloneOptions.stream = this.stream; + cloneOptions.streams = this.streams; if (options) { Object.keys(options).forEach(function(k) { cloneOptions[k] = options[k]; @@ -195,11 +284,18 @@ Logger.prototype._emit = function (rec) { obj[k] = recFields[k]; }); } - obj.level = rec[2]; + var level = obj.level = rec[2]; paul("Record:", rec) obj.msg = format.apply(this, rec[3]); obj.v = VERSION; - this.stream.write(JSON.stringify(obj) + '\n'); + var str = JSON.stringify(obj) + '\n'; + this.streams.forEach(function(s) { + if (s.level <= level) { + paul('writing log rec "%s" to "%s" stream (%d <= %d)', obj.msg, s.type, + s.level, level); + s.stream.write(str); + } + }); }