UNPKG

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