node-bunyan-lite/bin/bunyan

431 lines
11 KiB
Text
Raw Normal View History

#!/usr/bin/env node
//
// bunyan -- filter and pretty-print JSON logs, like Bunyan logs.
//
// See <https://github.com/trentm/node-bunyan>.
//
2012-02-02 23:16:28 +00:00
var VERSION = "0.1.1";
var util = require('util');
var pathlib = require('path');
var vm = require('vm');
var warn = console.warn;
//---- globals and constants
// Output modes.
var OM_PAUL = 1;
var OM_JSON = 2;
var OM_INSPECT = 3;
var OM_SIMPLE = 4;
var OM_FROM_NAME = {
"paul": OM_PAUL,
"json": OM_JSON,
"inspect": OM_INSPECT,
"simple": OM_SIMPLE
}
// Levels
var TRACE = 1;
var DEBUG = 2;
var INFO = 3;
var WARN = 4;
var ERROR = 5;
var FATAL = 6;
var levelFromName = {
'trace': TRACE,
'debug': DEBUG,
'info': INFO,
'warn': WARN,
'error': ERROR,
'fatal': FATAL
};
var nameFromLevel = [undefined].concat(Object.keys(levelFromName));
var upperNameFromLevel = [undefined].concat(
Object.keys(levelFromName).map(function (l) { return l.toUpperCase(); }));
//---- support functions
function getVersion() {
return VERSION;
}
var format = util.format;
if (!format) {
// If not node 0.6, then use its `util.format`:
// <https://github.com/joyent/node/blob/master/lib/util.js#L22>:
var inspect = util.inspect;
var formatRegExp = /%[sdj%]/g;
format = 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 indent(s) {
return ' ' + s.split(/\r?\n/).join('\n ')
}
function isArray(ar) {
return ar instanceof Array ||
Array.isArray(ar) ||
(ar && ar !== Object.prototype && isArray(ar.__proto__));
}
function printHelp() {
util.puts("Usage:");
util.puts(" cat some-json-log-file.log | bunyan [OPTIONS]");
util.puts("");
util.puts("Pipe in your JSON log file content (e.g. bunyan logs) for nicer ");
util.puts("output");
util.puts("");
util.puts("Options:");
util.puts(" -h, --help print this help info and exit");
util.puts(" --version print version of this command and exit");
util.puts("");
util.puts(" -o, --output MODE");
util.puts(" Specify an output mode/format. One of");
util.puts(" paul: (the default) pretty")
util.puts(" json: JSON output, 2-space indent");
util.puts(" json-N: JSON output, N-space indent, e.g. 'json-4'");
util.puts(" inspect: node.js `util.inspect` output");
util.puts(" -j shortcut for `-o json`");
util.puts("");
util.puts("See <https://github.com/trentm/json> for more complete docs.");
}
/**
* Parse the command-line options and arguments into an object.
*
* {
* 'args': [...] // arguments
* 'help': true, // true if '-h' option given
* // etc.
* }
*
* @return {Object} The parsed options. `.args` is the argument list.
* @throws {Error} If there is an error parsing argv.
*/
function parseArgv(argv) {
var parsed = {
args: [],
help: false,
quiet: false,
outputMode: OM_PAUL,
jsonIndent: 2
};
// Turn '-iH' into '-i -H', except for argument-accepting options.
var args = argv.slice(2); // drop ['node', 'scriptname']
var newArgs = [];
var optTakesArg = {'d': true, 'o': true};
for (var i = 0; i < args.length; i++) {
if (args[i].charAt(0) === "-" && args[i].charAt(1) !== '-' && args[i].length > 2) {
var splitOpts = args[i].slice(1).split("");
for (var j = 0; j < splitOpts.length; j++) {
newArgs.push('-' + splitOpts[j])
if (optTakesArg[splitOpts[j]]) {
var optArg = splitOpts.slice(j+1).join("");
if (optArg.length) {
newArgs.push(optArg);
}
break;
}
}
} else {
newArgs.push(args[i]);
}
}
args = newArgs;
endOfOptions = false;
while (args.length > 0) {
var arg = args.shift();
switch(arg) {
case "--":
endOfOptions = true;
break;
case "-h": // display help and exit
case "--help":
parsed.help = true;
break;
case "--version":
parsed.version = true;
break;
case "-q":
case "--quiet":
parsed.quiet = true;
break;
case "-o":
case "--output":
var name = args.shift();
var idx = name.lastIndexOf('-');
if (idx !== -1) {
var indent = Number(name.slice(idx+1));
if (! isNaN(indent)) {
parsed.jsonIndent = indent;
name = name.slice(0, idx);
}
}
parsed.outputMode = OM_FROM_NAME[name];
if (parsed.outputMode === undefined) {
throw new Error("unknown output mode: '"+name+"'");
}
break;
case "-j": // output with JSON.stringify
parsed.outputMode = OM_JSON;
break;
default: // arguments
if (!endOfOptions && arg.length > 0 && arg[0] === '-') {
throw new Error("unknown option '"+arg+"'");
}
parsed.args.push(arg);
break;
}
}
//TODO: '--' handling and error on a first arg that looks like an option.
return parsed;
}
function isInteger(s) {
return (s.search(/^-?[0-9]+$/) == 0);
}
/**
* Print out a single result, considering input options.
*/
function handleLogLine(line, opts) {
// Handle non-JSON lines.
var rec;
if (!line) {
return emit(line + '\n');
} else if (line[0] !== '{') {
return emit(line + '\n'); // not JSON
} else {
try {
rec = JSON.parse(line);
} catch(e) {
return emit(line + '\n');
}
}
switch (opts.outputMode) {
case OM_PAUL:
// Single-line msg:
// [time] LEVEL: service[/component] on hostname: msg (extras...)
// Multi-line msg:
// [time] LEVEL: service[/component] on hostname: (extras...)
// msg
// If 'req', show the request.
// If 'res', show the response.
// If 'err' and 'err.stack' then show that.
var serviceStr = rec.service;
if (rec.component) {
serviceStr += '/' + rec.component;
}
var levelStr = (upperNameFromLevel[rec.level]
|| "<unknown-level " + rec.level + ">");
var extras = [];
if (rec.request_id) extras.push(rec.request_id);
if (rec.latency) extras.push(rec.latency + "ms");
extras = (extras.length ? ' (' + extras.join(', ') + ')' : '');
var details = [];
onelineMsg = ' ' + rec.msg;
if (rec.msg.indexOf('\n') !== -1) {
onelineMsg = '';
details.push(indent(rec.msg));
}
if (rec.req) {
var headers = rec.req.headers;
details.push(indent(format("%s %s\n%s", rec.req.method,
rec.req.url,
Object.keys(headers).map(
function (h) { return h + ': ' + headers[h]}).join('\n'))));
}
if (rec.res) {
var s = rec.res._header.trimRight()
+ (rec.res._hasBody ? '\n(body)\n' : '') + rec.res._trailer;
details.push(indent(s.trimRight()));
}
if (rec.err && rec.err.stack) {
details.push(indent(rec.err.stack))
}
details = (details.length ? details.join('\n--\n') + '\n' : '');
emit(format("[%s] %s: %s on %s:%s%s\n%s",
rec.time,
upperNameFromLevel[rec.level] || "<unknown-level " + rec.level + ">",
serviceStr,
rec.hostname || "<no-hostname>",
onelineMsg,
extras,
details));
break;
case OM_INSPECT:
emit(util.inspect(rec, false, Infinity, true) + '\n');
break;
case OM_JSON:
emit(JSON.stringify(rec, null, opts.jsonIndent) + '\n');
break;
case OM_SIMPLE:
// <http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/SimpleLayout.html>
emit(format("%s - %s",
upperNameFromLevel[rec.level] || "???",
rec.msg));
break;
default:
throw new Error("unknown output mode: "+opts.outputMode);
}
}
var stdoutFlushed = true;
function emit(s) {
// TODO:PERF If this is try/catch is too slow (too granular): move up to
// mainline and be sure to only catch this particular error.
try {
stdoutFlushed = process.stdout.write(s);
} catch (e) {
// Handle any exceptions in stdout writing in the "error" event above.
}
}
process.stdout.on("error", function (err) {
if (err.code === "EPIPE") {
// Pass. See <https://github.com/trentm/json/issues/9>.
} else {
warn(err)
drainStdoutAndExit(1);
}
});
/**
* A hacked up version of "process.exit" that will first drain stdout
* before exiting. *WARNING: This doesn't stop event processing.* IOW,
* callers have to be careful that code following this call isn't
* accidentally executed.
*
* In node v0.6 "process.stdout and process.stderr are blocking when they
* refer to regular files or TTY file descriptors." However, this hack might
* still be necessary in a shell pipeline.
*/
function drainStdoutAndExit(code) {
process.stdout.on('drain', function () {
process.exit(code);
});
if (stdoutFlushed) {
process.exit(code);
}
}
//---- mainline
function main(argv) {
var opts;
try {
opts = parseArgv(argv);
} catch (e) {
warn("json: error: %s", e.message)
return drainStdoutAndExit(1);
}
//warn(opts);
if (opts.help) {
printHelp();
return;
}
if (opts.version) {
util.puts("bunyan " + getVersion());
return;
}
var leftover = ""; // Left-over partial line from last chunk.
var stdin = process.openStdin();
stdin.setEncoding('utf8');
stdin.on('data', function (chunk) {
var lines = chunk.split(/\r\n|\n/);
var length = lines.length;
if (length === 1) {
leftover += lines[0];
return;
}
if (length > 1) {
handleLogLine(leftover + lines[0], opts);
}
leftover = lines.pop();
length -= 1;
for (var i=1; i < length; i++) {
handleLogLine(lines[i], opts);
}
});
stdin.on('end', function () {
if (leftover) {
handleLogLine(leftover, opts);
leftover = '';
}
});
}
if (require.main === module) {
// HACK guard for <https://github.com/trentm/json/issues/24>.
// We override the `process.stdout.end` guard that core node.js puts in
// place. The real fix is that `.end()` shouldn't be called on stdout
// in node core. Hopefully node v0.6.9 will fix that. Only guard
// for v0.6.0..v0.6.8.
var nodeVer = process.versions.node.split('.').map(Number);
if ([0,6,0] <= nodeVer && nodeVer <= [0,6,8]) {
var stdout = process.stdout;
stdout.end = stdout.destroy = stdout.destroySoon = function() {
/* pass */
};
}
main(process.argv);
}