start serializers support. Add 'req' standard serializer. You can add your own.
This commit is contained in:
parent
193120a25f
commit
947e46f877
4 changed files with 201 additions and 11 deletions
42
README.md
42
README.md
|
@ -90,7 +90,49 @@ same config as its parent, plus include the "component" field.
|
||||||
log.info("done with the wuzzle")
|
log.info("done with the wuzzle")
|
||||||
|
|
||||||
|
|
||||||
|
Back to the `log.{trace|debug|...|fatal}(...)` API:
|
||||||
|
|
||||||
|
log.info(); // returns a boolean: is the "info" level enabled?
|
||||||
|
log.info("hi"); // log a simple string message
|
||||||
|
log.info("hi %s", bob, anotherVar); // uses `util.format` for msg formatting
|
||||||
|
log.info({foo: "bar"}, "hi"); // adds "foo" field to log record
|
||||||
|
|
||||||
|
Bunyan has a concept of **"serializers" to produce a JSON-able object from a
|
||||||
|
JavaScript object**, so your can easily do the following:
|
||||||
|
|
||||||
|
log.info({req: <request object>}, "something about handling this request");
|
||||||
|
|
||||||
|
Association is by log record field name, "req" in this example, so this
|
||||||
|
requires a registered serializer something like this:
|
||||||
|
|
||||||
|
function reqSerializer(req) {
|
||||||
|
return {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
headers: req.headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var log = new Logger({
|
||||||
|
...
|
||||||
|
serializers: {
|
||||||
|
req: reqSerializer
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Or this:
|
||||||
|
|
||||||
|
var log = new Logger({
|
||||||
|
...
|
||||||
|
serializers: {req: Logger.stdSerializers.req}
|
||||||
|
});
|
||||||
|
|
||||||
|
because Buyan includes a small set of standard serializers. To use all the
|
||||||
|
standard serializers you can use:
|
||||||
|
|
||||||
|
var log = new Logger({
|
||||||
|
...
|
||||||
|
serializers: Logger.stdSerializers
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
# Future
|
# Future
|
||||||
|
|
16
TODO.md
16
TODO.md
|
@ -1,7 +1,12 @@
|
||||||
- serializer support (i.e. repr of a restify request obj):
|
- serializer support:
|
||||||
server.js example
|
- Ask mark what else to put in `req`
|
||||||
restify-server.js example
|
- Ask mark what to put in `res`
|
||||||
- 'x' extra fields object or no? discuss
|
- restify-server.js example -> restifyReq ? or have `req` detect that.
|
||||||
|
That is nicer for the "use all standard ones". *Does* restify req
|
||||||
|
have anything special?
|
||||||
|
- Add `err`.
|
||||||
|
- `request_id` that pulls it from req? `log.info({request_id: req}, "hi")`
|
||||||
|
- `log.close` to close streams and shutdown and `this.closed`
|
||||||
- expand set of fields: from dap
|
- expand set of fields: from dap
|
||||||
time, hostname
|
time, hostname
|
||||||
<https://github.com/Graylog2/graylog2-docs/wiki/GELF>
|
<https://github.com/Graylog2/graylog2-docs/wiki/GELF>
|
||||||
|
@ -32,10 +37,13 @@
|
||||||
- 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
|
- "canWrite" handling for full streams. Need to buffer a la log4js
|
||||||
|
- test file log with logadm rotation: does it handle that?
|
||||||
- test suite:
|
- test suite:
|
||||||
- test for a cloned logger double-`stream.end()` causing problems.
|
- test for a cloned logger double-`stream.end()` causing problems.
|
||||||
Perhaps the "closeOnExit" for existing streams should be false for
|
Perhaps the "closeOnExit" for existing streams should be false for
|
||||||
clones.
|
clones.
|
||||||
|
- test that a `log.clone(...)` adding a new field matching a serializer
|
||||||
|
works *and* that an existing field in the parent is not *re-serialized*.
|
||||||
- a "rolling-file" stream: but specifically by time, e.g. hourly. (MarkC
|
- a "rolling-file" stream: but specifically by time, e.g. hourly. (MarkC
|
||||||
requested)
|
requested)
|
||||||
|
|
||||||
|
|
30
examples/server.js
Normal file
30
examples/server.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
var http = require('http');
|
||||||
|
var Logger = require('../lib/bunyan');
|
||||||
|
|
||||||
|
// Basic usage.
|
||||||
|
var log = new Logger({
|
||||||
|
service: "myserver",
|
||||||
|
serializers: {req: Logger.stdSerializers.req}
|
||||||
|
});
|
||||||
|
|
||||||
|
http.createServer(function (req, res) {
|
||||||
|
log.info({req: req}, "start request"); // <-- this is the guy we're testing
|
||||||
|
res.writeHead(200, {'Content-Type': 'text/plain'});
|
||||||
|
res.end('Hello World\n');
|
||||||
|
}).listen(1337, "127.0.0.1", function () {
|
||||||
|
log.info("server listening");
|
||||||
|
var options = {
|
||||||
|
port: 1337,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
headers: {
|
||||||
|
'X-Hi': 'Mom'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var req = http.request(options);
|
||||||
|
req.on('response', function (res) {
|
||||||
|
res.on('end', function () {
|
||||||
|
process.exit();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
req.end();
|
||||||
|
});
|
124
lib/bunyan.js
124
lib/bunyan.js
|
@ -11,7 +11,7 @@ var LOG_VERSION = 0;
|
||||||
|
|
||||||
|
|
||||||
var xxx = function xxx(s) { // internal dev/debug logging
|
var xxx = function xxx(s) { // internal dev/debug logging
|
||||||
var args = ["PAUL: "+s].concat(Array.prototype.slice.call(arguments, 1));
|
var args = ['XX' + 'X: '+s].concat(Array.prototype.slice.call(arguments, 1));
|
||||||
console.error.apply(this, args);
|
console.error.apply(this, args);
|
||||||
};
|
};
|
||||||
var xxx = function xxx() {}; // uncomment to turn of debug logging
|
var xxx = function xxx() {}; // uncomment to turn of debug logging
|
||||||
|
@ -101,10 +101,20 @@ function getLevel(nameOrNum) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//---- Logger class
|
//---- Logger class
|
||||||
|
|
||||||
function Logger(options) {
|
/**
|
||||||
paul('Logger start:', options)
|
* Create a Logger instance.
|
||||||
|
*
|
||||||
|
* @param options {Object} See documentation for full details. At minimum
|
||||||
|
* this must include a "service" string key.
|
||||||
|
* @param _newCloneKeys {Array} Internal var. Should not be used externally.
|
||||||
|
* Array of new keys for this clone. This is necessary to assist with
|
||||||
|
* applying necessary serializers to the new keys.
|
||||||
|
*/
|
||||||
|
function Logger(options, _newCloneKeys) {
|
||||||
|
xxx('Logger start:', options)
|
||||||
if (! this instanceof Logger) {
|
if (! this instanceof Logger) {
|
||||||
return new Logger(options);
|
return new Logger(options);
|
||||||
}
|
}
|
||||||
|
@ -116,6 +126,9 @@ function Logger(options) {
|
||||||
if (options.stream && options.streams) {
|
if (options.stream && options.streams) {
|
||||||
throw new TypeError('can only have one of "stream" or "streams"');
|
throw new TypeError('can only have one of "stream" or "streams"');
|
||||||
}
|
}
|
||||||
|
if (_newCloneKeys && !Array.isArray(_newCloneKeys)) {
|
||||||
|
throw new TypeError('_newCloneKeys (Array) is an internal var');
|
||||||
|
}
|
||||||
|
|
||||||
// These are the default fields for log records (minus the attributes
|
// These are the default fields for log records (minus the attributes
|
||||||
// removed in this constructor). To allow storing raw log records
|
// removed in this constructor). To allow storing raw log records
|
||||||
|
@ -136,6 +149,7 @@ function Logger(options) {
|
||||||
} else {
|
} else {
|
||||||
level = INFO;
|
level = INFO;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.streams = [];
|
this.streams = [];
|
||||||
if (options.stream) {
|
if (options.stream) {
|
||||||
this.streams.push({
|
this.streams.push({
|
||||||
|
@ -210,15 +224,41 @@ function Logger(options) {
|
||||||
}
|
}
|
||||||
this.level = lowestLevel;
|
this.level = lowestLevel;
|
||||||
|
|
||||||
|
delete this.fields.serializers;
|
||||||
|
if (!options.serializers) {
|
||||||
|
this.serializers = null;
|
||||||
|
} else {
|
||||||
|
this.serializers = {};
|
||||||
|
Object.keys(options.serializers).forEach(function (field) {
|
||||||
|
var serializer = options.serializers[field];
|
||||||
|
if (typeof(serializer) !== "function") {
|
||||||
|
throw new TypeError(format(
|
||||||
|
'invalid serializer for "%s" field: must be a function', field));
|
||||||
|
} else {
|
||||||
|
self.serializers[field] = serializer;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
xxx("Logger: ", self)
|
xxx("Logger: ", self)
|
||||||
|
|
||||||
|
// Apply serializers to initial fields.
|
||||||
|
if (this.serializers) {
|
||||||
|
if (_newCloneKeys && _newCloneKeys.length > 0) {
|
||||||
|
// Note that this includes *config* vars send to `log.clone()` in
|
||||||
|
// addition to log record *fields*, so the impl. needs to handle that.
|
||||||
|
this._applySerializers(this.fields, _newCloneKeys);
|
||||||
|
} else {
|
||||||
|
this._applySerializers(this.fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Automatic fields.
|
// Automatic fields.
|
||||||
if (!this.fields.hostname) {
|
if (!this.fields.hostname) {
|
||||||
this.fields.hostname = os.hostname();
|
this.fields.hostname = os.hostname();
|
||||||
}
|
}
|
||||||
|
|
||||||
//XXX Non-core fields should go in 'x' sub-object.
|
//XXX Turn this on or ditch it.
|
||||||
|
|
||||||
//process.on('exit', function () {
|
//process.on('exit', function () {
|
||||||
// self.streams.forEach(function (s) {
|
// self.streams.forEach(function (s) {
|
||||||
// if (s.closeOnExit) {
|
// if (s.closeOnExit) {
|
||||||
|
@ -250,11 +290,54 @@ Logger.prototype.clone = function (options) {
|
||||||
var cloneOptions = objCopy(this.fields);
|
var cloneOptions = objCopy(this.fields);
|
||||||
cloneOptions.streams = this.streams;
|
cloneOptions.streams = this.streams;
|
||||||
if (options) {
|
if (options) {
|
||||||
Object.keys(options).forEach(function(k) {
|
var newCloneKeys = Object.keys(options);
|
||||||
|
newCloneKeys.forEach(function(k) {
|
||||||
cloneOptions[k] = options[k];
|
cloneOptions[k] = options[k];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new Logger(cloneOptions);
|
return new Logger(cloneOptions, newCloneKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply registered serializers to the appropriate keys in the given fields.
|
||||||
|
*
|
||||||
|
* Pre-condition: This is only called if there is at least one serializer.
|
||||||
|
*
|
||||||
|
* @param fields (Object) The log record fields.
|
||||||
|
* @param keys (Array) Optional array of keys to which to limit processing.
|
||||||
|
*/
|
||||||
|
Logger.prototype._applySerializers = function (fields, keys) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Mapping of keys to potentially serialize.
|
||||||
|
var applyKeys = fields;
|
||||||
|
if (keys) {
|
||||||
|
applyKeys = {};
|
||||||
|
for (var i=0; i < keys.length; i++) {
|
||||||
|
applyKeys[keys[i]] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xxx('_applySerializers: applyKeys', applyKeys);
|
||||||
|
|
||||||
|
// Check each serializer against these (presuming number of serializers
|
||||||
|
// is typically less than number of fields).
|
||||||
|
Object.keys(this.serializers).forEach(function (name) {
|
||||||
|
if (applyKeys[name]) {
|
||||||
|
xxx('_applySerializers; apply to "%s" key', name)
|
||||||
|
try {
|
||||||
|
fields[name] = self.serializers[name](fields[name]);
|
||||||
|
} catch (err) {
|
||||||
|
process.stderr.write(format('bunyan: ERROR: This should never happen. '
|
||||||
|
+ 'This is a bug in <https://github.com/trentm/node-bunyan> or '
|
||||||
|
+ 'in this application. Exception from "%s" Logger serializer: %s',
|
||||||
|
name, err.stack || err));
|
||||||
|
fields[name] = format('(Error in Bunyan log "%s" serializer '
|
||||||
|
+ 'broke field. See stderr for details.)', name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -282,6 +365,9 @@ Logger.prototype._emit = function (rec) {
|
||||||
var obj = objCopy(rec[0]);
|
var obj = objCopy(rec[0]);
|
||||||
var recFields = rec[1];
|
var recFields = rec[1];
|
||||||
if (recFields) {
|
if (recFields) {
|
||||||
|
if (this.serializers) {
|
||||||
|
this._applySerializers(recFields);
|
||||||
|
}
|
||||||
Object.keys(recFields).forEach(function (k) {
|
Object.keys(recFields).forEach(function (k) {
|
||||||
obj[k] = recFields[k];
|
obj[k] = recFields[k];
|
||||||
});
|
});
|
||||||
|
@ -294,6 +380,7 @@ Logger.prototype._emit = function (rec) {
|
||||||
}
|
}
|
||||||
obj.v = LOG_VERSION;
|
obj.v = LOG_VERSION;
|
||||||
|
|
||||||
|
xxx('_emit: stringify this:', obj);
|
||||||
var str = JSON.stringify(obj) + '\n';
|
var str = JSON.stringify(obj) + '\n';
|
||||||
this.streams.forEach(function(s) {
|
this.streams.forEach(function(s) {
|
||||||
if (s.level <= level) {
|
if (s.level <= level) {
|
||||||
|
@ -486,6 +573,29 @@ Logger.prototype.fatal = function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//---- Standard serializers
|
||||||
|
// A serializer is a function that serializes a JavaScript object to a
|
||||||
|
// JSON representation for logging. There is a standard set of presumed
|
||||||
|
// interesting objects in node.js-land.
|
||||||
|
|
||||||
|
Logger.stdSerializers = {};
|
||||||
|
|
||||||
|
// Serialize an HTTP request.
|
||||||
|
Logger.stdSerializers.req = function req(req) {
|
||||||
|
return {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
headers: req.headers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//---- Exports
|
||||||
|
|
||||||
module.exports = Logger;
|
module.exports = Logger;
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue