multiple streams support at different levels; add 'file' stream type

master
Trent Mick 2012-01-30 14:28:02 -08:00
parent 6d94985f2d
commit fdb9114218
5 changed files with 162 additions and 28 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/tmp

26
TODO.md
View File

@ -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 - `bunyan` cli
- expand set of fields - renderer support (i.e. repr of a restify request obj)
- expand set of fields: from dap
time, hostname
<https://github.com/Graylog2/graylog2-docs/wiki/GELF> <https://github.com/Graylog2/graylog2-docs/wiki/GELF>
<http://journal.paul.querna.org/articles/2011/12/26/log-for-machines-in-json/> <http://journal.paul.querna.org/articles/2011/12/26/log-for-machines-in-json/>
require: facility and hostname require: facility and hostname
- renderer support (i.e. repr of a restify request obj)
- docs - docs
- feel out usage - mark wants pretty output for debug output
- not sure about `log.info()` for is-enabled. Perhaps `log.isInfo()` because - not sure if 'bunyan --pretty' or whatever would suffice
can then use that for `log.isInfo(true)` for 'ring' argument. Separate level - ringBuffer stream
and ringLevel. - 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.set to mutate config or `this.fields`
- Logger.del to remove a field - Logger.del to remove a field
- "canWrite" handling for full streams. Need to buffer a la log4js
- test suite - test suite

View File

@ -1,14 +1,22 @@
var Logger = require('./lib/bunyan'); var Logger = require('../lib/bunyan');
// Basic usage.
var log = new Logger({facility: "myapp", level: "info"}); var log = new Logger({facility: "myapp", level: "info"});
// isInfoEnabled replacement
console.log("log.info() is:", log.info()) console.log("log.info() is:", log.info())
// `util.format`-based printf handling
log.info("hi"); log.info("hi");
log.info("hi", "trent"); log.info("hi", "trent");
log.info("hi %s there", true); 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); 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) { function Wuzzle(options) {
this.log = options.log; this.log = options.log;

21
examples/multi.js Normal file
View File

@ -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");

View File

@ -97,6 +97,12 @@ var nameFromLevel = {
FATAL: 'fatal' FATAL: 'fatal'
}; };
function getLevel(nameOrNum) {
return (typeof(nameOrNum) === 'string'
? levelFromName[nameOrNum]
: nameOrNum);
}
//---- Logger class //---- Logger class
@ -105,8 +111,13 @@ function Logger(options) {
if (! this instanceof Logger) { if (! this instanceof Logger) {
return new Logger(options); return new Logger(options);
} }
var self = this;
if (!options) { 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 // These are the default fields for log records (minus the attributes
@ -115,25 +126,105 @@ function Logger(options) {
// any changes. // any changes.
this.fields = objCopy(options); this.fields = objCopy(options);
if (options.stream) { // Extract and setup the configuration options (the remaining ones are
this.stream = options.stream; // log record fields).
delete this.fields.stream; var lowestLevel = Number.POSITIVE_INFINITY;
} else { var level;
this.stream = process.stdout;
}
if (options.level) { if (options.level) {
this.level = (typeof(options.level) === 'string' level = getLevel(options.level);
? levelFromName[options.level] if (! (DEBUG <= level && level <= FATAL)) {
: options.level);
if (! (DEBUG <= this.level && this.level <= FATAL)) {
throw new Error('invalid level: ' + options.level); throw new Error('invalid level: ' + options.level);
} }
delete this.fields.level; delete this.fields.level;
} else { } 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. //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. * Supports the same set of options as the constructor.
*/ */
Logger.prototype.clone = function (options) { Logger.prototype.clone = function (options) {
paul("keys", Object.keys(this))
var cloneOptions = objCopy(this.fields); var cloneOptions = objCopy(this.fields);
cloneOptions.level = this.level; cloneOptions.streams = this.streams;
cloneOptions.stream = this.stream;
if (options) { if (options) {
Object.keys(options).forEach(function(k) { Object.keys(options).forEach(function(k) {
cloneOptions[k] = options[k]; cloneOptions[k] = options[k];
@ -195,11 +284,18 @@ Logger.prototype._emit = function (rec) {
obj[k] = recFields[k]; obj[k] = recFields[k];
}); });
} }
obj.level = rec[2]; var level = obj.level = rec[2];
paul("Record:", rec) paul("Record:", rec)
obj.msg = format.apply(this, rec[3]); obj.msg = format.apply(this, rec[3]);
obj.v = VERSION; 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);
}
});
} }