Name: js-handler/node_modules/restify/node_modules/bunyan/bin/bunyan
| 1: | #!/usr/bin/env node |
| 2: | // -*- mode: js -*- |
| 3: | // |
| 4: | // bunyan -- filter and pretty-print JSON logs, like Bunyan logs. |
| 5: | // |
| 6: | // See <https://github.com/trentm/node-bunyan>. |
| 7: | // |
| 8: | |
| 9: | var VERSION = '0.21.1'; |
| 10: | |
| 11: | var util = require('util'); |
| 12: | var pathlib = require('path'); |
| 13: | var vm = require('vm'); |
| 14: | var http = require('http'); |
| 15: | var fs = require('fs'); |
| 16: | var warn = console.warn; |
| 17: | var child_process = require('child_process'), |
| 18: | spawn = child_process.spawn, |
| 19: | exec = child_process.exec, |
| 20: | execFile = child_process.execFile; |
| 21: | var assert = require('assert'); |
| 22: | |
| 23: | var nodeSpawnSupportsStdio = ( |
| 24: | Number(process.version.split('.')[0]) >= 0 || |
| 25: | Number(process.version.split('.')[1]) >= 8); |
| 26: | |
| 27: | |
| 28: | |
| 29: | //---- globals and constants |
| 30: | |
| 31: | // Internal debug logging via `console.warn`. |
| 32: | var _DEBUG = false; |
| 33: | |
| 34: | // Output modes. |
| 35: | var OM_LONG = 1; |
| 36: | var OM_JSON = 2; |
| 37: | var OM_INSPECT = 3; |
| 38: | var OM_SIMPLE = 4; |
| 39: | var OM_SHORT = 5; |
| 40: | var OM_BUNYAN = 6; |
| 41: | var OM_FROM_NAME = { |
| 42: | 'long': OM_LONG, |
| 43: | 'paul': OM_LONG, /* backward compat */ |
| 44: | 'json': OM_JSON, |
| 45: | 'inspect': OM_INSPECT, |
| 46: | 'simple': OM_SIMPLE, |
| 47: | 'short': OM_SHORT, |
| 48: | 'bunyan': OM_BUNYAN |
| 49: | }; |
| 50: | |
| 51: | |
| 52: | // Levels |
| 53: | var TRACE = 10; |
| 54: | var DEBUG = 20; |
| 55: | var INFO = 30; |
| 56: | var WARN = 40; |
| 57: | var ERROR = 50; |
| 58: | var FATAL = 60; |
| 59: | |
| 60: | var levelFromName = { |
| 61: | 'trace': TRACE, |
| 62: | 'debug': DEBUG, |
| 63: | 'info': INFO, |
| 64: | 'warn': WARN, |
| 65: | 'error': ERROR, |
| 66: | 'fatal': FATAL |
| 67: | }; |
| 68: | var nameFromLevel = {}; |
| 69: | var upperNameFromLevel = {}; |
| 70: | var upperPaddedNameFromLevel = {}; |
| 71: | Object.keys(levelFromName).forEach(function (name) { |
| 72: | var lvl = levelFromName[name]; |
| 73: | nameFromLevel[lvl] = name; |
| 74: | upperNameFromLevel[lvl] = name.toUpperCase(); |
| 75: | upperPaddedNameFromLevel[lvl] = ( |
| 76: | name.length === 4 ? ' ' : '') + name.toUpperCase(); |
| 77: | }); |
| 78: | |
| 79: | |
| 80: | // The current raw input line being processed. Used for `uncaughtException`. |
| 81: | var currLine = null; |
| 82: | |
| 83: | // Child dtrace process, if any. Used for signal-handling. |
| 84: | var child = null; |
| 85: | |
| 86: | // Whether ANSI codes are being used. Used for signal-handling. |
| 87: | var usingAnsiCodes = false; |
| 88: | |
| 89: | // Global ref to options used only by 'uncaughtException' handler. |
| 90: | var gOptsForUncaughtException; |
| 91: | |
| 92: | // Pager child process, and output stream to which to write. |
| 93: | var pager = null; |
| 94: | var stdout = process.stdout; |
| 95: | |
| 96: | |
| 97: | |
| 98: | //---- support functions |
| 99: | |
| 100: | function getVersion() { |
| 101: | return VERSION; |
| 102: | } |
| 103: | |
| 104: | |
| 105: | var format = util.format; |
| 106: | if (!format) { |
| 107: | /* BEGIN JSSTYLED */ |
| 108: | // If not node 0.6, then use its `util.format`: |
| 109: | // <https://github.com/joyent/node/blob/master/lib/util.js#L22>: |
| 110: | var inspect = util.inspect; |
| 111: | var formatRegExp = /%[sdj%]/g; |
| 112: | format = function format(f) { |
| 113: | if (typeof f !== 'string') { |
| 114: | var objects = []; |
| 115: | for (var i = 0; i < arguments.length; i++) { |
| 116: | objects.push(inspect(arguments[i])); |
| 117: | } |
| 118: | return objects.join(' '); |
| 119: | } |
| 120: | |
| 121: | var i = 1; |
| 122: | var args = arguments; |
| 123: | var len = args.length; |
| 124: | var str = String(f).replace(formatRegExp, function (x) { |
| 125: | if (i >= len) |
| 126: | return x; |
| 127: | switch (x) { |
| 128: | case '%s': return String(args[i++]); |
| 129: | case '%d': return Number(args[i++]); |
| 130: | case '%j': return JSON.stringify(args[i++]); |
| 131: | case '%%': return '%'; |
| 132: | default: |
| 133: | return x; |
| 134: | } |
| 135: | }); |
| 136: | for (var x = args[i]; i < len; x = args[++i]) { |
| 137: | if (x === null || typeof x !== 'object') { |
| 138: | str += ' ' + x; |
| 139: | } else { |
| 140: | str += ' ' + inspect(x); |
| 141: | } |
| 142: | } |
| 143: | return str; |
| 144: | }; |
| 145: | /* END JSSTYLED */ |
| 146: | } |
| 147: | |
| 148: | function indent(s) { |
| 149: | return ' ' + s.split(/\r?\n/).join('\n '); |
| 150: | } |
| 151: | |
| 152: | function objCopy(obj) { |
| 153: | if (obj === null) { |
| 154: | return null; |
| 155: | } else if (Array.isArray(obj)) { |
| 156: | return obj.slice(); |
| 157: | } else { |
| 158: | var copy = {}; |
| 159: | Object.keys(obj).forEach(function (k) { |
| 160: | copy[k] = obj[k]; |
| 161: | }); |
| 162: | return copy; |
| 163: | } |
| 164: | } |
| 165: | |
| 166: | function printHelp() { |
| 167: | /* BEGIN JSSTYLED */ |
| 168: | var p = console.log; |
| 169: | p('Usage:'); |
| 170: | p(' bunyan [OPTIONS] [FILE ...]'); |
| 171: | p(' ... | bunyan [OPTIONS]'); |
| 172: | p(' bunyan [OPTIONS] -p PID'); |
| 173: | p(''); |
| 174: | p('Filter and pretty-print Bunyan log file content.'); |
| 175: | p(''); |
| 176: | p('General options:'); |
| 177: | p(' -h, --help print this help info and exit'); |
| 178: | p(' --version print version of this command and exit'); |
| 179: | p(''); |
| 180: | p('Dtrace options (only on dtrace-supporting platforms):'); |
| 181: | p(' -p PID Process bunyan:log-* probes from the process'); |
| 182: | p(' with the given PID. Can be used multiple times,'); |
| 183: | p(' or specify all processes with "*", or a set of'); |
| 184: | p(' processes whose command & args match a pattern'); |
| 185: | p(' with "-p NAME".'); |
| 186: | p(''); |
| 187: | p('Filtering options:'); |
| 188: | p(' -l, --level LEVEL'); |
| 189: | p(' Only show messages at or above the specified level.'); |
| 190: | p(' You can specify level *names* or numeric values.'); |
| 191: | p(' (See "Log Levels" below.)'); |
| 192: | p(' -c, --condition CONDITION'); |
| 193: | p(' Run each log message through the condition and'); |
| 194: | p(' only show those that return truish. E.g.:'); |
| 195: | p(' -c \'this.pid == 123\''); |
| 196: | p(' -c \'this.level == DEBUG\''); |
| 197: | p(' -c \'this.msg.indexOf("boom") != -1\''); |
| 198: | p(' "CONDITION" must be legal JS code. `this` holds'); |
| 199: | p(' the log record. The TRACE, DEBUG, ... FATAL values'); |
| 200: | p(' are defined to help with comparing `this.level`.'); |
| 201: | p(' --strict Suppress all but legal Bunyan JSON log lines. By default'); |
| 202: | p(' non-JSON, and non-Bunyan lines are passed through.'); |
| 203: | p(''); |
| 204: | p('Output options:'); |
| 205: | p(' --pager Pipe output into `less` (or $PAGER if set), if'); |
| 206: | p(' stdout is a TTY. This overrides $BUNYAN_NO_PAGER.'); |
| 207: | p(' Note: Paging is only supported on node >=0.8.'); |
| 208: | p(' --no-pager Do not pipe output into a pager.'); |
| 209: | p(' --color Colorize output. Defaults to try if output'); |
| 210: | p(' stream is a TTY.'); |
| 211: | p(' --no-color Force no coloring (e.g. terminal doesn\'t support it)'); |
| 212: | p(' -o, --output MODE'); |
| 213: | p(' Specify an output mode/format. One of'); |
| 214: | p(' long: (the default) pretty'); |
| 215: | p(' json: JSON output, 2-space indent'); |
| 216: | p(' json-N: JSON output, N-space indent, e.g. "json-4"'); |
| 217: | p(' bunyan: 0 indented JSON, bunyan\'s native format'); |
| 218: | p(' inspect: node.js `util.inspect` output'); |
| 219: | p(' short: like "long", but more concise'); |
| 220: | p(' -j shortcut for `-o json`'); |
| 221: | p(''); |
| 222: | p('Log Levels:'); |
| 223: | p(' Either numeric values or their associated strings are valid for the'); |
| 224: | p(' -l|--level argument. However, -c|--condition scripts will see a numeric'); |
| 225: | p(' "level" value, not a string.'); |
| 226: | p(''); |
| 227: | Object.keys(levelFromName).forEach(function (name) { |
| 228: | var n = name; |
| 229: | while (n.length < 6) |
| 230: | n += ' '; |
| 231: | p(' %s %d', n, levelFromName[name]); |
| 232: | }); |
| 233: | p(''); |
| 234: | p('Environment Variables:'); |
| 235: | p(' BUNYAN_NO_COLOR Set to a non-empty value to force no output '); |
| 236: | p(' coloring. See "--no-color".'); |
| 237: | p(' BUNYAN_NO_PAGER Disable piping output to a pager. '); |
| 238: | p(' See "--no-pager".'); |
| 239: | p(''); |
| 240: | p('See <https://github.com/trentm/node-bunyan> for more complete docs.'); |
| 241: | p('Please report bugs to <https://github.com/trentm/node-bunyan/issues>.'); |
| 242: | /* END JSSTYLED */ |
| 243: | } |
| 244: | |
| 245: | /* |
| 246: | * If the user specifies multiple input sources, we want to print out records |
| 247: | * from all sources in a single, chronologically ordered stream. To do this |
| 248: | * efficiently, we first assume that all records within each source are ordered |
| 249: | * already, so we need only keep track of the next record in each source and |
| 250: | * the time of the last record emitted. To avoid excess memory usage, we |
| 251: | * pause() streams that are ahead of others. |
| 252: | * |
| 253: | * 'streams' is an object indexed by source name (file name) which specifies: |
| 254: | * |
| 255: | * stream Actual stream object, so that we can pause and resume it. |
| 256: | * |
| 257: | * records Array of log records we've read, but not yet emitted. Each |
| 258: | * record includes 'line' (the raw line), 'rec' (the JSON |
| 259: | * record), and 'time' (the parsed time value). |
| 260: | * |
| 261: | * done Whether the stream has any more records to emit. |
| 262: | */ |
| 263: | var streams = {}; |
| 264: | |
| 265: | function gotRecord(file, line, rec, opts, stylize) |
| 266: | { |
| 267: | var time = new Date(rec.time); |
| 268: | |
| 269: | streams[file]['records'].push({ line: line, rec: rec, time: time }); |
| 270: | emitNextRecord(opts, stylize); |
| 271: | } |
| 272: | |
| 273: | function filterRecord(rec, opts) |
| 274: | { |
| 275: | if (opts.level && rec.level < opts.level) { |
| 276: | return false; |
| 277: | } |
| 278: | |
| 279: | if (opts.conditions) { |
| 280: | for (var i = 0; i < opts.conditions.length; i++) { |
| 281: | var pass = opts.conditions[i].runInNewContext(rec); |
| 282: | if (!pass) |
| 283: | return false; |
| 284: | } |
| 285: | } |
| 286: | |
| 287: | return true; |
| 288: | } |
| 289: | |
| 290: | function emitNextRecord(opts, stylize) |
| 291: | { |
| 292: | var ofile, ready, minfile, rec; |
| 293: | |
| 294: | for (;;) { |
| 295: | /* |
| 296: | * Take a first pass through the input streams to see if we have a |
| 297: | * record from all of them. If not, we'll pause any streams for |
| 298: | * which we do already have a record (to avoid consuming excess |
| 299: | * memory) and then wait until we have records from the others |
| 300: | * before emitting the next record. |
| 301: | * |
| 302: | * As part of the same pass, we look for the earliest record |
| 303: | * we have not yet emitted. |
| 304: | */ |
| 305: | minfile = undefined; |
| 306: | ready = true; |
| 307: | for (ofile in streams) { |
| 308: | |
| 309: | if (streams[ofile].stream === null || |
| 310: | (!streams[ofile].done && streams[ofile].records.length === 0)) { |
| 311: | ready = false; |
| 312: | break; |
| 313: | } |
| 314: | |
| 315: | if (streams[ofile].records.length > 0 && |
| 316: | (minfile === undefined || |
| 317: | streams[minfile].records[0].time > |
| 318: | streams[ofile].records[0].time)) { |
| 319: | minfile = ofile; |
| 320: | } |
| 321: | } |
| 322: | |
| 323: | if (!ready || minfile === undefined) { |
| 324: | for (ofile in streams) { |
| 325: | if (!streams[ofile].stream || streams[ofile].done) |
| 326: | continue; |
| 327: | |
| 328: | if (streams[ofile].records.length > 0) { |
| 329: | if (!streams[ofile].paused) { |
| 330: | streams[ofile].paused = true; |
| 331: | streams[ofile].stream.pause(); |
| 332: | } |
| 333: | } else if (streams[ofile].paused) { |
| 334: | streams[ofile].paused = false; |
| 335: | streams[ofile].stream.resume(); |
| 336: | } |
| 337: | } |
| 338: | |
| 339: | return; |
| 340: | } |
| 341: | |
| 342: | /* |
| 343: | * Emit the next record for 'minfile', and invoke ourselves again to |
| 344: | * make sure we emit as many records as we can right now. |
| 345: | */ |
| 346: | rec = streams[minfile].records.shift(); |
| 347: | emitRecord(rec.rec, rec.line, opts, stylize); |
| 348: | } |
| 349: | } |
| 350: | |
| 351: | /** |
| 352: | * Parse the command-line options and arguments into an object. |
| 353: | * |
| 354: | * { |
| 355: | * 'args': [...] // arguments |
| 356: | * 'help': true, // true if '-h' option given |
| 357: | * // etc. |
| 358: | * } |
| 359: | * |
| 360: | * @return {Object} The parsed options. `.args` is the argument list. |
| 361: | * @throws {Error} If there is an error parsing argv. |
| 362: | */ |
| 363: | function parseArgv(argv) { |
| 364: | var parsed = { |
| 365: | args: [], |
| 366: | help: false, |
| 367: | color: null, |
| 368: | paginate: null, |
| 369: | outputMode: OM_LONG, |
| 370: | jsonIndent: 2, |
| 371: | level: null, |
| 372: | conditions: null, |
| 373: | strict: false, |
| 374: | pids: null, |
| 375: | pidsType: null |
| 376: | }; |
| 377: | |
| 378: | // Turn '-iH' into '-i -H', except for argument-accepting options. |
| 379: | var args = argv.slice(2); // drop ['node', 'scriptname'] |
| 380: | var newArgs = []; |
| 381: | var optTakesArg = {'d': true, 'o': true, 'c': true, 'l': true, 'p': true}; |
| 382: | for (var i = 0; i < args.length; i++) { |
| 383: | if (args[i].charAt(0) === '-' && args[i].charAt(1) !== '-' && |
| 384: | args[i].length > 2) |
| 385: | { |
| 386: | var splitOpts = args[i].slice(1).split(''); |
| 387: | for (var j = 0; j < splitOpts.length; j++) { |
| 388: | newArgs.push('-' + splitOpts[j]); |
| 389: | if (optTakesArg[splitOpts[j]]) { |
| 390: | var optArg = splitOpts.slice(j+1).join(''); |
| 391: | if (optArg.length) { |
| 392: | newArgs.push(optArg); |
| 393: | } |
| 394: | break; |
| 395: | } |
| 396: | } |
| 397: | } else { |
| 398: | newArgs.push(args[i]); |
| 399: | } |
| 400: | } |
| 401: | args = newArgs; |
| 402: | |
| 403: | var condDefines = []; |
| 404: | Object.keys(upperNameFromLevel).forEach(function (lvl) { |
| 405: | condDefines.push( |
| 406: | format('Object.prototype.%s = %s;', upperNameFromLevel[lvl], lvl)); |
| 407: | }); |
| 408: | condDefines = condDefines.join('\n') + '\n'; |
| 409: | |
| 410: | var endOfOptions = false; |
| 411: | while (args.length > 0) { |
| 412: | var arg = args.shift(); |
| 413: | switch (arg) { |
| 414: | case '--': |
| 415: | endOfOptions = true; |
| 416: | break; |
| 417: | case '-h': // display help and exit |
| 418: | case '--help': |
| 419: | parsed.help = true; |
| 420: | break; |
| 421: | case '--version': |
| 422: | parsed.version = true; |
| 423: | break; |
| 424: | case '--strict': |
| 425: | parsed.strict = true; |
| 426: | break; |
| 427: | case '--color': |
| 428: | parsed.color = true; |
| 429: | break; |
| 430: | case '--no-color': |
| 431: | parsed.color = false; |
| 432: | break; |
| 433: | case '--pager': |
| 434: | parsed.paginate = true; |
| 435: | break; |
| 436: | case '--no-pager': |
| 437: | parsed.paginate = false; |
| 438: | break; |
| 439: | case '-o': |
| 440: | case '--output': |
| 441: | var name = args.shift(); |
| 442: | var idx = name.lastIndexOf('-'); |
| 443: | if (idx !== -1) { |
| 444: | var indentation = Number(name.slice(idx+1)); |
| 445: | if (! isNaN(indentation)) { |
| 446: | parsed.jsonIndent = indentation; |
| 447: | name = name.slice(0, idx); |
| 448: | } |
| 449: | } |
| 450: | parsed.outputMode = OM_FROM_NAME[name]; |
| 451: | if (parsed.outputMode === undefined) { |
| 452: | throw new Error('unknown output mode: "'+name+'"'); |
| 453: | } |
| 454: | break; |
| 455: | case '-j': // output with JSON.stringify |
| 456: | parsed.outputMode = OM_JSON; |
| 457: | break; |
| 458: | case '-p': |
| 459: | if (!parsed.pids) { |
| 460: | parsed.pids = []; |
| 461: | } |
| 462: | var pidArg = args.shift(); |
| 463: | var pid = +(pidArg); |
| 464: | if (!isNaN(pid) || pidArg === '*') { |
| 465: | if (parsed.pidsType && parsed.pidsType !== 'num') { |
| 466: | throw new Error(format('cannot mix PID name and ' |
| 467: | + 'number arguments: "%s"', pidArg)); |
| 468: | } |
| 469: | parsed.pidsType = 'num'; |
| 470: | if (!parsed.pids) { |
| 471: | parsed.pids = []; |
| 472: | } |
| 473: | parsed.pids.push(isNaN(pid) ? pidArg : pid); |
| 474: | } else { |
| 475: | if (parsed.pidsType && parsed.pidsType !== 'name') { |
| 476: | throw new Error(format('cannot mix PID name and ' |
| 477: | + 'number arguments: "%s"', pidArg)); |
| 478: | } |
| 479: | parsed.pidsType = 'name'; |
| 480: | parsed.pids = pidArg; |
| 481: | } |
| 482: | break; |
| 483: | case '-l': |
| 484: | case '--level': |
| 485: | var levelArg = args.shift(); |
| 486: | var level = +(levelArg); |
| 487: | if (isNaN(level)) { |
| 488: | level = +levelFromName[levelArg.toLowerCase()]; |
| 489: | } |
| 490: | if (isNaN(level)) { |
| 491: | throw new Error('unknown level value: "'+levelArg+'"'); |
| 492: | } |
| 493: | parsed.level = level; |
| 494: | break; |
| 495: | case '-c': |
| 496: | case '--condition': |
| 497: | var condition = args.shift(); |
| 498: | parsed.conditions = parsed.conditions || []; |
| 499: | var scriptName = 'bunyan-condition-'+parsed.conditions.length; |
| 500: | var code = condDefines + condition; |
| 501: | try { |
| 502: | var script = vm.createScript(code, scriptName); |
| 503: | } catch (compileErr) { |
| 504: | throw new Error(format('illegal CONDITION code: %s\n' |
| 505: | + ' CONDITION script:\n' |
| 506: | + '%s\n' |
| 507: | + ' Error:\n' |
| 508: | + '%s', |
| 509: | compileErr, indent(code), indent(compileErr.stack))); |
| 510: | } |
| 511: | |
| 512: | // Ensure this is a reasonably safe CONDITION. |
| 513: | try { |
| 514: | script.runInNewContext(minValidRecord); |
| 515: | } catch (condErr) { |
| 516: | throw new Error(format( |
| 517: | /* JSSTYLED */ |
| 518: | 'CONDITION code cannot safely filter a minimal Bunyan log record\n' |
| 519: | + ' CONDITION script:\n' |
| 520: | + '%s\n' |
| 521: | + ' Minimal Bunyan log record:\n' |
| 522: | + '%s\n' |
| 523: | + ' Filter error:\n' |
| 524: | + '%s', |
| 525: | indent(code), |
| 526: | indent(JSON.stringify(minValidRecord, null, 2)), |
| 527: | indent(condErr.stack) |
| 528: | )); |
| 529: | } |
| 530: | |
| 531: | parsed.conditions.push(script); |
| 532: | break; |
| 533: | default: // arguments |
| 534: | if (!endOfOptions && arg.length > 0 && arg[0] === '-') { |
| 535: | throw new Error('unknown option "'+arg+'"'); |
| 536: | } |
| 537: | parsed.args.push(arg); |
| 538: | break; |
| 539: | } |
| 540: | } |
| 541: | //TODO: '--' handling and error on a first arg that looks like an option. |
| 542: | |
| 543: | return parsed; |
| 544: | } |
| 545: | |
| 546: | |
| 547: | function isInteger(s) { |
| 548: | return (s.search(/^-?[0-9]+$/) == 0); |
| 549: | } |
| 550: | |
| 551: | |
| 552: | // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics |
| 553: | // Suggested colors (some are unreadable in common cases): |
| 554: | // - Good: cyan, yellow (limited use), grey, bold, green, magenta, red |
| 555: | // - Bad: blue (not visible on cmd.exe) |
| 556: | var colors = { |
| 557: | 'bold' : [1, 22], |
| 558: | 'italic' : [3, 23], |
| 559: | 'underline' : [4, 24], |
| 560: | 'inverse' : [7, 27], |
| 561: | 'white' : [37, 39], |
| 562: | 'grey' : [90, 39], |
| 563: | 'black' : [30, 39], |
| 564: | 'blue' : [34, 39], |
| 565: | 'cyan' : [36, 39], |
| 566: | 'green' : [32, 39], |
| 567: | 'magenta' : [35, 39], |
| 568: | 'red' : [31, 39], |
| 569: | 'yellow' : [33, 39] |
| 570: | }; |
| 571: | |
| 572: | function stylizeWithColor(str, color) { |
| 573: | if (!str) |
| 574: | return ''; |
| 575: | var codes = colors[color]; |
| 576: | if (codes) { |
| 577: | return '\033[' + codes[0] + 'm' + str + |
| 578: | '\033[' + codes[1] + 'm'; |
| 579: | } else { |
| 580: | return str; |
| 581: | } |
| 582: | } |
| 583: | |
| 584: | function stylizeWithoutColor(str, color) { |
| 585: | return str; |
| 586: | } |
| 587: | |
| 588: | |
| 589: | /** |
| 590: | * Is this a valid Bunyan log record. |
| 591: | */ |
| 592: | function isValidRecord(rec) { |
| 593: | if (rec.v == null || |
| 594: | rec.level == null || |
| 595: | rec.name == null || |
| 596: | rec.hostname == null || |
| 597: | rec.pid == null || |
| 598: | rec.time == null || |
| 599: | rec.msg == null) { |
| 600: | // Not valid Bunyan log. |
| 601: | return false; |
| 602: | } else { |
| 603: | return true; |
| 604: | } |
| 605: | } |
| 606: | var minValidRecord = { |
| 607: | v: 0, //TODO: get this from bunyan.LOG_VERSION |
| 608: | level: INFO, |
| 609: | name: 'name', |
| 610: | hostname: 'hostname', |
| 611: | pid: 123, |
| 612: | time: Date.now(), |
| 613: | msg: 'msg' |
| 614: | }; |
| 615: | |
| 616: | |
| 617: | /** |
| 618: | * Parses the given log line and either emits it right away (for invalid |
| 619: | * records) or enqueues it for emitting later when it's the next line to show. |
| 620: | */ |
| 621: | function handleLogLine(file, line, opts, stylize) { |
| 622: | currLine = line; // intentionally global |
| 623: | |
| 624: | // Emit non-JSON lines immediately. |
| 625: | var rec; |
| 626: | if (!line) { |
| 627: | if (!opts.strict) emit(line + '\n'); |
| 628: | return; |
| 629: | } else if (line[0] !== '{') { |
| 630: | if (!opts.strict) emit(line + '\n'); // not JSON |
| 631: | return; |
| 632: | } else { |
| 633: | try { |
| 634: | rec = JSON.parse(line); |
| 635: | } catch (e) { |
| 636: | if (!opts.strict) emit(line + '\n'); |
| 637: | return; |
| 638: | } |
| 639: | } |
| 640: | |
| 641: | if (!isValidRecord(rec)) { |
| 642: | if (!opts.strict) emit(line + '\n'); |
| 643: | return; |
| 644: | } |
| 645: | |
| 646: | if (!filterRecord(rec, opts)) |
| 647: | return; |
| 648: | |
| 649: | if (file === null) |
| 650: | return emitRecord(rec, line, opts, stylize); |
| 651: | |
| 652: | return gotRecord(file, line, rec, opts, stylize); |
| 653: | } |
| 654: | |
| 655: | /** |
| 656: | * Print out a single result, considering input options. |
| 657: | */ |
| 658: | function emitRecord(rec, line, opts, stylize) { |
| 659: | var short = false; |
| 660: | |
| 661: | switch (opts.outputMode) { |
| 662: | case OM_SHORT: |
| 663: | short = true; |
| 664: | /* jsl:fall-thru */ |
| 665: | |
| 666: | case OM_LONG: |
| 667: | // [time] LEVEL: name[/comp]/pid on hostname (src): msg* (extras...) |
| 668: | // msg* |
| 669: | // -- |
| 670: | // long and multi-line extras |
| 671: | // ... |
| 672: | // If 'msg' is single-line, then it goes in the top line. |
| 673: | // If 'req', show the request. |
| 674: | // If 'res', show the response. |
| 675: | // If 'err' and 'err.stack' then show that. |
| 676: | if (!isValidRecord(rec)) { |
| 677: | return emit(line + '\n'); |
| 678: | } |
| 679: | |
| 680: | delete rec.v; |
| 681: | |
| 682: | /* |
| 683: | * We assume the Date is formatted according to ISO8601, in which |
| 684: | * case we can safely chop off the date information. |
| 685: | */ |
| 686: | if (short && rec.time[10] == 'T') { |
| 687: | var time = rec.time.substr(11); |
| 688: | time = stylize(time, 'XXX'); |
| 689: | } else { |
| 690: | var time = stylize('[' + rec.time + ']', 'XXX'); |
| 691: | } |
| 692: | |
| 693: | delete rec.time; |
| 694: | |
| 695: | var nameStr = rec.name; |
| 696: | delete rec.name; |
| 697: | |
| 698: | if (rec.component) { |
| 699: | nameStr += '/' + rec.component; |
| 700: | } |
| 701: | delete rec.component; |
| 702: | |
| 703: | if (!short) |
| 704: | nameStr += '/' + rec.pid; |
| 705: | delete rec.pid; |
| 706: | |
| 707: | var level = (upperPaddedNameFromLevel[rec.level] || 'LVL' + rec.level); |
| 708: | if (opts.color) { |
| 709: | var colorFromLevel = { |
| 710: | 10: 'grey', // TRACE |
| 711: | 20: 'grey', // DEBUG |
| 712: | 30: 'cyan', // INFO |
| 713: | 40: 'magenta', // WARN |
| 714: | 50: 'red', // ERROR |
| 715: | 60: 'inverse', // FATAL |
| 716: | }; |
| 717: | level = stylize(level, colorFromLevel[rec.level]); |
| 718: | } |
| 719: | delete rec.level; |
| 720: | |
| 721: | var src = ''; |
| 722: | if (rec.src && rec.src.file) { |
| 723: | var s = rec.src; |
| 724: | if (s.func) { |
| 725: | src = format(' (%s:%d in %s)', s.file, s.line, s.func); |
| 726: | } else { |
| 727: | src = format(' (%s:%d)', s.file, s.line); |
| 728: | } |
| 729: | src = stylize(src, 'green'); |
| 730: | } |
| 731: | delete rec.src; |
| 732: | |
| 733: | var hostname = rec.hostname; |
| 734: | delete rec.hostname; |
| 735: | |
| 736: | var extras = []; |
| 737: | var details = []; |
| 738: | |
| 739: | if (rec.req_id) { |
| 740: | extras.push('req_id=' + rec.req_id); |
| 741: | } |
| 742: | delete rec.req_id; |
| 743: | |
| 744: | var onelineMsg; |
| 745: | if (rec.msg.indexOf('\n') !== -1) { |
| 746: | onelineMsg = ''; |
| 747: | details.push(indent(stylize(rec.msg, 'cyan'))); |
| 748: | } else { |
| 749: | onelineMsg = ' ' + stylize(rec.msg, 'cyan'); |
| 750: | } |
| 751: | delete rec.msg; |
| 752: | |
| 753: | if (rec.req && typeof (rec.req) === 'object') { |
| 754: | var req = rec.req; |
| 755: | delete rec.req; |
| 756: | var headers = req.headers; |
| 757: | var s = format('%s %s HTTP/%s%s', req.method, |
| 758: | req.url, |
| 759: | req.httpVersion || '1.1', |
| 760: | (headers ? |
| 761: | '\n' + Object.keys(headers).map(function (h) { |
| 762: | return h + ': ' + headers[h]; |
| 763: | }).join('\n') : |
| 764: | '') |
| 765: | ); |
| 766: | delete req.url; |
| 767: | delete req.method; |
| 768: | delete req.httpVersion; |
| 769: | delete req.headers; |
| 770: | if (req.body) { |
| 771: | s += '\n\n' + (typeof (req.body) === 'object' |
| 772: | ? JSON.stringify(req.body, null, 2) : req.body); |
| 773: | delete req.body; |
| 774: | } |
| 775: | if (req.trailers && Object.keys(req.trailers) > 0) { |
| 776: | s += '\n' + Object.keys(req.trailers).map(function (t) { |
| 777: | return t + ': ' + req.trailers[t]; |
| 778: | }).join('\n'); |
| 779: | } |
| 780: | delete req.trailers; |
| 781: | details.push(indent(s)); |
| 782: | // E.g. for extra 'foo' field on 'req', add 'req.foo' at |
| 783: | // top-level. This *does* have the potential to stomp on a |
| 784: | // literal 'req.foo' key. |
| 785: | Object.keys(req).forEach(function (k) { |
| 786: | rec['req.' + k] = req[k]; |
| 787: | }) |
| 788: | } |
| 789: | |
| 790: | if (rec.client_req && typeof (rec.client_req) === 'object') { |
| 791: | var client_req = rec.client_req; |
| 792: | delete rec.client_req; |
| 793: | var headers = client_req.headers; |
| 794: | var hostHeaderLine = ''; |
| 795: | var s = ''; |
| 796: | if (client_req.address) { |
| 797: | hostHeaderLine = 'Host: ' + client_req.address; |
| 798: | if (client_req.port) |
| 799: | hostHeaderLine += ':' + client_req.port; |
| 800: | hostHeaderLine += '\n'; |
| 801: | } |
| 802: | delete client_req.headers; |
| 803: | delete client_req.address; |
| 804: | delete client_req.port; |
| 805: | s += format('%s %s HTTP/%s\n%s%s', client_req.method, |
| 806: | client_req.url, |
| 807: | client_req.httpVersion || '1.1', |
| 808: | hostHeaderLine, |
| 809: | (headers ? |
| 810: | Object.keys(headers).map( |
| 811: | function (h) { |
| 812: | return h + ': ' + headers[h]; |
| 813: | }).join('\n') : |
| 814: | '')); |
| 815: | delete client_req.method; |
| 816: | delete client_req.url; |
| 817: | delete client_req.httpVersion; |
| 818: | if (client_req.body) { |
| 819: | s += '\n\n' + (typeof (client_req.body) === 'object' ? |
| 820: | JSON.stringify(client_req.body, null, 2) : |
| 821: | client_req.body); |
| 822: | delete client_req.body; |
| 823: | } |
| 824: | // E.g. for extra 'foo' field on 'client_req', add |
| 825: | // 'client_req.foo' at top-level. This *does* have the potential |
| 826: | // to stomp on a literal 'client_req.foo' key. |
| 827: | Object.keys(client_req).forEach(function (k) { |
| 828: | rec['client_req.' + k] = client_req[k]; |
| 829: | }) |
| 830: | details.push(indent(s)); |
| 831: | } |
| 832: | |
| 833: | function _res(res) { |
| 834: | var s = ''; |
| 835: | if (res.header) { |
| 836: | s += res.header.trimRight(); |
| 837: | } else if (res.headers) { |
| 838: | if (res.statusCode) { |
| 839: | s += format('HTTP/1.1 %s %s\n', res.statusCode, |
| 840: | http.STATUS_CODES[res.statusCode]); |
| 841: | } |
| 842: | var headers = res.headers; |
| 843: | s += Object.keys(headers).map( |
| 844: | function (h) { return h + ': ' + headers[h]; }).join('\n'); |
| 845: | } |
| 846: | delete res.header; |
| 847: | delete res.headers; |
| 848: | delete res.statusCode; |
| 849: | if (res.body) { |
| 850: | s += '\n\n' + (typeof (res.body) === 'object' |
| 851: | ? JSON.stringify(res.body, null, 2) : res.body); |
| 852: | delete res.body; |
| 853: | } |
| 854: | if (res.trailer) { |
| 855: | s += '\n' + res.trailer; |
| 856: | } |
| 857: | delete res.trailer; |
| 858: | if (s) { |
| 859: | details.push(indent(s)); |
| 860: | } |
| 861: | // E.g. for extra 'foo' field on 'res', add 'res.foo' at |
| 862: | // top-level. This *does* have the potential to stomp on a |
| 863: | // literal 'res.foo' key. |
| 864: | Object.keys(res).forEach(function (k) { |
| 865: | rec['res.' + k] = res[k]; |
| 866: | }); |
| 867: | } |
| 868: | |
| 869: | if (rec.res && typeof (rec.res) === 'object') { |
| 870: | _res(rec.res); |
| 871: | delete rec.res; |
| 872: | } |
| 873: | if (rec.client_res && typeof (rec.client_res) === 'object') { |
| 874: | _res(rec.client_res); |
| 875: | delete rec.res; |
| 876: | } |
| 877: | |
| 878: | if (rec.err && rec.err.stack) { |
| 879: | details.push(indent(rec.err.stack)); |
| 880: | delete rec.err; |
| 881: | } |
| 882: | |
| 883: | var leftover = Object.keys(rec); |
| 884: | for (var i = 0; i < leftover.length; i++) { |
| 885: | var key = leftover[i]; |
| 886: | var value = rec[key]; |
| 887: | var stringified = false; |
| 888: | if (typeof (value) !== 'string') { |
| 889: | value = JSON.stringify(value, null, 2); |
| 890: | stringified = true; |
| 891: | } |
| 892: | if (value.indexOf('\n') !== -1 || value.length > 50) { |
| 893: | details.push(indent(key + ': ' + value)); |
| 894: | } else if (!stringified && (value.indexOf(' ') != -1 || |
| 895: | value.length === 0)) |
| 896: | { |
| 897: | extras.push(key + '=' + JSON.stringify(value)); |
| 898: | } else { |
| 899: | extras.push(key + '=' + value); |
| 900: | } |
| 901: | } |
| 902: | |
| 903: | extras = stylize( |
| 904: | (extras.length ? ' (' + extras.join(', ') + ')' : ''), 'grey'); |
| 905: | details = stylize( |
| 906: | (details.length ? details.join('\n --\n') + '\n' : ''), 'grey'); |
| 907: | if (!short) |
| 908: | emit(format('%s %s: %s on %s%s:%s%s\n%s', |
| 909: | time, |
| 910: | level, |
| 911: | nameStr, |
| 912: | hostname || '<no-hostname>', |
| 913: | src, |
| 914: | onelineMsg, |
| 915: | extras, |
| 916: | details)); |
| 917: | else |
| 918: | emit(format('%s %s %s:%s%s\n%s', |
| 919: | time, |
| 920: | level, |
| 921: | nameStr, |
| 922: | onelineMsg, |
| 923: | extras, |
| 924: | details)); |
| 925: | break; |
| 926: | |
| 927: | case OM_INSPECT: |
| 928: | emit(util.inspect(rec, false, Infinity, true) + '\n'); |
| 929: | break; |
| 930: | |
| 931: | case OM_BUNYAN: |
| 932: | emit(JSON.stringify(rec, null, 0) + '\n'); |
| 933: | break; |
| 934: | |
| 935: | case OM_JSON: |
| 936: | emit(JSON.stringify(rec, null, opts.jsonIndent) + '\n'); |
| 937: | break; |
| 938: | |
| 939: | case OM_SIMPLE: |
| 940: | /* JSSTYLED */ |
| 941: | // <http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/SimpleLayout.html> |
| 942: | if (!isValidRecord(rec)) { |
| 943: | return emit(line + '\n'); |
| 944: | } |
| 945: | emit(format('%s - %s\n', |
| 946: | upperNameFromLevel[rec.level] || 'LVL' + rec.level, |
| 947: | rec.msg)); |
| 948: | break; |
| 949: | default: |
| 950: | throw new Error('unknown output mode: '+opts.outputMode); |
| 951: | } |
| 952: | } |
| 953: | |
| 954: | |
| 955: | var stdoutFlushed = true; |
| 956: | function emit(s) { |
| 957: | try { |
| 958: | stdoutFlushed = stdout.write(s); |
| 959: | } catch (e) { |
| 960: | // Handle any exceptions in stdout writing in `stdout.on('error', ...)`. |
| 961: | } |
| 962: | } |
| 963: | |
| 964: | |
| 965: | /** |
| 966: | * A hacked up version of 'process.exit' that will first drain stdout |
| 967: | * before exiting. *WARNING: This doesn't stop event processing.* IOW, |
| 968: | * callers have to be careful that code following this call isn't |
| 969: | * accidentally executed. |
| 970: | * |
| 971: | * In node v0.6 "process.stdout and process.stderr are blocking when they |
| 972: | * refer to regular files or TTY file descriptors." However, this hack might |
| 973: | * still be necessary in a shell pipeline. |
| 974: | */ |
| 975: | function drainStdoutAndExit(code) { |
| 976: | if (_DEBUG) warn('(drainStdoutAndExit(%d))', code); |
| 977: | stdout.on('drain', function () { |
| 978: | cleanupAndExit(code); |
| 979: | }); |
| 980: | if (stdoutFlushed) { |
| 981: | cleanupAndExit(code); |
| 982: | } |
| 983: | } |
| 984: | |
| 985: | |
| 986: | /** |
| 987: | * Process all input from stdin. |
| 988: | * |
| 989: | * @params opts {Object} Bunyan options object. |
| 990: | * @param stylize {Function} Output stylize function to use. |
| 991: | * @param callback {Function} `function ()` |
| 992: | */ |
| 993: | function processStdin(opts, stylize, callback) { |
| 994: | var leftover = ''; // Left-over partial line from last chunk. |
| 995: | var stdin = process.stdin; |
| 996: | stdin.resume(); |
| 997: | stdin.setEncoding('utf8'); |
| 998: | stdin.on('data', function (chunk) { |
| 999: | var lines = chunk.split(/\r\n|\n/); |
| 1000: | var length = lines.length; |
| 1001: | if (length === 1) { |
| 1002: | leftover += lines[0]; |
| 1003: | return; |
| 1004: | } |
| 1005: | |
| 1006: | if (length > 1) { |
| 1007: | handleLogLine(null, leftover + lines[0], opts, stylize); |
| 1008: | } |
| 1009: | leftover = lines.pop(); |
| 1010: | length -= 1; |
| 1011: | for (var i = 1; i < length; i++) { |
| 1012: | handleLogLine(null, lines[i], opts, stylize); |
| 1013: | } |
| 1014: | }); |
| 1015: | stdin.on('end', function () { |
| 1016: | if (leftover) { |
| 1017: | handleLogLine(null, leftover, opts, stylize); |
| 1018: | leftover = ''; |
| 1019: | } |
| 1020: | callback(); |
| 1021: | }); |
| 1022: | } |
| 1023: | |
| 1024: | |
| 1025: | /** |
| 1026: | * Process bunyan:log-* probes from the given pid. |
| 1027: | * |
| 1028: | * @params opts {Object} Bunyan options object. |
| 1029: | * @param stylize {Function} Output stylize function to use. |
| 1030: | * @param callback {Function} `function (code)` |
| 1031: | */ |
| 1032: | function processPids(opts, stylize, callback) { |
| 1033: | var leftover = ''; // Left-over partial line from last chunk. |
| 1034: | |
| 1035: | /** |
| 1036: | * Get the PIDs to dtrace. |
| 1037: | * |
| 1038: | * @param cb {Function} `function (errCode, pids)` |
| 1039: | */ |
| 1040: | function getPids(cb) { |
| 1041: | if (opts.pidsType === 'num') { |
| 1042: | return cb(null, opts.pids); |
| 1043: | } |
| 1044: | if (process.platform === 'sunos') { |
| 1045: | execFile('/bin/pgrep', ['-lf', opts.pids], |
| 1046: | function (pidsErr, stdout, stderr) { |
| 1047: | if (pidsErr) { |
| 1048: | warn('bunyan: error getting PIDs for "%s": %s\n%s\n%s', |
| 1049: | opts.pids, pidsErr.message, stdout, stderr); |
| 1050: | return cb(1); |
| 1051: | } |
| 1052: | var pids = stdout.trim().split('\n') |
| 1053: | .map(function (line) { |
| 1054: | return line.trim().split(/\s+/)[0] |
| 1055: | }) |
| 1056: | .filter(function (pid) { |
| 1057: | return Number(pid) !== process.pid |
| 1058: | }); |
| 1059: | if (pids.length === 0) { |
| 1060: | warn('bunyan: error: no matching PIDs found for "%s"', |
| 1061: | opts.pids); |
| 1062: | return cb(2); |
| 1063: | } |
| 1064: | cb(null, pids); |
| 1065: | } |
| 1066: | ); |
| 1067: | } else { |
| 1068: | var regex = opts.pids; |
| 1069: | if (regex && /[a-zA-Z0-9_]/.test(regex[0])) { |
| 1070: | // 'foo' -> '[f]oo' trick to exclude the 'grep' PID from its |
| 1071: | // own search. |
| 1072: | regex = '[' + regex[0] + ']' + regex.slice(1); |
| 1073: | } |
| 1074: | exec(format('ps -A -o pid,command | grep \'%s\'', regex), |
| 1075: | function (pidsErr, stdout, stderr) { |
| 1076: | if (pidsErr) { |
| 1077: | warn('bunyan: error getting PIDs for "%s": %s\n%s\n%s', |
| 1078: | opts.pids, pidsErr.message, stdout, stderr); |
| 1079: | return cb(1); |
| 1080: | } |
| 1081: | var pids = stdout.trim().split('\n') |
| 1082: | .map(function (line) { |
| 1083: | return line.trim().split(/\s+/)[0]; |
| 1084: | }) |
| 1085: | .filter(function (pid) { |
| 1086: | return Number(pid) !== process.pid; |
| 1087: | }); |
| 1088: | if (pids.length === 0) { |
| 1089: | warn('bunyan: error: no matching PIDs found for "%s"', |
| 1090: | opts.pids); |
| 1091: | return cb(2); |
| 1092: | } |
| 1093: | cb(null, pids); |
| 1094: | } |
| 1095: | ); |
| 1096: | } |
| 1097: | } |
| 1098: | |
| 1099: | getPids(function (errCode, pids) { |
| 1100: | if (errCode) { |
| 1101: | return callback(errCode); |
| 1102: | } |
| 1103: | |
| 1104: | var probes = pids.map(function (pid) { |
| 1105: | return format('bunyan%s:::log-*', pid); |
| 1106: | }).join(','); |
| 1107: | var argv = ['dtrace', '-Z', '-x', 'strsize=4k', '-qn', |
| 1108: | format('%s{printf("%s", copyinstr(arg0))}', probes)]; |
| 1109: | //console.log('dtrace argv: %s', argv); |
| 1110: | var dtrace = spawn(argv[0], argv.slice(1), |
| 1111: | // Share the stderr handle to have error output come |
| 1112: | // straight through. Only supported in v0.8+. |
| 1113: | {stdio: ['pipe', 'pipe', process.stderr]}); |
| 1114: | child = dtrace; // intentionally global |
| 1115: | |
| 1116: | function finish(code) { |
| 1117: | if (leftover) { |
| 1118: | handleLogLine(null, leftover, opts, stylize); |
| 1119: | leftover = ''; |
| 1120: | } |
| 1121: | callback(returnCode); |
| 1122: | } |
| 1123: | |
| 1124: | dtrace.stdout.setEncoding('utf8'); |
| 1125: | dtrace.stdout.on('data', function (chunk) { |
| 1126: | var lines = chunk.split(/\r\n|\n/); |
| 1127: | var length = lines.length; |
| 1128: | if (length === 1) { |
| 1129: | leftover += lines[0]; |
| 1130: | return; |
| 1131: | } |
| 1132: | if (length > 1) { |
| 1133: | handleLogLine(null, leftover + lines[0], opts, stylize); |
| 1134: | } |
| 1135: | leftover = lines.pop(); |
| 1136: | length -= 1; |
| 1137: | for (var i = 1; i < length; i++) { |
| 1138: | handleLogLine(null, lines[i], opts, stylize); |
| 1139: | } |
| 1140: | }); |
| 1141: | |
| 1142: | if (nodeSpawnSupportsStdio) { |
| 1143: | dtrace.on('exit', finish); |
| 1144: | } else { |
| 1145: | // Fallback (for < v0.8) to pipe the dtrace process' stderr to |
| 1146: | // this stderr. Wait for all of (1) process 'exit', (2) stderr |
| 1147: | // 'end', and (2) stdout 'end' before returning to ensure all |
| 1148: | // stderr is flushed (issue #54). |
| 1149: | var returnCode = null; |
| 1150: | var eventsRemaining = 3; |
| 1151: | function countdownToFinish(code) { |
| 1152: | returnCode = code; |
| 1153: | eventsRemaining--; |
| 1154: | if (eventsRemaining == 0) { |
| 1155: | finish(returnCode); |
| 1156: | } |
| 1157: | } |
| 1158: | dtrace.stderr.pipe(process.stderr); |
| 1159: | dtrace.stderr.on('end', countdownToFinish); |
| 1160: | dtrace.stderr.on('end', countdownToFinish); |
| 1161: | dtrace.on('exit', countdownToFinish); |
| 1162: | } |
| 1163: | }); |
| 1164: | } |
| 1165: | |
| 1166: | |
| 1167: | /** |
| 1168: | * Process all input from the given log file. |
| 1169: | * |
| 1170: | * @param file {String} Log file path to process. |
| 1171: | * @params opts {Object} Bunyan options object. |
| 1172: | * @param stylize {Function} Output stylize function to use. |
| 1173: | * @param callback {Function} `function ()` |
| 1174: | */ |
| 1175: | function processFile(file, opts, stylize, callback) { |
| 1176: | var stream = fs.createReadStream(file); |
| 1177: | if (/\.gz$/.test(file)) { |
| 1178: | stream = stream.pipe(require('zlib').createGunzip()); |
| 1179: | } |
| 1180: | // Manually decode streams - lazy load here as per node/lib/fs.js |
| 1181: | var decoder = new (require('string_decoder').StringDecoder)('utf8'); |
| 1182: | |
| 1183: | streams[file].stream = stream; |
| 1184: | |
| 1185: | stream.on('error', function (err) { |
| 1186: | streams[file].done = true; |
| 1187: | callback(err); |
| 1188: | }); |
| 1189: | |
| 1190: | var leftover = ''; // Left-over partial line from last chunk. |
| 1191: | stream.on('data', function (data) { |
| 1192: | var chunk = decoder.write(data); |
| 1193: | if (!chunk.length) { |
| 1194: | return; |
| 1195: | } |
| 1196: | var lines = chunk.split(/\r\n|\n/); |
| 1197: | var length = lines.length; |
| 1198: | if (length === 1) { |
| 1199: | leftover += lines[0]; |
| 1200: | return; |
| 1201: | } |
| 1202: | |
| 1203: | if (length > 1) { |
| 1204: | handleLogLine(file, leftover + lines[0], opts, stylize); |
| 1205: | } |
| 1206: | leftover = lines.pop(); |
| 1207: | length -= 1; |
| 1208: | for (var i = 1; i < length; i++) { |
| 1209: | handleLogLine(file, lines[i], opts, stylize); |
| 1210: | } |
| 1211: | }); |
| 1212: | |
| 1213: | stream.on('end', function () { |
| 1214: | streams[file].done = true; |
| 1215: | if (leftover) { |
| 1216: | handleLogLine(file, leftover, opts, stylize); |
| 1217: | leftover = ''; |
| 1218: | } else { |
| 1219: | emitNextRecord(opts, stylize); |
| 1220: | } |
| 1221: | callback(); |
| 1222: | }); |
| 1223: | } |
| 1224: | |
| 1225: | |
| 1226: | /** |
| 1227: | * From node async module. |
| 1228: | */ |
| 1229: | /* BEGIN JSSTYLED */ |
| 1230: | function asyncForEach(arr, iterator, callback) { |
| 1231: | callback = callback || function () {}; |
| 1232: | if (!arr.length) { |
| 1233: | return callback(); |
| 1234: | } |
| 1235: | var completed = 0; |
| 1236: | arr.forEach(function (x) { |
| 1237: | iterator(x, function (err) { |
| 1238: | if (err) { |
| 1239: | callback(err); |
| 1240: | callback = function () {}; |
| 1241: | } |
| 1242: | else { |
| 1243: | completed += 1; |
| 1244: | if (completed === arr.length) { |
| 1245: | callback(); |
| 1246: | } |
| 1247: | } |
| 1248: | }); |
| 1249: | }); |
| 1250: | }; |
| 1251: | /* END JSSTYLED */ |
| 1252: | |
| 1253: | |
| 1254: | |
| 1255: | /** |
| 1256: | * Cleanup and exit properly. |
| 1257: | * |
| 1258: | * Warning: this doesn't stop processing, i.e. process exit might be delayed. |
| 1259: | * It is up to the caller to ensure that no subsequent bunyan processing |
| 1260: | * is done after calling this. |
| 1261: | * |
| 1262: | * @param code {Number} exit code. |
| 1263: | * @param signal {String} Optional signal name, if this was exitting because |
| 1264: | * of a signal. |
| 1265: | */ |
| 1266: | var cleanedUp = false; |
| 1267: | function cleanupAndExit(code, signal) { |
| 1268: | // Guard one call. |
| 1269: | if (cleanedUp) { |
| 1270: | return; |
| 1271: | } |
| 1272: | cleanedUp = true; |
| 1273: | if (_DEBUG) warn('(bunyan: cleanupAndExit)'); |
| 1274: | |
| 1275: | // Clear possibly interrupted ANSI code (issue #59). |
| 1276: | if (usingAnsiCodes) { |
| 1277: | stdout.write('\033[0m'); |
| 1278: | } |
| 1279: | |
| 1280: | // Kill possible dtrace child. |
| 1281: | if (child) { |
| 1282: | child.kill(signal); |
| 1283: | } |
| 1284: | |
| 1285: | if (pager) { |
| 1286: | // Let pager know that output is done, then wait for pager to exit. |
| 1287: | stdout.end(); |
| 1288: | pager.on('exit', function (pagerCode) { |
| 1289: | if (_DEBUG) |
| 1290: | warn('(bunyan: pager exit -> process.exit(%s))', |
| 1291: | pagerCode || code); |
| 1292: | process.exit(pagerCode || code); |
| 1293: | }); |
| 1294: | } else { |
| 1295: | if (_DEBUG) warn('(bunyan: process.exit(%s))', code); |
| 1296: | process.exit(code); |
| 1297: | } |
| 1298: | } |
| 1299: | |
| 1300: | |
| 1301: | |
| 1302: | //---- mainline |
| 1303: | |
| 1304: | process.on('SIGINT', function () { cleanupAndExit(1, 'SIGINT'); }); |
| 1305: | process.on('SIGQUIT', function () { cleanupAndExit(1, 'SIGQUIT'); }); |
| 1306: | process.on('SIGTERM', function () { cleanupAndExit(1, 'SIGTERM'); }); |
| 1307: | process.on('SIGHUP', function () { cleanupAndExit(1, 'SIGHUP'); }); |
| 1308: | |
| 1309: | process.on('uncaughtException', function (err) { |
| 1310: | function _indent(s) { |
| 1311: | var lines = s.split(/\r?\n/); |
| 1312: | for (var i = 0; i < lines.length; i++) { |
| 1313: | lines[i] = '* ' + lines[i]; |
| 1314: | } |
| 1315: | return lines.join('\n'); |
| 1316: | } |
| 1317: | |
| 1318: | var title = encodeURIComponent(format( |
| 1319: | 'Bunyan %s crashed: %s', getVersion(), String(err))); |
| 1320: | var e = console.error; |
| 1321: | e('* * *'); |
| 1322: | e('* The Bunyan CLI crashed!'); |
| 1323: | e('*'); |
| 1324: | if (err.name === 'ReferenceError' && gOptsForUncaughtException.conditions) { |
| 1325: | e('* A "ReferenceError" is often the result of given'); |
| 1326: | e('* `-c CONDITION` code that doesn\'t guard against undefined'); |
| 1327: | e('* values. If that is not the problem:'); |
| 1328: | e('*'); |
| 1329: | } |
| 1330: | e('* Please report this issue and include the details below:'); |
| 1331: | e('*'); |
| 1332: | e('* https://github.com/trentm/node-bunyan/issues/new?title=%s', title); |
| 1333: | e('*'); |
| 1334: | e('* * *'); |
| 1335: | e('* node version:', process.version); |
| 1336: | e('* bunyan version:', getVersion()); |
| 1337: | e('* argv: %j', process.argv); |
| 1338: | e('* log line: %j', currLine); |
| 1339: | e('* stack:'); |
| 1340: | e(_indent(err.stack)); |
| 1341: | e('* * *'); |
| 1342: | process.exit(1); |
| 1343: | }); |
| 1344: | |
| 1345: | |
| 1346: | function main(argv) { |
| 1347: | try { |
| 1348: | var opts = parseArgv(argv); |
| 1349: | } catch (e) { |
| 1350: | warn('bunyan: error: %s', e.message); |
| 1351: | return drainStdoutAndExit(1); |
| 1352: | } |
| 1353: | gOptsForUncaughtException = opts; // intentionally global |
| 1354: | if (opts.help) { |
| 1355: | printHelp(); |
| 1356: | return; |
| 1357: | } |
| 1358: | if (opts.version) { |
| 1359: | console.log('bunyan ' + getVersion()); |
| 1360: | return; |
| 1361: | } |
| 1362: | if (opts.pid && opts.args.length > 0) { |
| 1363: | warn('bunyan: error: can\'t use both "-p PID" (%s) and file (%s) args', |
| 1364: | opts.pid, opts.args.join(' ')); |
| 1365: | return drainStdoutAndExit(1); |
| 1366: | } |
| 1367: | if (opts.color === null) { |
| 1368: | if (process.env.BUNYAN_NO_COLOR && |
| 1369: | process.env.BUNYAN_NO_COLOR.length > 0) { |
| 1370: | opts.color = false; |
| 1371: | } else { |
| 1372: | opts.color = process.stdout.isTTY; |
| 1373: | } |
| 1374: | } |
| 1375: | usingAnsiCodes = opts.color; // intentionally global |
| 1376: | var stylize = (opts.color ? stylizeWithColor : stylizeWithoutColor); |
| 1377: | |
| 1378: | // Pager. |
| 1379: | var nodeVer = process.versions.node.split('.').map(Number); |
| 1380: | var paginate = ( |
| 1381: | process.stdout.isTTY && |
| 1382: | process.stdin.isTTY && |
| 1383: | !opts.pids && // Don't page if following process output. |
| 1384: | opts.args.length > 0 && // Don't page if no file args to process. |
| 1385: | process.platform !== 'win32' && |
| 1386: | nodeVer >= [0, 8, 0] && |
| 1387: | (opts.paginate === true || |
| 1388: | (opts.paginate !== false && |
| 1389: | (!process.env.BUNYAN_NO_PAGER || |
| 1390: | process.env.BUNYAN_NO_PAGER.length === 0)))); |
| 1391: | if (paginate) { |
| 1392: | var pagerCmd = process.env.PAGER || 'less'; |
| 1393: | /* JSSTYLED */ |
| 1394: | assert.ok(pagerCmd.indexOf('"') === -1 && pagerCmd.indexOf("'") === -1, |
| 1395: | 'cannot parse PAGER quotes yet'); |
| 1396: | var argv = pagerCmd.split(/\s+/g); |
| 1397: | var env = objCopy(process.env); |
| 1398: | if (env.LESS === undefined) { |
| 1399: | // git's default is LESS=FRSX. I don't like the 'S' here because |
| 1400: | // lines are *typically* wide with bunyan output and scrolling |
| 1401: | // horizontally is a royal pain. Note a bug in Mac's `less -F`, |
| 1402: | // such that SIGWINCH can kill it. If that rears too much then |
| 1403: | // I'll remove 'F' from here. |
| 1404: | env.LESS = 'FRX'; |
| 1405: | } |
| 1406: | if (_DEBUG) warn('(pager: argv=%j, env.LESS=%j)', argv, env.LESS); |
| 1407: | // `pager` and `stdout` intentionally global. |
| 1408: | pager = spawn(argv[0], argv.slice(1), |
| 1409: | // Share the stderr handle to have error output come |
| 1410: | // straight through. Only supported in v0.8+. |
| 1411: | {env: env, stdio: ['pipe', 1, 2]}); |
| 1412: | stdout = pager.stdin; |
| 1413: | |
| 1414: | // Early termination of the pager: just stop. |
| 1415: | pager.on('exit', function (pagerCode) { |
| 1416: | if (_DEBUG) warn('(bunyan: pager exit)'); |
| 1417: | pager = null; |
| 1418: | stdout.end() |
| 1419: | stdout = process.stdout; |
| 1420: | cleanupAndExit(pagerCode); |
| 1421: | }); |
| 1422: | } |
| 1423: | |
| 1424: | // Stdout error handling. (Couldn't setup until `stdout` was determined.) |
| 1425: | stdout.on('error', function (err) { |
| 1426: | if (_DEBUG) warn('(stdout error event: %s)', err); |
| 1427: | if (err.code === 'EPIPE') { |
| 1428: | drainStdoutAndExit(0); |
| 1429: | } else if (err.toString() === 'Error: This socket is closed.') { |
| 1430: | // Could get this if the pager closes its stdin, but hasn't |
| 1431: | // exited yet. |
| 1432: | drainStdoutAndExit(1); |
| 1433: | } else { |
| 1434: | warn(err); |
| 1435: | drainStdoutAndExit(1); |
| 1436: | } |
| 1437: | }); |
| 1438: | |
| 1439: | var retval = 0; |
| 1440: | if (opts.pids) { |
| 1441: | processPids(opts, stylize, function (code) { |
| 1442: | cleanupAndExit(code); |
| 1443: | }); |
| 1444: | } else if (opts.args.length > 0) { |
| 1445: | var files = opts.args; |
| 1446: | files.forEach(function (file) { |
| 1447: | streams[file] = { stream: null, records: [], done: false } |
| 1448: | }); |
| 1449: | asyncForEach(files, |
| 1450: | function (file, next) { |
| 1451: | processFile(file, opts, stylize, function (err) { |
| 1452: | if (err) { |
| 1453: | warn('bunyan: %s', err.message); |
| 1454: | retval += 1; |
| 1455: | } |
| 1456: | next(); |
| 1457: | }); |
| 1458: | }, |
| 1459: | function (err) { |
| 1460: | if (err) { |
| 1461: | warn('bunyan: unexpected error: %s', err.stack || err); |
| 1462: | return drainStdoutAndExit(1); |
| 1463: | } |
| 1464: | cleanupAndExit(retval); |
| 1465: | } |
| 1466: | ); |
| 1467: | } else { |
| 1468: | processStdin(opts, stylize, function () { |
| 1469: | cleanupAndExit(retval); |
| 1470: | }); |
| 1471: | } |
| 1472: | } |
| 1473: | |
| 1474: | if (require.main === module) { |
| 1475: | // HACK guard for <https://github.com/trentm/json/issues/24>. |
| 1476: | // We override the `process.stdout.end` guard that core node.js puts in |
| 1477: | // place. The real fix is that `.end()` shouldn't be called on stdout |
| 1478: | // in node core. Node v0.6.9 fixes that. Only guard for v0.6.0..v0.6.8. |
| 1479: | var nodeVer = process.versions.node.split('.').map(Number); |
| 1480: | if ([0, 6, 0] <= nodeVer && nodeVer <= [0, 6, 8]) { |
| 1481: | var stdout = process.stdout; |
| 1482: | stdout.end = stdout.destroy = stdout.destroySoon = function () { |
| 1483: | /* pass */ |
| 1484: | }; |
| 1485: | } |
| 1486: | |
| 1487: | main(process.argv); |
| 1488: | } |
