bunyan-remote
Version:
Remote logger for node-bunyan that logs to the browser via Bunyan DevTools
446 lines (414 loc) • 15.7 kB
JavaScript
var BunyanLogger = {};
(function () {
var moment = null;
BunyanLogger.TIME_UTC = 1;
BunyanLogger.OM_LONG = 1;
BunyanLogger.OM_JSON = 2;
BunyanLogger.OM_INSPECT = 3;
BunyanLogger.OM_SIMPLE = 4;
BunyanLogger.OM_SHORT = 5;
BunyanLogger.OM_BUNYAN = 6;
BunyanLogger.OM_FROM_NAME = {
'long': BunyanLogger.OM_LONG,
'paul': BunyanLogger.OM_LONG, /* backward compat */
'json': BunyanLogger.OM_JSON,
'inspect': BunyanLogger.OM_INSPECT,
'simple': BunyanLogger.OM_SIMPLE,
'short': BunyanLogger.OM_SHORT,
'bunyan': BunyanLogger.OM_BUNYAN
};
// Levels
var TRACE = 10;
var DEBUG = 20;
var INFO = 30;
var WARN = 40;
var ERROR = 50;
var FATAL = 60;
var levelFromName = {
'trace': TRACE,
'debug': DEBUG,
'info': INFO,
'warn': WARN,
'error': ERROR,
'fatal': FATAL
};
var nameFromLevel = {};
var upperNameFromLevel = {};
var upperPaddedNameFromLevel = {};
Object.keys(levelFromName).forEach(function (name) {
var lvl = levelFromName[name];
nameFromLevel[lvl] = name;
upperNameFromLevel[lvl] = name.toUpperCase();
upperPaddedNameFromLevel[lvl] = (
name.length === 4 ? ' ' : '') + name.toUpperCase();
});
var inspect = JSON.stringify;
var formatRegExp = /%[sdj%]/g;
function format(f) {
if (typeof f !== 'string') {
var objects = [];
for (var i = 0; i < arguments.length; i++) {
objects.push(inspect(arguments[i]));
}
return objects.join(' ');
}
var i = 1;
var args = arguments;
var len = args.length;
var str = String(f).replace(formatRegExp, function (x) {
if (i >= len)
return x;
switch (x) {
case '%s': return String(args[i++]);
case '%d': return Number(args[i++]);
case '%j': return JSON.stringify(args[i++]);
case '%%': return '%';
default:
return x;
}
});
for (var x = args[i]; i < len; x = args[++i]) {
if (x === null || typeof x !== 'object') {
str += ' ' + x;
} else {
str += ' ' + inspect(x);
}
}
return str;
};
function isValidRecord(rec) {
if (rec.v == null ||
rec.level == null ||
rec.name == null ||
rec.hostname == null ||
rec.pid == null ||
rec.time == null ||
rec.msg == null) {
// Not valid Bunyan log.
return false;
} else {
return true;
}
}
function indent(s) {
return ' ' + s.split(/\r?\n/).join('\n ');
}
BunyanLogger.emitRecord = function (rec, line, opts, stylize) {
var short = false;
rec = JSON.parse(JSON.stringify(rec));
switch (opts.outputMode) {
case BunyanLogger.OM_SHORT:
short = true;
/* jsl:fall-thru */
case BunyanLogger.OM_LONG:
// [time] LEVEL: name[/comp]/pid on hostname (src): msg* (extras...)
// msg*
// --
// long and multi-line extras
// ...
// If 'msg' is single-line, then it goes in the top line.
// If 'req', show the request.
// If 'res', show the response.
// If 'err' and 'err.stack' then show that.
if (!isValidRecord(rec)) {
return opts.emit(line + '\n', rec);
}
delete rec.v;
// Time.
var time;
if (!short && opts.timeFormat === BunyanLogger.TIME_UTC) {
// Fast default path: We assume the raw `rec.time` is a UTC time
// in ISO 8601 format (per spec).
time = '[' + rec.time + ']';
} else if (!moment && opts.timeFormat === BunyanLogger.TIME_UTC) {
// Don't require momentjs install, as long as not using TIME_LOCAL.
time = rec.time.substr(11);
} else {
var tzFormat;
var moTime = moment(rec.time);
switch (opts.timeFormat) {
case BunyanLogger.TIME_UTC:
tzFormat = TIMEZONE_UTC_FORMATS[short ? 'short' : 'long'];
moTime.utc();
break;
case TIME_LOCAL:
tzFormat = TIMEZONE_LOCAL_FORMATS[short ? 'short' : 'long'];
break;
default:
throw new Error('unexpected timeFormat: ' + opts.timeFormat);
};
time = moTime.format(tzFormat);
}
time = stylize(time, 'XXX');
delete rec.time;
var nameStr = rec.name;
delete rec.name;
if (rec.component) {
nameStr += '/' + rec.component;
}
delete rec.component;
if (!short)
nameStr += '/' + rec.pid;
delete rec.pid;
var level = (upperPaddedNameFromLevel[rec.level] || 'LVL' + rec.level);
if (opts.color) {
var colorFromLevel = {
10: '#999999', // TRACE
20: '#F4BF00', // DEBUG
30: '#1ABEE3', // INFO
40: '#E31ACB', // WARN
50: '#FF0000', // ERROR
60: 'white/#FF0000', // FATAL
};
level = stylize(level, colorFromLevel[rec.level]);
}
delete rec.level;
var src = '';
if (rec.src && rec.src.file) {
var s = rec.src;
if (s.func) {
src = format(' (%s:%d in %s)', s.file, s.line, s.func);
} else {
src = format(' (%s:%d)', s.file, s.line);
}
src = stylize(src, 'green');
}
delete rec.src;
var hostname = rec.hostname;
delete rec.hostname;
var extras = [];
var details = [];
if (rec.req_id) {
extras.push('req_id=' + rec.req_id);
}
delete rec.req_id;
var onelineMsg;
if (rec.msg.indexOf('\n') !== -1) {
onelineMsg = '';
details.push(indent(stylize(rec.msg, '#1ABEE3')));
} else {
onelineMsg = ' ' + stylize(rec.msg, '#1ABEE3');
}
delete rec.msg;
if (rec.req && typeof (rec.req) === 'object') {
var req = rec.req;
delete rec.req;
var headers = req.headers;
if (!headers) {
headers = '';
} else if (typeof (headers) === 'string') {
headers = '\n' + headers;
} else if (typeof (headers) === 'object') {
headers = '\n' + Object.keys(headers).map(function (h) {
return h + ': ' + headers[h];
}).join('\n');
}
var s = format('%s %s HTTP/%s%s', req.method,
req.url,
req.httpVersion || '1.1',
headers
);
delete req.url;
delete req.method;
delete req.httpVersion;
delete req.headers;
if (req.body) {
s += '\n\n' + (typeof (req.body) === 'object'
? JSON.stringify(req.body, null, 2) : req.body);
delete req.body;
}
if (req.trailers && Object.keys(req.trailers) > 0) {
s += '\n' + Object.keys(req.trailers).map(function (t) {
return t + ': ' + req.trailers[t];
}).join('\n');
}
delete req.trailers;
details.push(indent(s));
// E.g. for extra 'foo' field on 'req', add 'req.foo' at
// top-level. This *does* have the potential to stomp on a
// literal 'req.foo' key.
Object.keys(req).forEach(function (k) {
rec['req.' + k] = req[k];
})
}
if (rec.client_req && typeof (rec.client_req) === 'object') {
var client_req = rec.client_req;
delete rec.client_req;
var headers = client_req.headers;
var hostHeaderLine = '';
var s = '';
if (client_req.address) {
hostHeaderLine = '\nHost: ' + client_req.address;
if (client_req.port)
hostHeaderLine += ':' + client_req.port;
}
delete client_req.headers;
delete client_req.address;
delete client_req.port;
s += format('%s %s HTTP/%s%s%s', client_req.method,
client_req.url,
client_req.httpVersion || '1.1',
hostHeaderLine,
(headers ?
'\n' + Object.keys(headers).map(
function (h) {
return h + ': ' + headers[h];
}).join('\n') :
''));
delete client_req.method;
delete client_req.url;
delete client_req.httpVersion;
if (client_req.body) {
s += '\n\n' + (typeof (client_req.body) === 'object' ?
JSON.stringify(client_req.body, null, 2) :
client_req.body);
delete client_req.body;
}
// E.g. for extra 'foo' field on 'client_req', add
// 'client_req.foo' at top-level. This *does* have the potential
// to stomp on a literal 'client_req.foo' key.
Object.keys(client_req).forEach(function (k) {
rec['client_req.' + k] = client_req[k];
})
details.push(indent(s));
}
function _res(res) {
var s = '';
if (res.statusCode !== undefined) {
s += format('HTTP/1.1 %s %s\n', res.statusCode,
http.STATUS_CODES[res.statusCode]);
delete res.statusCode;
}
// Handle `res.header` or `res.headers` as either a string or
// and object of header key/value pairs. Prefer `res.header` if set
// (TODO: Why? I don't recall. Typical of restify serializer?
// Typical JSON.stringify of a core node HttpResponse?)
var headerTypes = {string: true, object: true};
var headers;
if (res.header && headerTypes[typeof (res.header)]) {
headers = res.header;
delete res.header;
} else if (res.headers && headerTypes[typeof (res.headers)]) {
headers = res.headers;
delete res.headers;
}
if (headers === undefined) {
/* pass through */
} else if (typeof (headers) === 'string') {
s += headers.trimRight();
} else {
s += Object.keys(headers).map(
function (h) { return h + ': ' + headers[h]; }).join('\n');
}
if (res.body !== undefined) {
var body = (typeof (res.body) === 'object'
? JSON.stringify(res.body, null, 2) : res.body);
if (body.length > 0) { s += '\n\n' + body };
delete res.body;
} else {
s = s.trimRight();
}
if (res.trailer) {
s += '\n' + res.trailer;
}
delete res.trailer;
if (s) {
details.push(indent(s));
}
// E.g. for extra 'foo' field on 'res', add 'res.foo' at
// top-level. This *does* have the potential to stomp on a
// literal 'res.foo' key.
Object.keys(res).forEach(function (k) {
rec['res.' + k] = res[k];
});
}
if (rec.res && typeof (rec.res) === 'object') {
_res(rec.res);
delete rec.res;
}
if (rec.client_res && typeof (rec.client_res) === 'object') {
_res(rec.client_res);
delete rec.client_res;
}
if (rec.err && rec.err.stack) {
var err = rec.err
if (typeof (err.stack) !== 'string') {
details.push('<pre>' + indent(err.stack.toString()) + '</pre>');
} else {
details.push('<pre>' + indent(err.stack) + '</pre>');
}
delete err.message;
delete err.name;
delete err.stack;
// E.g. for extra 'foo' field on 'err', add 'err.foo' at
// top-level. This *does* have the potential to stomp on a
// literal 'err.foo' key.
Object.keys(err).forEach(function (k) {
rec['err.' + k] = err[k];
})
delete rec.err;
}
// var leftover = Object.keys(rec);
// for (var i = 0; i < leftover.length; i++) {
// var key = leftover[i];
// var value = rec[key];
// var stringified = false;
// if (typeof (value) !== 'string') {
// value = JSON.stringify(value, null, 2);
// stringified = true;
// }
// if (value.indexOf('\n') !== -1 || value.length > 50) {
// details.push(indent(key + ': ' + value));
// } else if (!stringified && (value.indexOf(' ') != -1 ||
// value.length === 0))
// {
// extras.push(key + '=' + JSON.stringify(value));
// } else {
// extras.push(key + '=' + value);
// }
// }
details = stylize(
(details.length ? details.join('\n --\n') + '\n' : ''), 'XXX');
if (!short)
opts.emit(format('%s %s: %s on %s%s:%s%s\n%s',
time,
level,
nameStr,
hostname || '<no-hostname>',
src,
onelineMsg,
extras,
details), rec);
else
opts.emit(format('%s %s %s:%s%s\n%s',
time,
level,
nameStr,
onelineMsg,
extras,
details), rec);
break;
case BunyanLogger.OM_INSPECT:
opts.emit(util.inspect(rec, false, Infinity, true) + '\n', rec);
break;
case BunyanLogger.OM_BUNYAN:
opts.emit(JSON.stringify(rec, null, 0) + '\n', rec);
break;
case BunyanLogger.OM_JSON:
opts.emit(JSON.stringify(rec, null, opts.jsonIndent) + '\n', rec);
break;
case BunyanLogger.OM_SIMPLE:
/* JSSTYLED */
// <http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/SimpleLayout.html>
if (!isValidRecord(rec)) {
return opts.emit(line + '\n', rec);
}
opts.emit(format('%s - %s\n',
upperNameFromLevel[rec.level] || 'LVL' + rec.level,
rec.msg), rec);
break;
default:
throw new Error('unknown output mode: '+opts.outputMode);
}
}
})();