bunyan CLI auto-paging
This commit is contained in:
parent
70ae34a35d
commit
b1c143c87a
7 changed files with 218 additions and 41 deletions
17
CHANGES.md
17
CHANGES.md
|
@ -6,7 +6,22 @@ Known issues:
|
||||||
bug](https://github.com/TooTallNate/node-gyp/issues/65).
|
bug](https://github.com/TooTallNate/node-gyp/issues/65).
|
||||||
|
|
||||||
|
|
||||||
## bunyan 0.17.1 (not yet released)
|
## bunyan 0.18.0 (not yet released)
|
||||||
|
|
||||||
|
- Automatic paging support in the `bunyan` CLI (similar to `git log` et al).
|
||||||
|
IOW, `bunyan` will open your pager (by default `less`) and pipe rendered
|
||||||
|
log output through it. A main benefit of this is getting colored logs with
|
||||||
|
a pager without the pain. Before you had to explicit use `--color` to tell
|
||||||
|
bunyan to color output when the output was not a TTY:
|
||||||
|
|
||||||
|
bunyan foo.log --color | less -R # before
|
||||||
|
bunyan foo.log # now
|
||||||
|
|
||||||
|
Disable with the `--no-pager` option or the `BUNYAN_NO_PAGER=1` environment
|
||||||
|
variable.
|
||||||
|
|
||||||
|
Limitations: Only supported for node >=0.8. Windows is not supported (at
|
||||||
|
least not yet).
|
||||||
|
|
||||||
- Switch test suite to nodeunit (still using a node-tap'ish API via
|
- Switch test suite to nodeunit (still using a node-tap'ish API via
|
||||||
a helper).
|
a helper).
|
||||||
|
|
8
TODO.md
8
TODO.md
|
@ -1,12 +1,16 @@
|
||||||
- man page for the bunyan CLI (refer to it in the readme)
|
- man page for the bunyan CLI (refer to it in the readme)
|
||||||
- full-on docs. The readme is too clunky now.
|
- coloring bug: in less the indented extra info lines only have the first
|
||||||
- paging
|
line colored. Do we need the ANSI char on *each* line? That'll be
|
||||||
|
slower.
|
||||||
- `tail -f`-like support
|
- `tail -f`-like support
|
||||||
- 1.0 with `v: 1` in log records. Fwd/bwd compat in `bunyan` CLI
|
- 1.0 with `v: 1` in log records. Fwd/bwd compat in `bunyan` CLI
|
||||||
|
|
||||||
|
|
||||||
# someday/maybe
|
# someday/maybe
|
||||||
|
|
||||||
|
- full-on docs
|
||||||
|
- better examples/
|
||||||
|
- better coloring
|
||||||
- "template" support for 'rotating-file' stream to get dated rolled files
|
- "template" support for 'rotating-file' stream to get dated rolled files
|
||||||
- "all" or "off" levels? log4j? logging.py?
|
- "all" or "off" levels? log4j? logging.py?
|
||||||
logging.py has NOTSET === 0. I think that is only needed/used for
|
logging.py has NOTSET === 0. I think that is only needed/used for
|
||||||
|
|
168
bin/bunyan
168
bin/bunyan
|
@ -5,7 +5,7 @@
|
||||||
// See <https://github.com/trentm/node-bunyan>.
|
// See <https://github.com/trentm/node-bunyan>.
|
||||||
//
|
//
|
||||||
|
|
||||||
var VERSION = "0.17.1";
|
var VERSION = "0.18.0";
|
||||||
|
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
var pathlib = require('path');
|
var pathlib = require('path');
|
||||||
|
@ -17,6 +17,7 @@ var child_process = require('child_process'),
|
||||||
spawn = child_process.spawn,
|
spawn = child_process.spawn,
|
||||||
exec = child_process.exec,
|
exec = child_process.exec,
|
||||||
execFile = child_process.execFile;
|
execFile = child_process.execFile;
|
||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
var nodeSpawnSupportsStdio = (
|
var nodeSpawnSupportsStdio = (
|
||||||
Number(process.version.split('.')[0]) >= 0
|
Number(process.version.split('.')[0]) >= 0
|
||||||
|
@ -26,6 +27,9 @@ var nodeSpawnSupportsStdio = (
|
||||||
|
|
||||||
//---- globals and constants
|
//---- globals and constants
|
||||||
|
|
||||||
|
// Internal debug logging via `console.warn`.
|
||||||
|
var _DEBUG = false;
|
||||||
|
|
||||||
// Output modes.
|
// Output modes.
|
||||||
var OM_PAUL = 1;
|
var OM_PAUL = 1;
|
||||||
var OM_JSON = 2;
|
var OM_JSON = 2;
|
||||||
|
@ -81,6 +85,10 @@ var usingAnsiCodes = false;
|
||||||
// Global ref to options used only by 'uncaughtException' handler.
|
// Global ref to options used only by 'uncaughtException' handler.
|
||||||
var gOptsForUncaughtException;
|
var gOptsForUncaughtException;
|
||||||
|
|
||||||
|
// Pager child process, and output stream to which to write.
|
||||||
|
var pager = null;
|
||||||
|
var stdout = process.stdout;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//---- support functions
|
//---- support functions
|
||||||
|
@ -185,6 +193,10 @@ function printHelp() {
|
||||||
console.log(" non-JSON, and non-Bunyan lines are passed through.");
|
console.log(" non-JSON, and non-Bunyan lines are passed through.");
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Output options:");
|
console.log("Output options:");
|
||||||
|
console.log(" --pager Pipe output into `less` (or $PAGER if set), if");
|
||||||
|
console.log(" stdout is a terminal. This overrides $BUNYAN_NO_PAGER.");
|
||||||
|
console.log(" Note: Paging is only supported on node >=0.8.");
|
||||||
|
console.log(" --no-pager Do not pipe output into a pager.");
|
||||||
console.log(" --color Colorize output. Defaults to try if output");
|
console.log(" --color Colorize output. Defaults to try if output");
|
||||||
console.log(" stream is a TTY.");
|
console.log(" stream is a TTY.");
|
||||||
console.log(" --no-color Force no coloring (e.g. terminal doesn't support it)");
|
console.log(" --no-color Force no coloring (e.g. terminal doesn't support it)");
|
||||||
|
@ -212,6 +224,8 @@ function printHelp() {
|
||||||
console.log("Environment Variables:");
|
console.log("Environment Variables:");
|
||||||
console.log(" BUNYAN_NO_COLOR Set to a non-empty value to force no output ");
|
console.log(" BUNYAN_NO_COLOR Set to a non-empty value to force no output ");
|
||||||
console.log(" coloring. See '--no-color'.");
|
console.log(" coloring. See '--no-color'.");
|
||||||
|
console.log(" BUNYAN_NO_PAGER Disable piping output to a pager. ");
|
||||||
|
console.log(" See '--no-pager'.");
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("See <https://github.com/trentm/node-bunyan> for more complete docs.");
|
console.log("See <https://github.com/trentm/node-bunyan> for more complete docs.");
|
||||||
console.log("Please report bugs to <https://github.com/trentm/node-bunyan/issues>.");
|
console.log("Please report bugs to <https://github.com/trentm/node-bunyan/issues>.");
|
||||||
|
@ -337,6 +351,7 @@ function parseArgv(argv) {
|
||||||
args: [],
|
args: [],
|
||||||
help: false,
|
help: false,
|
||||||
color: null,
|
color: null,
|
||||||
|
paginate: null,
|
||||||
outputMode: OM_PAUL,
|
outputMode: OM_PAUL,
|
||||||
jsonIndent: 2,
|
jsonIndent: 2,
|
||||||
level: null,
|
level: null,
|
||||||
|
@ -399,6 +414,12 @@ function parseArgv(argv) {
|
||||||
case "--no-color":
|
case "--no-color":
|
||||||
parsed.color = false;
|
parsed.color = false;
|
||||||
break;
|
break;
|
||||||
|
case "--pager":
|
||||||
|
parsed.paginate = true;
|
||||||
|
break;
|
||||||
|
case "--no-pager":
|
||||||
|
parsed.paginate = false;
|
||||||
|
break;
|
||||||
case "-o":
|
case "-o":
|
||||||
case "--output":
|
case "--output":
|
||||||
var name = args.shift();
|
var name = args.shift();
|
||||||
|
@ -893,25 +914,13 @@ function emitRecord(rec, line, opts, stylize) {
|
||||||
|
|
||||||
var stdoutFlushed = true;
|
var stdoutFlushed = true;
|
||||||
function emit(s) {
|
function emit(s) {
|
||||||
// TODO:PERF If this is try/catch is too slow (too granular): move up to
|
|
||||||
// mainline and be sure to only catch this particular error.
|
|
||||||
try {
|
try {
|
||||||
stdoutFlushed = process.stdout.write(s);
|
stdoutFlushed = stdout.write(s);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Handle any exceptions in stdout writing in the "error" event above.
|
// Handle any exceptions in stdout writing in `stdout.on("error", ...)`.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.on("error", function (err) {
|
|
||||||
if (err.code === "EPIPE") {
|
|
||||||
// Pass. See <https://github.com/trentm/json/issues/9>.
|
|
||||||
drainStdoutAndExit(0);
|
|
||||||
} else {
|
|
||||||
warn(err);
|
|
||||||
drainStdoutAndExit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hacked up version of "process.exit" that will first drain stdout
|
* A hacked up version of "process.exit" that will first drain stdout
|
||||||
|
@ -924,11 +933,12 @@ process.stdout.on("error", function (err) {
|
||||||
* still be necessary in a shell pipeline.
|
* still be necessary in a shell pipeline.
|
||||||
*/
|
*/
|
||||||
function drainStdoutAndExit(code) {
|
function drainStdoutAndExit(code) {
|
||||||
process.stdout.on('drain', function () {
|
if (_DEBUG) warn('(drainStdoutAndExit(%d))', code);
|
||||||
process.exit(code);
|
stdout.on('drain', function () {
|
||||||
|
cleanupAndExit(code);
|
||||||
});
|
});
|
||||||
if (stdoutFlushed) {
|
if (stdoutFlushed) {
|
||||||
process.exit(code);
|
cleanupAndExit(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1189,24 +1199,58 @@ function asyncForEach(arr, iterator, callback) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//---- mainline
|
/**
|
||||||
|
* Cleanup and exit properly.
|
||||||
// Try to ensure we close a child 'dtrace' process on signalled exit.
|
*
|
||||||
function signalCleanupAndExit(signal) {
|
* Warning: this doesn't stop processing, i.e. process exit might be delayed.
|
||||||
if (usingAnsiCodes) {
|
* It is up to the caller to ensure that no subsequent bunyan processing
|
||||||
// Clear possibly interrupted ANSI code (issue #59).
|
* is done after calling this.
|
||||||
process.stdout.write('\033[0m');
|
*
|
||||||
|
* @param code {Number} exit code.
|
||||||
|
* @param signal {String} Optional signal name, if this was exitting because
|
||||||
|
* of a signal.
|
||||||
|
*/
|
||||||
|
var cleanedUp = false;
|
||||||
|
function cleanupAndExit(code, signal) {
|
||||||
|
// Guard one call.
|
||||||
|
if (cleanedUp) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
cleanedUp = true;
|
||||||
|
if (_DEBUG) warn('(bunyan: cleanupAndExit)');
|
||||||
|
|
||||||
|
// Clear possibly interrupted ANSI code (issue #59).
|
||||||
|
if (usingAnsiCodes) {
|
||||||
|
stdout.write('\033[0m');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill possible dtrace child.
|
||||||
if (child) {
|
if (child) {
|
||||||
child.kill(signal);
|
child.kill(signal);
|
||||||
}
|
}
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
process.on('SIGINT', function () { signalCleanupAndExit('SIGINT'); });
|
|
||||||
process.on('SIGQUIT', function () { signalCleanupAndExit('SIGQUIT'); });
|
|
||||||
process.on('SIGTERM', function () { signalCleanupAndExit('SIGTERM'); });
|
|
||||||
process.on('SIGHUP', function () { signalCleanupAndExit('SIGHUP'); });
|
|
||||||
|
|
||||||
|
if (pager) {
|
||||||
|
// Let pager know that output is done, then wait for pager to exit.
|
||||||
|
stdout.end();
|
||||||
|
pager.on('exit', function (pagerCode) {
|
||||||
|
if (_DEBUG)
|
||||||
|
warn('(bunyan: pager exit -> process.exit(%s))', pagerCode || code);
|
||||||
|
process.exit(pagerCode || code);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (_DEBUG) warn('(bunyan: process.exit(%s))', code);
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//---- mainline
|
||||||
|
|
||||||
|
process.on('SIGINT', function () { cleanupAndExit(1, 'SIGINT'); });
|
||||||
|
process.on('SIGQUIT', function () { cleanupAndExit(1, 'SIGQUIT'); });
|
||||||
|
process.on('SIGTERM', function () { cleanupAndExit(1, 'SIGTERM'); });
|
||||||
|
process.on('SIGHUP', function () { cleanupAndExit(1, 'SIGHUP'); });
|
||||||
|
|
||||||
process.on('uncaughtException', function (err) {
|
process.on('uncaughtException', function (err) {
|
||||||
function _indent(s) {
|
function _indent(s) {
|
||||||
|
@ -1243,6 +1287,7 @@ process.on('uncaughtException', function (err) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function main(argv) {
|
function main(argv) {
|
||||||
try {
|
try {
|
||||||
var opts = parseArgv(argv);
|
var opts = parseArgv(argv);
|
||||||
|
@ -1275,10 +1320,65 @@ function main(argv) {
|
||||||
usingAnsiCodes = opts.color; // intentionally global
|
usingAnsiCodes = opts.color; // intentionally global
|
||||||
var stylize = (opts.color ? stylizeWithColor : stylizeWithoutColor);
|
var stylize = (opts.color ? stylizeWithColor : stylizeWithoutColor);
|
||||||
|
|
||||||
|
// Pager.
|
||||||
|
var nodeVer = process.versions.node.split('.').map(Number);
|
||||||
|
var paginate = (process.stdout.isTTY
|
||||||
|
&& process.platform !== 'win32'
|
||||||
|
&& nodeVer >= [0,8,0]
|
||||||
|
&& (opts.paginate === true
|
||||||
|
|| (opts.paginate !== false
|
||||||
|
&& (!process.env.BUNYAN_NO_PAGER
|
||||||
|
|| process.env.BUNYAN_NO_PAGER.length === 0))));
|
||||||
|
if (paginate) {
|
||||||
|
var pagerCmd = process.env.PAGER || 'less';
|
||||||
|
assert.ok(pagerCmd.indexOf('"') === -1 && pagerCmd.indexOf("'") === -1,
|
||||||
|
'cannot parse PAGER quotes yet');
|
||||||
|
var argv = pagerCmd.split(/\s+/g);
|
||||||
|
var env = objCopy(process.env);
|
||||||
|
if (env.LESS === undefined) {
|
||||||
|
// git's default is LESS=FRSX. I don't like the 'S' here because lines
|
||||||
|
// are *typically* wide with bunyan output and scrolling horizontally
|
||||||
|
// is a royal pain.
|
||||||
|
// Note a bug in Mac's `less -F`, such that SIGWINCH can kill it.
|
||||||
|
// If that rears too much then I'll remove 'F' from here.
|
||||||
|
env.LESS = 'FRX';
|
||||||
|
}
|
||||||
|
if (_DEBUG) warn("(pager: argv=%j, env.LESS=%j)", argv, env.LESS);
|
||||||
|
// `pager` and `stdout` intentionally global.
|
||||||
|
pager = spawn(argv[0], argv.slice(1),
|
||||||
|
// Share the stderr handle to have error output come
|
||||||
|
// straight through. Only supported in v0.8+.
|
||||||
|
{env: env, stdio: ['pipe',1,2]});
|
||||||
|
stdout = pager.stdin;
|
||||||
|
|
||||||
|
// Early termination of the pager: just stop.
|
||||||
|
pager.on('exit', function (pagerCode) {
|
||||||
|
if (_DEBUG) warn('(bunyan: pager exit)');
|
||||||
|
pager = null;
|
||||||
|
stdout.end()
|
||||||
|
stdout = process.stdout;
|
||||||
|
cleanupAndExit(pagerCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stdout error handling. (Couldn't setup until `stdout` was determined.)
|
||||||
|
stdout.on("error", function (err) {
|
||||||
|
if (_DEBUG) warn('(stdout error event: %s)', err);
|
||||||
|
if (err.code === "EPIPE") {
|
||||||
|
drainStdoutAndExit(0);
|
||||||
|
} else if (err.toString() === 'Error: This socket is closed.') {
|
||||||
|
// Could get this if the pager closes its stdin, but hasn't exited yet.
|
||||||
|
drainStdoutAndExit(1);
|
||||||
|
} else {
|
||||||
|
warn(err);
|
||||||
|
drainStdoutAndExit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var retval = 0;
|
var retval = 0;
|
||||||
if (opts.pids) {
|
if (opts.pids) {
|
||||||
processPids(opts, stylize, function (code) {
|
processPids(opts, stylize, function (code) {
|
||||||
process.exit(code);
|
cleanupAndExit(code);
|
||||||
});
|
});
|
||||||
} else if (opts.args.length > 0) {
|
} else if (opts.args.length > 0) {
|
||||||
var files = opts.args;
|
var files = opts.args;
|
||||||
|
@ -1300,12 +1400,12 @@ function main(argv) {
|
||||||
warn("bunyan: unexpected error: %s", err.stack || err);
|
warn("bunyan: unexpected error: %s", err.stack || err);
|
||||||
return drainStdoutAndExit(1);
|
return drainStdoutAndExit(1);
|
||||||
}
|
}
|
||||||
process.exit(retval);
|
cleanupAndExit(retval);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
processStdin(opts, stylize, function () {
|
processStdin(opts, stylize, function () {
|
||||||
process.exit(retval);
|
cleanupAndExit(retval);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
58
examples/long-running.js
Normal file
58
examples/long-running.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* A long-running process that does some periodic logging. Use bunyan with
|
||||||
|
* it some of these ways:
|
||||||
|
*
|
||||||
|
* 1. Direct piping:
|
||||||
|
* node long-running.js | bunyan
|
||||||
|
* 2. Logging to file (e.g. if run via a service system like upstart or
|
||||||
|
* illumos' SMF that sends std output to a log file), then tail -f that
|
||||||
|
* log file.
|
||||||
|
* node long-running.js > long-running.log 2>&1
|
||||||
|
* tail -f long-running.log | bunyan
|
||||||
|
* 3. Dtrace to watch the logging. This has the bonus of being able to watch
|
||||||
|
* all log levels... even if not normally emitted.
|
||||||
|
* node long-running.js > long-running.log 2>&1
|
||||||
|
* bunyan -p $(head -1 long-running.log | json pid)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
var bunyan = require('../lib/bunyan');
|
||||||
|
|
||||||
|
|
||||||
|
function randint(n) {
|
||||||
|
return Math.floor(Math.random() * n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function randchoice(array) {
|
||||||
|
return array[randint(array.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//---- mainline
|
||||||
|
|
||||||
|
var words = fs.readFileSync(__dirname + '/long-running.js', 'utf8').split(/\s+/);
|
||||||
|
var levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
||||||
|
var timeout;
|
||||||
|
|
||||||
|
var log = bunyan.createLogger({name: 'lr', level: 'debug'});
|
||||||
|
|
||||||
|
// We're logging to stdout. Let's exit gracefully on EPIPE. E.g. if piped
|
||||||
|
// to `head` which will close after N lines.
|
||||||
|
process.stdout.on('error', function (err) {
|
||||||
|
if (err.code === 'EPIPE') {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function logOne() {
|
||||||
|
var level = randchoice(levels);
|
||||||
|
var msg = [randchoice(words), randchoice(words)].join(' ');
|
||||||
|
var delay = randint(300);
|
||||||
|
//console.warn('long-running about to log.%s(..., "%s")', level, msg)
|
||||||
|
log[level]({"word": randchoice(words), "delay": delay}, msg);
|
||||||
|
timeout = setTimeout(logOne, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('hi, this is the start');
|
||||||
|
timeout = setTimeout(logOne, 1000);
|
|
@ -4,7 +4,7 @@
|
||||||
* The bunyan logging library for node.js.
|
* The bunyan logging library for node.js.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var VERSION = '0.17.1';
|
var VERSION = '0.18.0';
|
||||||
|
|
||||||
// Bunyan log format version. This becomes the 'v' field on all log records.
|
// Bunyan log format version. This becomes the 'v' field on all log records.
|
||||||
// `0` is until I release a version '1.0.0' of node-bunyan. Thereafter,
|
// `0` is until I release a version '1.0.0' of node-bunyan. Thereafter,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bunyan",
|
"name": "bunyan",
|
||||||
"version": "0.17.1",
|
"version": "0.18.0",
|
||||||
"description": "a JSON Logger library for node.js services",
|
"description": "a JSON Logger library for node.js services",
|
||||||
"author": "Trent Mick <trentm@gmail.com> (http://trentm.com)",
|
"author": "Trent Mick <trentm@gmail.com> (http://trentm.com)",
|
||||||
"main": "./lib/bunyan.js",
|
"main": "./lib/bunyan.js",
|
||||||
|
|
|
@ -90,7 +90,7 @@ test('simple.log with color', function (t) {
|
||||||
t.ifError(err)
|
t.ifError(err)
|
||||||
t.equal(stdout,
|
t.equal(stdout,
|
||||||
'[2012-02-08T22:56:52.856Z] \u001b[36m INFO\u001b[39m: myservice/123 '
|
'[2012-02-08T22:56:52.856Z] \u001b[36m INFO\u001b[39m: myservice/123 '
|
||||||
+ 'on example.com: \u001b[36mMy message\u001b[39m\n');
|
+ 'on example.com: \u001b[36mMy message\u001b[39m\n\u001b[0m');
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -112,7 +112,7 @@ test('extrafield.log with color', function (t) {
|
||||||
t.equal(stdout,
|
t.equal(stdout,
|
||||||
'[2012-02-08T22:56:52.856Z] \u001b[36m INFO\u001b[39m: myservice/123 '
|
'[2012-02-08T22:56:52.856Z] \u001b[36m INFO\u001b[39m: myservice/123 '
|
||||||
+ 'on example.com: \u001b[36mMy message\u001b[39m'
|
+ 'on example.com: \u001b[36mMy message\u001b[39m'
|
||||||
+ '\u001b[90m (extra=field)\u001b[39m\n');
|
+ '\u001b[90m (extra=field)\u001b[39m\n\u001b[0m');
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue