UNPKG

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