From 89429a34c1bba7fb2bc242e0909011de13c8b9e5 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Wed, 2 Jun 2021 21:52:18 +0000 Subject: [PATCH] Refactor entire project into ecmamodules, remove all dependencies, and use eltro as our test runner. --- .npmignore | 7 - .npmrc | 1 + Makefile | 127 -- bin/bunyan | 1407 --------------- bin/bunyan.mjs | 1397 +++++++++++++++ lib/bunyan.js | 1585 ----------------- lib/bunyan.mjs | 1128 ++++++++++++ lib/safe-json.mjs | 69 + package.json | 28 +- test/add-stream.test.js | 39 - test/add-stream.test.mjs | 28 + test/buffer.test.js | 81 - test/buffer.test.mjs | 74 + ...viour.test.js => child-behaviour.test.mjs} | 91 +- test/cli-client-req.test.js | 68 - test/cli-client-req.test.mjs | 45 + test/cli-res.test.js | 61 - test/cli-res.test.mjs | 38 + test/cli.test.js | 489 ----- test/cli.test.mjs | 302 ++++ test/ctor.test.js | 154 -- test/ctor.test.mjs | 124 ++ test/cycles.test.js | 90 - test/cycles.test.mjs | 83 + test/error-event.test.js | 151 -- test/error-event.test.mjs | 164 ++ test/helper.mjs | 29 + test/level.test.js | 98 - test/level.test.mjs | 79 + test/log.test.js | 271 --- test/log.test.mjs | 227 +++ test/other-api.test.js | 42 - test/other-api.test.mjs | 33 + test/process-exit.js | 11 - test/process-exit.test.js | 32 - test/raw-stream.test.js | 117 -- test/raw-stream.test.mjs | 107 ++ test/ringbuffer.test.js | 47 - test/ringbuffer.test.mjs | 38 + ...ringify-1.js => safe-json-stringify-1.mjs} | 2 +- ...ringify-2.js => safe-json-stringify-2.mjs} | 2 +- ...ringify-3.js => safe-json-stringify-3.mjs} | 2 +- ...ringify-4.js => safe-json-stringify-4.mjs} | 2 +- test/safe-json-stringify.test.js | 69 - test/safe-json-stringify.test.mjs | 46 + test/safe-json.test.mjs | 175 ++ test/serializers.test.js | 334 ---- test/serializers.test.mjs | 242 +++ test/src.test.js | 62 - test/src.test.mjs | 52 + test/stream-levels.test.js | 184 -- test/stream-levels.test.mjs | 152 ++ test/tap4nodeunit.js | 61 - tools/ben.js | 43 + tools/timechild.js | 2 +- tools/timeguard.js | 2 +- tools/timenop.js | 2 +- tools/timesrc.js | 2 +- 58 files changed, 4734 insertions(+), 5664 deletions(-) delete mode 100644 .npmignore create mode 100644 .npmrc delete mode 100644 Makefile delete mode 100755 bin/bunyan create mode 100644 bin/bunyan.mjs delete mode 100644 lib/bunyan.js create mode 100644 lib/bunyan.mjs create mode 100644 lib/safe-json.mjs delete mode 100644 test/add-stream.test.js create mode 100644 test/add-stream.test.mjs delete mode 100644 test/buffer.test.js create mode 100644 test/buffer.test.mjs rename test/{child-behaviour.test.js => child-behaviour.test.mjs} (51%) delete mode 100644 test/cli-client-req.test.js create mode 100644 test/cli-client-req.test.mjs delete mode 100644 test/cli-res.test.js create mode 100644 test/cli-res.test.mjs delete mode 100644 test/cli.test.js create mode 100644 test/cli.test.mjs delete mode 100644 test/ctor.test.js create mode 100644 test/ctor.test.mjs delete mode 100644 test/cycles.test.js create mode 100644 test/cycles.test.mjs delete mode 100644 test/error-event.test.js create mode 100644 test/error-event.test.mjs create mode 100644 test/helper.mjs delete mode 100644 test/level.test.js create mode 100644 test/level.test.mjs delete mode 100644 test/log.test.js create mode 100644 test/log.test.mjs delete mode 100644 test/other-api.test.js create mode 100644 test/other-api.test.mjs delete mode 100644 test/process-exit.js delete mode 100644 test/process-exit.test.js delete mode 100644 test/raw-stream.test.js create mode 100644 test/raw-stream.test.mjs delete mode 100644 test/ringbuffer.test.js create mode 100644 test/ringbuffer.test.mjs rename test/{safe-json-stringify-1.js => safe-json-stringify-1.mjs} (85%) rename test/{safe-json-stringify-2.js => safe-json-stringify-2.mjs} (87%) rename test/{safe-json-stringify-3.js => safe-json-stringify-3.mjs} (91%) rename test/{safe-json-stringify-4.js => safe-json-stringify-4.mjs} (92%) delete mode 100644 test/safe-json-stringify.test.js create mode 100644 test/safe-json-stringify.test.mjs create mode 100644 test/safe-json.test.mjs delete mode 100644 test/serializers.test.js create mode 100644 test/serializers.test.mjs delete mode 100644 test/src.test.js create mode 100644 test/src.test.mjs delete mode 100644 test/stream-levels.test.js create mode 100644 test/stream-levels.test.mjs delete mode 100644 test/tap4nodeunit.js create mode 100644 tools/ben.js diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 84d129f..0000000 --- a/.npmignore +++ /dev/null @@ -1,7 +0,0 @@ -/tmp -/node_modules -*.log -/examples -/test -/*.tgz -/tools diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index a98cb27..0000000 --- a/Makefile +++ /dev/null @@ -1,127 +0,0 @@ - -#---- Tools - -NODEUNIT := ./node_modules/.bin/nodeunit -NODEOPT ?= $(HOME)/opt - - -#---- Files - -JSSTYLE_FILES := $(shell find lib test tools examples -name "*.js") bin/bunyan -# All test files -TEST_FILES := $(shell ls -1 test/*.test.js | xargs) - - -#---- Targets - -all $(NODEUNIT): - npm install $(NPM_INSTALL_FLAGS) - -# Ensure all version-carrying files have the same version. -.PHONY: versioncheck -versioncheck: - @echo version is: $(shell cat package.json | json version) - [[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -2 | tail -1 | awk '{print $$2}'` ]] - @echo Version check ok. - -.PHONY: cutarelease -cutarelease: check - [[ -z `git status --short` ]] # If this fails, the working dir is dirty. - @which json 2>/dev/null 1>/dev/null && \ - ver=$(shell json -f package.json version) && \ - name=$(shell json -f package.json name) && \ - publishedVer=$(shell npm view -j $(shell json -f package.json name)@$(shell json -f package.json version) version 2>/dev/null) && \ - if [[ -n "$$publishedVer" ]]; then \ - echo "error: $$name@$$ver is already published to npm"; \ - exit 1; \ - fi && \ - echo "** Are you sure you want to tag and publish $$name@$$ver to npm?" && \ - echo "** Enter to continue, Ctrl+C to abort." && \ - read - ver=$(shell cat package.json | json version) && \ - date=$(shell date -u "+%Y-%m-%d") && \ - git tag -a "$$ver" -m "version $$ver ($$date) beta" && \ - git push --tags origin && \ - npm publish --tag beta - -.PHONY: docs -docs: toc - @[[ `which ronn` ]] || (echo "No 'ronn' on your PATH. Install with 'gem install ronn'" && exit 2) - mkdir -p man/man1 - ronn --style=toc --manual="bunyan manual" --date=$(shell git log -1 --pretty=format:%cd --date=short) --roff --html docs/bunyan.1.ronn - python -c 'import sys; h = open("docs/bunyan.1.html").read(); h = h.replace(".mp dt.flush {float:left;width:8ex}", ""); open("docs/bunyan.1.html", "w").write(h)' - python -c 'import sys; h = open("docs/bunyan.1.html").read(); h = h.replace("", """Fork me on GitHub"""); open("docs/bunyan.1.html", "w").write(h)' - @echo "# test with 'man ./docs/bunyan.1' and 'open ./docs/bunyan.1.html'" - -# Re-generate the README.md table of contents. -toc: - ./node_modules/.bin/markdown-toc -i README.md - - -.PHONY: publish -publish: - mkdir -p tmp - [[ -d tmp/bunyan-gh-pages ]] || git clone git@github.com:trentm/node-bunyan.git tmp/bunyan-gh-pages - cd tmp/bunyan-gh-pages && git checkout gh-pages && git pull --rebase origin gh-pages - cp docs/index.html tmp/bunyan-gh-pages/index.html - cp docs/bunyan.1.html tmp/bunyan-gh-pages/bunyan.1.html - (cd tmp/bunyan-gh-pages \ - && git commit -a -m "publish latest docs" \ - && git push origin gh-pages || true) - -.PHONY: distclean -distclean: - rm -rf node_modules - - -#---- test - -.PHONY: test -test: $(NODEUNIT) - $(NODEUNIT) $(TEST_FILES) - -# Test with all node supported versions (presumes install locations I use on -# my machine -- "~/opt/node-VER"): -.PHONY: testall -testall: test7 test6 test012 test010 test4 - -.PHONY: test7 -test7: - @echo "# Test node 7.x (with node `$(NODEOPT)/node-7/bin/node --version`)" - @$(NODEOPT)/node-7/bin/node --version | grep '^v7\.' - PATH="$(NODEOPT)/node-7/bin:$(PATH)" make distclean all test -.PHONY: test6 -test6: - @echo "# Test node 6.x (with node `$(NODEOPT)/node-6/bin/node --version`)" - @$(NODEOPT)/node-6/bin/node --version | grep '^v6\.' - PATH="$(NODEOPT)/node-6/bin:$(PATH)" make distclean all test -.PHONY: test4 -test4: - @echo "# Test node 4.x (with node `$(NODEOPT)/node-4/bin/node --version`)" - @$(NODEOPT)/node-4/bin/node --version | grep '^v4\.' - PATH="$(NODEOPT)/node-4/bin:$(PATH)" make distclean all test -.PHONY: test012 -test012: - @echo "# Test node 0.12.x (with node `$(NODEOPT)/node-0.12/bin/node --version`)" - @$(NODEOPT)/node-0.12/bin/node --version | grep '^v0\.12\.' - PATH="$(NODEOPT)/node-0.12/bin:$(PATH)" make distclean all test -.PHONY: test010 -test010: - @echo "# Test node 0.10.x (with node `$(NODEOPT)/node-0.10/bin/node --version`)" - @$(NODEOPT)/node-0.10/bin/node --version | grep '^v0\.10\.' - PATH="$(NODEOPT)/node-0.10/bin:$(PATH)" make distclean all test - - -#---- check - -.PHONY: check-jsstyle -check-jsstyle: $(JSSTYLE_FILES) - ./tools/jsstyle -o indent=4,doxygen,unparenthesized-return=0,blank-after-start-comment=0,leading-right-paren-ok=1 $(JSSTYLE_FILES) - -.PHONY: check -check: check-jsstyle versioncheck - @echo "Check ok." - -.PHONY: prepush -prepush: check testall - @echo "Okay to push." diff --git a/bin/bunyan b/bin/bunyan deleted file mode 100755 index 7d85830..0000000 --- a/bin/bunyan +++ /dev/null @@ -1,1407 +0,0 @@ -#!/usr/bin/env node -/** - * Copyright 2017 Trent Mick - * Copyright 2017 Joyent Inc. - * - * bunyan -- filter and pretty-print Bunyan log files (line-delimited JSON) - * - * See . - * - * -*- mode: js -*- - * vim: expandtab:ts=4:sw=4 - */ - -var p = console.log; -var util = require('util'); -var pathlib = require('path'); -var vm = require('vm'); -var http = require('http'); -var fs = require('fs'); -var warn = console.warn; -var child_process = require('child_process'), - spawn = child_process.spawn, - exec = child_process.exec, - execFile = child_process.execFile; -var assert = require('assert'); - -//---- globals and constants - -var nodeVer = process.versions.node.split('.').map(Number); -var nodeSpawnSupportsStdio = (nodeVer[0] > 0 || nodeVer[1] >= 8); - -// Internal debug logging via `console.warn`. -var _selfTrace = function selfTraceNoop() {}; -if (process.env.BUNYAN_SELF_TRACE === '1') { - _selfTrace = function selfTrace() { - process.stderr.write('[bunyan self-trace] '); - console.warn.apply(null, arguments); - } -} - -// Output modes. -var OM_LONG = 1; -var OM_JSON = 2; -var OM_INSPECT = 3; -var OM_SIMPLE = 4; -var OM_SHORT = 5; -var OM_BUNYAN = 6; -var OM_FROM_NAME = { - 'long': OM_LONG, - 'paul': OM_LONG, /* backward compat */ - 'json': OM_JSON, - 'inspect': OM_INSPECT, - 'simple': OM_SIMPLE, - 'short': OM_SHORT, - 'bunyan': OM_BUNYAN -}; - - -// Levels -var TRACE = 10; -var DEBUG = 20; -var INFO = 30; -var WARN = 40; -var ERROR = 50; -var FATAL = 60; - -var levelFromName = { - 'trace': TRACE, - 'debug': DEBUG, - 'info': INFO, - 'warn': WARN, - 'error': ERROR, - 'fatal': FATAL -}; -var nameFromLevel = {}; -var upperNameFromLevel = {}; -var upperPaddedNameFromLevel = {}; -Object.keys(levelFromName).forEach(function (name) { - var lvl = levelFromName[name]; - nameFromLevel[lvl] = name; - upperNameFromLevel[lvl] = name.toUpperCase(); - upperPaddedNameFromLevel[lvl] = ( - name.length === 4 ? ' ' : '') + name.toUpperCase(); -}); - - -// Display time formats. -var TIME_UTC = 1; // the default, bunyan's native format -var TIME_LOCAL = 2; - - -// Boolean set to true when we are in the process of exiting. We don't always -// hard-exit (e.g. when staying alive while the pager completes). -var exiting = false; - -// The current raw input line being processed. Used for `uncaughtException`. -var currLine = null; - -// Whether ANSI codes are being used. Used for signal-handling. -var usingAnsiCodes = false; - -// Used to tell the 'uncaughtException' handler that '-c CODE' is being used. -var gUsingConditionOpts = false; - -// Pager child process, and output stream to which to write. -var pager = null; -var stdout = process.stdout; - - - -//---- support functions - -var _version = null; -function getVersion() { - if (_version === null) { - _version = require('../package.json').version; - } - return _version; -} - - -var format = util.format; -if (!format) { - /* BEGIN JSSTYLED */ - // If not node 0.6, then use its `util.format`: - // : - var inspect = util.inspect; - var formatRegExp = /%[sdj%]/g; - format = function format(f) { - if (typeof f !== 'string') { - var objects = []; - for (var i = 0; i < arguments.length; i++) { - objects.push(inspect(arguments[i])); - } - return objects.join(' '); - } - - var i = 1; - var args = arguments; - var len = args.length; - var str = String(f).replace(formatRegExp, function (x) { - if (i >= len) - return x; - switch (x) { - case '%s': return String(args[i++]); - case '%d': return Number(args[i++]); - case '%j': return JSON.stringify(args[i++]); - case '%%': return '%'; - default: - return x; - } - }); - for (var x = args[i]; i < len; x = args[++i]) { - if (x === null || typeof x !== 'object') { - str += ' ' + x; - } else { - str += ' ' + inspect(x); - } - } - return str; - }; - /* END JSSTYLED */ -} - -function indent(s) { - return ' ' + s.split(/\r?\n/).join('\n '); -} - -function objCopy(obj) { - if (obj === null) { - return null; - } else if (Array.isArray(obj)) { - return obj.slice(); - } else { - var copy = {}; - Object.keys(obj).forEach(function (k) { - copy[k] = obj[k]; - }); - return copy; - } -} - -function printHelp() { - /* BEGIN JSSTYLED */ - p('Usage:'); - p(' bunyan [OPTIONS] [FILE ...]'); - p(' ... | bunyan [OPTIONS]'); - p(' bunyan [OPTIONS] -p PID'); - p(''); - p('Filter and pretty-print Bunyan log file content.'); - p(''); - p('General options:'); - p(' -h, --help print this help info and exit'); - p(' --version print version of this command and exit'); - p(''); - p('Filtering options:'); - p(' -l, --level LEVEL'); - p(' Only show messages at or above the specified level.'); - p(' You can specify level *names* or the internal numeric'); - p(' values.'); - p(' -c, --condition CONDITION'); - p(' Run each log message through the condition and'); - p(' only show those that return truish. E.g.:'); - p(' -c \'this.pid == 123\''); - p(' -c \'this.level == DEBUG\''); - p(' -c \'this.msg.indexOf("boom") != -1\''); - p(' "CONDITION" must be legal JS code. `this` holds'); - p(' the log record. The TRACE, DEBUG, ... FATAL values'); - p(' are defined to help with comparing `this.level`.'); - p(' --strict Suppress all but legal Bunyan JSON log lines. By default'); - p(' non-JSON, and non-Bunyan lines are passed through.'); - p(''); - p('Output options:'); - p(' --pager Pipe output into `less` (or $PAGER if set), if'); - p(' stdout is a TTY. This overrides $BUNYAN_NO_PAGER.'); - p(' Note: Paging is only supported on node >=0.8.'); - p(' --no-pager Do not pipe output into a pager.'); - p(' --color Colorize output. Defaults to try if output'); - p(' stream is a TTY.'); - p(' --no-color Force no coloring (e.g. terminal doesn\'t support it)'); - p(' -o, --output MODE'); - p(' Specify an output mode/format. One of'); - p(' long: (the default) pretty'); - p(' json: JSON output, 2-space indent'); - p(' json-N: JSON output, N-space indent, e.g. "json-4"'); - p(' bunyan: 0 indented JSON, bunyan\'s native format'); - p(' inspect: node.js `util.inspect` output'); - p(' short: like "long", but more concise'); - p(' simple: level, followed by "-" and then the message'); - p(' -j shortcut for `-o json`'); - p(' -0 shortcut for `-o bunyan`'); - p(''); - p('Environment Variables:'); - p(' BUNYAN_NO_COLOR Set to a non-empty value to force no output '); - p(' coloring. See "--no-color".'); - p(' BUNYAN_NO_PAGER Disable piping output to a pager. '); - p(' See "--no-pager".'); - p(''); - p('See for more complete docs.'); - p('Please report bugs to .'); - /* END JSSTYLED */ -} - -/* - * If the user specifies multiple input sources, we want to print out records - * from all sources in a single, chronologically ordered stream. To do this - * efficiently, we first assume that all records within each source are ordered - * already, so we need only keep track of the next record in each source and - * the time of the last record emitted. To avoid excess memory usage, we - * pause() streams that are ahead of others. - * - * 'streams' is an object indexed by source name (file name) which specifies: - * - * stream Actual stream object, so that we can pause and resume it. - * - * records Array of log records we've read, but not yet emitted. Each - * record includes 'line' (the raw line), 'rec' (the JSON - * record), and 'time' (the parsed time value). - * - * done Whether the stream has any more records to emit. - */ -var streams = {}; - -function gotRecord(file, line, rec, opts, stylize) -{ - var time = new Date(rec.time); - - streams[file]['records'].push({ line: line, rec: rec, time: time }); - emitNextRecord(opts, stylize); -} - -function filterRecord(rec, opts) -{ - if (opts.level && rec.level < opts.level) { - return false; - } - - if (opts.condFuncs) { - var recCopy = objCopy(rec); - for (var i = 0; i < opts.condFuncs.length; i++) { - var pass = opts.condFuncs[i].call(recCopy); - if (!pass) - return false; - } - } else if (opts.condVm) { - for (var i = 0; i < opts.condVm.length; i++) { - var pass = opts.condVm[i].runInNewContext(rec); - if (!pass) - return false; - } - } - - return true; -} - -function emitNextRecord(opts, stylize) -{ - var ofile, ready, minfile, rec; - - for (;;) { - /* - * Take a first pass through the input streams to see if we have a - * record from all of them. If not, we'll pause any streams for - * which we do already have a record (to avoid consuming excess - * memory) and then wait until we have records from the others - * before emitting the next record. - * - * As part of the same pass, we look for the earliest record - * we have not yet emitted. - */ - minfile = undefined; - ready = true; - for (ofile in streams) { - - if (streams[ofile].stream === null || - (!streams[ofile].done && streams[ofile].records.length === 0)) { - ready = false; - break; - } - - if (streams[ofile].records.length > 0 && - (minfile === undefined || - streams[minfile].records[0].time > - streams[ofile].records[0].time)) { - minfile = ofile; - } - } - - if (!ready || minfile === undefined) { - for (ofile in streams) { - if (!streams[ofile].stream || streams[ofile].done) - continue; - - if (streams[ofile].records.length > 0) { - if (!streams[ofile].paused) { - streams[ofile].paused = true; - streams[ofile].stream.pause(); - } - } else if (streams[ofile].paused) { - streams[ofile].paused = false; - streams[ofile].stream.resume(); - } - } - - return; - } - - /* - * Emit the next record for 'minfile', and invoke ourselves again to - * make sure we emit as many records as we can right now. - */ - rec = streams[minfile].records.shift(); - emitRecord(rec.rec, rec.line, opts, stylize); - } -} - -/** - * Return a function for the given JS code that returns. - * - * If no 'return' in the given javascript snippet, then assume we are a single - * statement and wrap in 'return (...)'. This is for convenience for short - * '-c ...' snippets. - */ -function funcWithReturnFromSnippet(js) { - // auto-"return" - if (js.indexOf('return') === -1) { - if (js.substring(js.length - 1) === ';') { - js = js.substring(0, js.length - 1); - } - js = 'return (' + js + ')'; - } - - // Expose level definitions to condition func context - var varDefs = []; - Object.keys(upperNameFromLevel).forEach(function (lvl) { - varDefs.push(format('var %s = %d;', - upperNameFromLevel[lvl], lvl)); - }); - varDefs = varDefs.join('\n') + '\n'; - - return (new Function(varDefs + js)); -} - -/** - * Parse the command-line options and arguments into an object. - * - * { - * 'args': [...] // arguments - * 'help': true, // true if '-h' option given - * // etc. - * } - * - * @return {Object} The parsed options. `.args` is the argument list. - * @throws {Error} If there is an error parsing argv. - */ -function parseArgv(argv) { - var parsed = { - args: [], - help: false, - color: null, - paginate: null, - outputMode: OM_LONG, - jsonIndent: 2, - level: null, - strict: false, - timeFormat: TIME_UTC // one of the TIME_ constants - }; - - // Turn '-iH' into '-i -H', except for argument-accepting options. - var args = argv.slice(2); // drop ['node', 'scriptname'] - var newArgs = []; - var optTakesArg = {'d': true, 'o': true, 'c': true, 'l': true, 'p': true}; - for (var i = 0; i < args.length; i++) { - if (args[i].charAt(0) === '-' && args[i].charAt(1) !== '-' && - args[i].length > 2) - { - var splitOpts = args[i].slice(1).split(''); - for (var j = 0; j < splitOpts.length; j++) { - newArgs.push('-' + splitOpts[j]); - if (optTakesArg[splitOpts[j]]) { - var optArg = splitOpts.slice(j+1).join(''); - if (optArg.length) { - newArgs.push(optArg); - } - break; - } - } - } else { - newArgs.push(args[i]); - } - } - args = newArgs; - - // Expose level definitions to condition vm context - var condDefines = []; - Object.keys(upperNameFromLevel).forEach(function (lvl) { - condDefines.push( - format('Object.prototype.%s = %s;', upperNameFromLevel[lvl], lvl)); - }); - condDefines = condDefines.join('\n') + '\n'; - - var endOfOptions = false; - while (args.length > 0) { - var arg = args.shift(); - switch (arg) { - case '--': - endOfOptions = true; - break; - case '-h': // display help and exit - case '--help': - parsed.help = true; - break; - case '--version': - parsed.version = true; - break; - case '--strict': - parsed.strict = true; - break; - case '--color': - parsed.color = true; - break; - case '--no-color': - parsed.color = false; - break; - case '--pager': - parsed.paginate = true; - break; - case '--no-pager': - parsed.paginate = false; - break; - case '-o': - case '--output': - var name = args.shift(); - var idx = name.lastIndexOf('-'); - if (idx !== -1) { - var indentation = Number(name.slice(idx+1)); - if (! isNaN(indentation)) { - parsed.jsonIndent = indentation; - name = name.slice(0, idx); - } - } - parsed.outputMode = OM_FROM_NAME[name]; - if (parsed.outputMode === undefined) { - throw new Error('unknown output mode: "'+name+'"'); - } - break; - case '-j': // output with JSON.stringify - parsed.outputMode = OM_JSON; - break; - case '-0': - parsed.outputMode = OM_BUNYAN; - break; - case '--time': - var timeArg = args.shift(); - switch (timeArg) { - case 'utc': - parsed.timeFormat = TIME_UTC; - break - case undefined: - throw new Error('missing argument to "--time"'); - default: - throw new Error(format('invalid time format: "%s"', - timeArg)); - } - break; - case '-l': - case '--level': - var levelArg = args.shift(); - var level = +(levelArg); - if (isNaN(level)) { - level = +levelFromName[levelArg.toLowerCase()]; - } - if (isNaN(level)) { - throw new Error('unknown level value: "'+levelArg+'"'); - } - parsed.level = level; - break; - case '-c': - case '--condition': - gUsingConditionOpts = true; - var condition = args.shift(); - if (Boolean(process.env.BUNYAN_EXEC && - process.env.BUNYAN_EXEC === 'vm')) - { - parsed.condVm = parsed.condVm || []; - var scriptName = 'bunyan-condition-'+parsed.condVm.length; - var code = condDefines + condition; - var script; - try { - script = vm.createScript(code, scriptName); - } catch (complErr) { - throw new Error(format('illegal CONDITION code: %s\n' - + ' CONDITION script:\n' - + '%s\n' - + ' Error:\n' - + '%s', - complErr, indent(code), indent(complErr.stack))); - } - - // Ensure this is a reasonably safe CONDITION. - try { - script.runInNewContext(minValidRecord); - } catch (condErr) { - throw new Error(format( - /* JSSTYLED */ - 'CONDITION code cannot safely filter a minimal Bunyan log record\n' - + ' CONDITION script:\n' - + '%s\n' - + ' Minimal Bunyan log record:\n' - + '%s\n' - + ' Filter error:\n' - + '%s', - indent(code), - indent(JSON.stringify(minValidRecord, null, 2)), - indent(condErr.stack) - )); - } - parsed.condVm.push(script); - } else { - parsed.condFuncs = parsed.condFuncs || []; - parsed.condFuncs.push(funcWithReturnFromSnippet(condition)); - } - break; - default: // arguments - if (!endOfOptions && arg.length > 0 && arg[0] === '-') { - throw new Error('unknown option "'+arg+'"'); - } - parsed.args.push(arg); - break; - } - } - //TODO: '--' handling and error on a first arg that looks like an option. - - return parsed; -} - - -function isInteger(s) { - return (s.search(/^-?[0-9]+$/) == 0); -} - - -// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics -// Suggested colors (some are unreadable in common cases): -// - Good: cyan, yellow (limited use), bold, green, magenta, red -// - Bad: blue (not visible on cmd.exe), grey (same color as background on -// Solarized Dark theme from , see -// issue #160) -var colors = { - 'bold' : [1, 22], - 'italic' : [3, 23], - 'underline' : [4, 24], - 'inverse' : [7, 27], - 'white' : [37, 39], - 'grey' : [90, 39], - 'black' : [30, 39], - 'blue' : [34, 39], - 'cyan' : [36, 39], - 'green' : [32, 39], - 'magenta' : [35, 39], - 'red' : [31, 39], - 'yellow' : [33, 39] -}; - -function stylizeWithColor(str, color) { - if (!str) - return ''; - var codes = colors[color]; - if (codes) { - return '\033[' + codes[0] + 'm' + str + - '\033[' + codes[1] + 'm'; - } else { - return str; - } -} - -function stylizeWithoutColor(str, color) { - return str; -} - - -/** - * Is this a valid Bunyan log record. - */ -function isValidRecord(rec) { - if (rec.v == null || - rec.level == null || - rec.name == null || - rec.hostname == null || - rec.pid == null || - rec.time == null || - rec.msg == null) { - // Not valid Bunyan log. - return false; - } else { - return true; - } -} -var minValidRecord = { - v: 0, //TODO: get this from bunyan.LOG_VERSION - level: INFO, - name: 'name', - hostname: 'hostname', - pid: 123, - time: Date.now(), - msg: 'msg' -}; - - -/** - * Parses the given log line and either emits it right away (for invalid - * records) or enqueues it for emitting later when it's the next line to show. - */ -function handleLogLine(file, line, opts, stylize) { - if (exiting) { - _selfTrace('warn: handleLogLine called while exiting'); - return; - } - - currLine = line; // intentionally global - - // Emit non-JSON lines immediately. - var rec; - if (!line) { - if (!opts.strict) emit(line + '\n'); - return; - } else if (line[0] !== '{') { - if (!opts.strict) emit(line + '\n'); // not JSON - return; - } else { - try { - rec = JSON.parse(line); - } catch (e) { - if (!opts.strict) emit(line + '\n'); - return; - } - } - - if (!isValidRecord(rec)) { - if (!opts.strict) emit(line + '\n'); - return; - } - - if (!filterRecord(rec, opts)) - return; - - if (file === null) - return emitRecord(rec, line, opts, stylize); - - return gotRecord(file, line, rec, opts, stylize); -} - -/** - * Print out a single result, considering input options. - */ -function emitRecord(rec, line, opts, stylize) { - var short = false; - - switch (opts.outputMode) { - case OM_SHORT: - short = true; - /* jsl:fall-thru */ - - case OM_LONG: - // [time] LEVEL: name[/comp]/pid on hostname (src): msg* (extras...) - // msg* - // -- - // long and multi-line extras - // ... - // If 'msg' is single-line, then it goes in the top line. - // If 'req', show the request. - // If 'res', show the response. - // If 'err' and 'err.stack' then show that. - if (!isValidRecord(rec)) { - return emit(line + '\n'); - } - - delete rec.v; - - // Time. - var time; - if (!short && opts.timeFormat === TIME_UTC) { - // Fast default path: We assume the raw `rec.time` is a UTC time - // in ISO 8601 format (per spec). - time = '[' + rec.time + ']'; - } else if (opts.timeFormat === TIME_UTC) { - time = rec.time.substr(11); - } - time = stylize(time, 'none'); - delete rec.time; - - var nameStr = rec.name; - delete rec.name; - - if (rec.component) { - nameStr += '/' + rec.component; - } - delete rec.component; - - if (!short) - nameStr += '/' + rec.pid; - delete rec.pid; - - var level = (upperPaddedNameFromLevel[rec.level] || 'LVL' + rec.level); - if (opts.color) { - var colorFromLevel = { - 10: 'white', // TRACE - 20: 'yellow', // DEBUG - 30: 'cyan', // INFO - 40: 'magenta', // WARN - 50: 'red', // ERROR - 60: 'inverse', // FATAL - }; - level = stylize(level, colorFromLevel[rec.level]); - } - delete rec.level; - - var src = ''; - if (rec.src && rec.src.file) { - var s = rec.src; - if (s.func) { - src = format(' (%s:%d in %s)', s.file, s.line, s.func); - } else { - src = format(' (%s:%d)', s.file, s.line); - } - src = stylize(src, 'green'); - } - delete rec.src; - - var hostname = rec.hostname; - delete rec.hostname; - - var extras = []; - var details = []; - - if (rec.req_id) { - extras.push('req_id=' + rec.req_id); - } - delete rec.req_id; - - var onelineMsg; - if (rec.msg.indexOf('\n') !== -1) { - onelineMsg = ''; - details.push(indent(stylize(rec.msg, 'cyan'))); - } else { - onelineMsg = ' ' + stylize(rec.msg, 'cyan'); - } - delete rec.msg; - - if (rec.req && typeof (rec.req) === 'object') { - var req = rec.req; - delete rec.req; - var headers = req.headers; - if (!headers) { - headers = ''; - } else if (typeof (headers) === 'string') { - headers = '\n' + headers; - } else if (typeof (headers) === 'object') { - headers = '\n' + Object.keys(headers).map(function (h) { - return h + ': ' + headers[h]; - }).join('\n'); - } - var s = format('%s %s HTTP/%s%s', req.method, - req.url, - req.httpVersion || '1.1', - headers - ); - delete req.url; - delete req.method; - delete req.httpVersion; - delete req.headers; - if (req.body) { - s += '\n\n' + (typeof (req.body) === 'object' - ? JSON.stringify(req.body, null, 2) : req.body); - delete req.body; - } - if (req.trailers && Object.keys(req.trailers) > 0) { - s += '\n' + Object.keys(req.trailers).map(function (t) { - return t + ': ' + req.trailers[t]; - }).join('\n'); - } - delete req.trailers; - details.push(indent(s)); - // E.g. for extra 'foo' field on 'req', add 'req.foo' at - // top-level. This *does* have the potential to stomp on a - // literal 'req.foo' key. - Object.keys(req).forEach(function (k) { - rec['req.' + k] = req[k]; - }) - } - - if (rec.client_req && typeof (rec.client_req) === 'object') { - var client_req = rec.client_req; - delete rec.client_req; - - var headers = client_req.headers; - delete client_req.headers; - - var s = format('%s %s HTTP/%s%s', - client_req.method, - client_req.url, - client_req.httpVersion || '1.1', - (headers ? - '\n' + Object.keys(headers).map( - function (h) { - return h + ': ' + headers[h]; - }).join('\n') : - '')); - delete client_req.method; - delete client_req.url; - delete client_req.httpVersion; - - if (client_req.body) { - s += '\n\n' + (typeof (client_req.body) === 'object' ? - JSON.stringify(client_req.body, null, 2) : - client_req.body); - delete client_req.body; - } - // E.g. for extra 'foo' field on 'client_req', add - // 'client_req.foo' at top-level. This *does* have the potential - // to stomp on a literal 'client_req.foo' key. - Object.keys(client_req).forEach(function (k) { - rec['client_req.' + k] = client_req[k]; - }); - details.push(indent(s)); - } - - function _res(res) { - var s = ''; - - /* - * Handle `res.header` or `res.headers` as either a string or - * an object of header key/value pairs. Prefer `res.header` if set, - * because that's what Bunyan's own `res` serializer specifies, - * because that's the value in Node.js's core HTTP server response - * implementation that has all the implicit headers. - * - * Note: `res.header` (string) typically includes the 'HTTP/1.1 ...' - * status line. - */ - var headerTypes = {string: true, object: true}; - var headers; - var headersStr = ''; - var headersHaveStatusLine = false; - if (res.header && headerTypes[typeof (res.header)]) { - headers = res.header; - delete res.header; - } else if (res.headers && headerTypes[typeof (res.headers)]) { - headers = res.headers; - delete res.headers; - } - if (headers === undefined) { - /* pass through */ - } else if (typeof (headers) === 'string') { - headersStr = headers.trimRight(); // Trim the CRLF. - if (headersStr.slice(0, 5) === 'HTTP/') { - headersHaveStatusLine = true; - } - } else { - headersStr += Object.keys(headers).map( - function (h) { return h + ': ' + headers[h]; }).join('\n'); - } - - /* - * Add a 'HTTP/1.1 ...' status line if the headers didn't already - * include it. - */ - if (!headersHaveStatusLine && res.statusCode !== undefined) { - s += format('HTTP/1.1 %s %s\n', res.statusCode, - http.STATUS_CODES[res.statusCode]); - } - delete res.statusCode; - s += headersStr; - - if (res.body !== undefined) { - var body = (typeof (res.body) === 'object' - ? JSON.stringify(res.body, null, 2) : res.body); - if (body.length > 0) { s += '\n\n' + body }; - delete res.body; - } else { - s = s.trimRight(); - } - if (res.trailer) { - s += '\n' + res.trailer; - } - delete res.trailer; - if (s) { - details.push(indent(s)); - } - // E.g. for extra 'foo' field on 'res', add 'res.foo' at - // top-level. This *does* have the potential to stomp on a - // literal 'res.foo' key. - Object.keys(res).forEach(function (k) { - rec['res.' + k] = res[k]; - }); - } - - if (rec.res && typeof (rec.res) === 'object') { - _res(rec.res); - delete rec.res; - } - if (rec.client_res && typeof (rec.client_res) === 'object') { - _res(rec.client_res); - delete rec.client_res; - } - - if (rec.err && rec.err.stack) { - var err = rec.err - if (typeof (err.stack) !== 'string') { - details.push(indent(err.stack.toString())); - } else { - details.push(indent(err.stack)); - } - delete err.message; - delete err.name; - delete err.stack; - // E.g. for extra 'foo' field on 'err', add 'err.foo' at - // top-level. This *does* have the potential to stomp on a - // literal 'err.foo' key. - Object.keys(err).forEach(function (k) { - rec['err.' + k] = err[k]; - }) - delete rec.err; - } - - var leftover = Object.keys(rec); - for (var i = 0; i < leftover.length; i++) { - var key = leftover[i]; - var value = rec[key]; - var stringified = false; - if (typeof (value) !== 'string') { - value = JSON.stringify(value, null, 2); - stringified = true; - } - if (value.indexOf('\n') !== -1 || value.length > 50) { - details.push(indent(key + ': ' + value)); - } else if (!stringified && (value.indexOf(' ') != -1 || - value.length === 0)) - { - extras.push(key + '=' + JSON.stringify(value)); - } else { - extras.push(key + '=' + value); - } - } - - extras = stylize( - (extras.length ? ' (' + extras.join(', ') + ')' : ''), 'none'); - details = stylize( - (details.length ? details.join('\n --\n') + '\n' : ''), 'none'); - if (!short) - emit(format('%s %s: %s on %s%s:%s%s\n%s', - time, - level, - nameStr, - hostname || '', - src, - onelineMsg, - extras, - details)); - else - emit(format('%s %s %s:%s%s\n%s', - time, - level, - nameStr, - onelineMsg, - extras, - details)); - break; - - case OM_INSPECT: - emit(util.inspect(rec, false, Infinity, true) + '\n'); - break; - - case OM_BUNYAN: - emit(JSON.stringify(rec, null, 0) + '\n'); - break; - - case OM_JSON: - emit(JSON.stringify(rec, null, opts.jsonIndent) + '\n'); - break; - - case OM_SIMPLE: - /* JSSTYLED */ - // - if (!isValidRecord(rec)) { - return emit(line + '\n'); - } - emit(format('%s - %s\n', - upperNameFromLevel[rec.level] || 'LVL' + rec.level, - rec.msg)); - break; - default: - throw new Error('unknown output mode: '+opts.outputMode); - } -} - - -function emit(s) { - try { - stdout.write(s); - } catch (writeErr) { - _selfTrace('exception from stdout.write:', writeErr) - // Handle any exceptions in stdout writing in `stdout.on('error', ...)`. - } -} - - -/** - * Process all input from stdin. - * - * @params opts {Object} Bunyan options object. - * @param stylize {Function} Output stylize function to use. - * @param callback {Function} `function ()` - */ -function processStdin(opts, stylize, callback) { - var leftover = ''; // Left-over partial line from last chunk. - var stdin = process.stdin; - stdin.resume(); - stdin.setEncoding('utf8'); - stdin.on('data', function (chunk) { - var lines = chunk.split(/\r\n|\n/); - var length = lines.length; - if (length === 1) { - leftover += lines[0]; - return; - } - - if (length > 1) { - handleLogLine(null, leftover + lines[0], opts, stylize); - } - leftover = lines.pop(); - length -= 1; - for (var i = 1; i < length; i++) { - handleLogLine(null, lines[i], opts, stylize); - } - }); - stdin.on('end', function () { - if (leftover) { - handleLogLine(null, leftover, opts, stylize); - leftover = ''; - } - callback(); - }); -} - - -/** - * Process all input from the given log file. - * - * @param file {String} Log file path to process. - * @params opts {Object} Bunyan options object. - * @param stylize {Function} Output stylize function to use. - * @param callback {Function} `function ()` - */ -function processFile(file, opts, stylize, callback) { - var stream = fs.createReadStream(file); - if (/\.gz$/.test(file)) { - stream = stream.pipe(require('zlib').createGunzip()); - } - // Manually decode streams - lazy load here as per node/lib/fs.js - var decoder = new (require('string_decoder').StringDecoder)('utf8'); - - streams[file].stream = stream; - - stream.on('error', function (err) { - streams[file].done = true; - callback(err); - }); - - var leftover = ''; // Left-over partial line from last chunk. - stream.on('data', function (data) { - if (exiting) { - _selfTrace('stop reading file "%s" because exiting', file); - stream.destroy(); - return; - } - - var chunk = decoder.write(data); - if (!chunk.length) { - return; - } - var lines = chunk.split(/\r\n|\n/); - var length = lines.length; - if (length === 1) { - leftover += lines[0]; - return; - } - - if (length > 1) { - handleLogLine(file, leftover + lines[0], opts, stylize); - } - leftover = lines.pop(); - length -= 1; - for (var i = 1; i < length; i++) { - handleLogLine(file, lines[i], opts, stylize); - } - }); - - stream.on('end', function () { - streams[file].done = true; - if (leftover) { - handleLogLine(file, leftover, opts, stylize); - leftover = ''; - } else { - emitNextRecord(opts, stylize); - } - callback(); - }); -} - - -/** - * From node async module. - */ -/* BEGIN JSSTYLED */ -function asyncForEach(arr, iterator, callback) { - callback = callback || function () {}; - if (!arr.length) { - return callback(); - } - var completed = 0; - arr.forEach(function (x) { - iterator(x, function (err) { - if (err) { - callback(err); - callback = function () {}; - } - else { - completed += 1; - if (completed === arr.length) { - callback(); - } - } - }); - }); -}; -/* END JSSTYLED */ - - - -/** - * Cleanup and exit properly. - * - * Warning: this doesn't necessarily stop processing, i.e. process exit - * might be delayed. It is up to the caller to ensure that no subsequent - * bunyan processing is done after calling this. - * - * @param code {Number} exit code. - * @param signal {String} Optional signal name, if this was exitting because - * of a signal. - */ -function cleanupAndExit(code, signal) { - // Guard one call. - if (exiting) { - return; - } - exiting = true; - _selfTrace('cleanupAndExit(%s, %s)', code, signal); - - // Clear possibly interrupted ANSI code (issue #59). - if (usingAnsiCodes) { - stdout.write('\033[0m'); - } - - if (pager) { - // Let pager know that output is done, then wait for pager to exit. - pager.removeListener('exit', onPrematurePagerExit); - pager.on('exit', function onPagerExit(pagerCode) { - _selfTrace('pager exit -> process.exit(%s)', pagerCode || code); - process.exit(pagerCode || code); - }); - stdout.end(); - } else if (code) { - // Non-zero exit: Something is wrong. We are very likely still - // processing log records -- i.e. we have open handles -- so we need - // a hard stop (aka `process.exit`). - _selfTrace('process.exit(%s)', code); - process.exit(code); - } else { - // Zero exit: This should be a "normal" exit, for which we want to - // flush stdout/stderr. - process.exit(0); - } -} - - - -//---- 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) { - function _indent(s) { - var lines = s.split(/\r?\n/); - for (var i = 0; i < lines.length; i++) { - lines[i] = '* ' + lines[i]; - } - return lines.join('\n'); - } - - var title = encodeURIComponent(format( - 'Bunyan %s crashed: %s', getVersion(), String(err))); - var e = console.error; - e('```'); - e('* The Bunyan CLI crashed!'); - e('*'); - if (err.name === 'ReferenceError' && gUsingConditionOpts) { - /* BEGIN JSSTYLED */ - e('* This crash was due to a "ReferenceError", which is often the result of given'); - e('* `-c CONDITION` code that doesn\'t guard against undefined values. If that is'); - /* END JSSTYLED */ - e('* not the problem:'); - e('*'); - } - e('* Please report this issue and include the details below:'); - e('*'); - e('* https://github.com/trentm/node-bunyan/issues/new?title=%s', title); - e('*'); - e('* * *'); - e('* platform:', process.platform); - e('* node version:', process.version); - e('* bunyan version:', getVersion()); - e('* argv: %j', process.argv); - e('* log line: %j', currLine); - e('* stack:'); - e(_indent(err.stack)); - e('```'); - process.exit(1); -}); - - -// Early termination of the pager: just stop. -function onPrematurePagerExit(pagerCode) { - _selfTrace('premature pager exit'); - // 'pager' and 'stdout' are intentionally global. - pager = null; - stdout.end() - stdout = process.stdout; - cleanupAndExit(pagerCode); -} - - -function main(argv) { - try { - var opts = parseArgv(argv); - } catch (e) { - warn('bunyan: error: %s', e.message); - cleanupAndExit(1); - return; - } - if (opts.help) { - printHelp(); - return; - } - if (opts.version) { - console.log('bunyan ' + getVersion()); - return; - } - if (opts.color === null) { - if (process.env.BUNYAN_NO_COLOR && - process.env.BUNYAN_NO_COLOR.length > 0) { - opts.color = false; - } else { - opts.color = process.stdout.isTTY; - } - } - usingAnsiCodes = opts.color; // intentionally global - var stylize = (opts.color ? stylizeWithColor : stylizeWithoutColor); - - // Pager. - var paginate = ( - process.stdout.isTTY && - process.stdin.isTTY && - opts.args.length > 0 && // Don't page if no file args to process. - process.platform !== 'win32' && - (nodeVer[0] > 0 || nodeVer[1] >= 8) && - (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'; - /* JSSTYLED */ - 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'; - } - _selfTrace('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; - pager.on('exit', onPrematurePagerExit); - } - - // Stdout error handling. (Couldn't setup until `stdout` was determined.) - stdout.on('error', function (err) { - _selfTrace('stdout error event: %s, exiting=%s', err, exiting); - if (exiting) { - return; - } else if (err.code === 'EPIPE') { - cleanupAndExit(0); - } else { - warn('bunyan: error on output stream: %s', err); - cleanupAndExit(1); - } - }); - - var retval = 0; - if (opts.args.length > 0) { - var files = opts.args; - files.forEach(function (file) { - streams[file] = { stream: null, records: [], done: false } - }); - asyncForEach(files, - function (file, next) { - processFile(file, opts, stylize, function (err) { - if (err) { - warn('bunyan: %s', err.message); - retval += 1; - } - next(); - }); - }, - function (err) { - if (err) { - warn('bunyan: unexpected error: %s', err.stack || err); - cleanupAndExit(1); - } else { - cleanupAndExit(retval); - } - } - ); - } else { - processStdin(opts, stylize, function () { - cleanupAndExit(retval); - }); - } -} - -if (require.main === module) { - // HACK guard for . - // We override the `process.stdout.end` guard that core node.js puts in - // place. The real fix is that `.end()` shouldn't be called on stdout - // in node core. Node v0.6.9 fixes that. Only guard for v0.6.0..v0.6.8. - if ([0, 6, 0] <= nodeVer && nodeVer <= [0, 6, 8]) { - var stdout = process.stdout; - stdout.end = stdout.destroy = stdout.destroySoon = function () { - /* pass */ - }; - } - - main(process.argv); -} diff --git a/bin/bunyan.mjs b/bin/bunyan.mjs new file mode 100644 index 0000000..4b943ea --- /dev/null +++ b/bin/bunyan.mjs @@ -0,0 +1,1397 @@ +#!/usr/bin/env node +/** + * Copyright 2017 Trent Mick + * Copyright 2017 Joyent Inc. + * + * bunyan -- filter and pretty-print Bunyan log files (line-delimited JSON) + * + * See . + * + * -*- mode: js -*- + * vim: expandtab:ts=4:sw=4 + */ + +var p = console.log; + +import util from 'util' +import vm from 'vm' +import http from 'http' +import path from 'path' +import fs from 'fs' +import { spawn } from 'child_process' +import { createGunzip } from 'zlib' +import { StringDecoder } from 'string_decoder' +import { fileURLToPath } from 'url' +import assert from 'assert' +var warn = console.warn; + +//---- globals and constants + +var nodeVer = process.versions.node.split('.').map(Number); +var nodeSpawnSupportsStdio = (nodeVer[0] > 0 || nodeVer[1] >= 8); + +// Internal debug logging via `console.warn`. +var _selfTrace = function selfTraceNoop() {}; +if (process.env.BUNYAN_SELF_TRACE === '1') { + _selfTrace = function selfTrace() { + process.stderr.write('[bunyan self-trace] '); + console.warn.apply(null, arguments); + } +} + +// Output modes. +var OM_LONG = 1; +var OM_JSON = 2; +var OM_INSPECT = 3; +var OM_SIMPLE = 4; +var OM_SHORT = 5; +var OM_BUNYAN = 6; +var OM_FROM_NAME = { + 'long': OM_LONG, + 'paul': OM_LONG, /* backward compat */ + 'json': OM_JSON, + 'inspect': OM_INSPECT, + 'simple': OM_SIMPLE, + 'short': OM_SHORT, + 'bunyan': OM_BUNYAN +}; + + +// Levels +var TRACE = 10; +var DEBUG = 20; +var INFO = 30; +var WARN = 40; +var ERROR = 50; +var FATAL = 60; + +var levelFromName = { + 'trace': TRACE, + 'debug': DEBUG, + 'info': INFO, + 'warn': WARN, + 'error': ERROR, + 'fatal': FATAL +}; +var nameFromLevel = {}; +var upperNameFromLevel = {}; +var upperPaddedNameFromLevel = {}; +Object.keys(levelFromName).forEach(function (name) { + var lvl = levelFromName[name]; + nameFromLevel[lvl] = name; + upperNameFromLevel[lvl] = name.toUpperCase(); + upperPaddedNameFromLevel[lvl] = ( + name.length === 4 ? ' ' : '') + name.toUpperCase(); +}); + + +// Display time formats. +var TIME_UTC = 1; // the default, bunyan's native format +var TIME_LOCAL = 2; + + +// Boolean set to true when we are in the process of exiting. We don't always +// hard-exit (e.g. when staying alive while the pager completes). +var exiting = false; + +// The current raw input line being processed. Used for `uncaughtException`. +var currLine = null; + +// Whether ANSI codes are being used. Used for signal-handling. +var usingAnsiCodes = false; + +// Used to tell the 'uncaughtException' handler that '-c CODE' is being used. +var gUsingConditionOpts = false; + +// Pager child process, and output stream to which to write. +var pager = null; +var stdout = process.stdout; + + + +//---- support functions + +var _version = null; +function getVersion() { + if (_version === null) { + let __dirname = path.dirname(fileURLToPath(import.meta.url)) + let pckg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'))) + _version = pckg.version + } + return _version; +} + + +var format = util.format; +if (!format) { + /* BEGIN JSSTYLED */ + // If not node 0.6, then use its `util.format`: + // : + var inspect = util.inspect; + var formatRegExp = /%[sdj%]/g; + format = function format(f) { + if (typeof f !== 'string') { + var objects = []; + for (var i = 0; i < arguments.length; i++) { + objects.push(inspect(arguments[i])); + } + return objects.join(' '); + } + + var i = 1; + var args = arguments; + var len = args.length; + var str = String(f).replace(formatRegExp, function (x) { + if (i >= len) + return x; + switch (x) { + case '%s': return String(args[i++]); + case '%d': return Number(args[i++]); + case '%j': return JSON.stringify(args[i++]); + case '%%': return '%'; + default: + return x; + } + }); + for (var x = args[i]; i < len; x = args[++i]) { + if (x === null || typeof x !== 'object') { + str += ' ' + x; + } else { + str += ' ' + inspect(x); + } + } + return str; + }; + /* END JSSTYLED */ +} + +function indent(s) { + return ' ' + s.split(/\r?\n/).join('\n '); +} + +function objCopy(obj) { + if (obj === null) { + return null; + } else if (Array.isArray(obj)) { + return obj.slice(); + } else { + var copy = {}; + Object.keys(obj).forEach(function (k) { + copy[k] = obj[k]; + }); + return copy; + } +} + +function printHelp() { + /* BEGIN JSSTYLED */ + p('Usage:'); + p(' bunyan [OPTIONS] [FILE ...]'); + p(' ... | bunyan [OPTIONS]'); + p(' bunyan [OPTIONS] -p PID'); + p(''); + p('Filter and pretty-print Bunyan log file content.'); + p(''); + p('General options:'); + p(' -h, --help print this help info and exit'); + p(' --version print version of this command and exit'); + p(''); + p('Filtering options:'); + p(' -l, --level LEVEL'); + p(' Only show messages at or above the specified level.'); + p(' You can specify level *names* or the internal numeric'); + p(' values.'); + p(' -c, --condition CONDITION'); + p(' Run each log message through the condition and'); + p(' only show those that return truish. E.g.:'); + p(' -c \'this.pid == 123\''); + p(' -c \'this.level == DEBUG\''); + p(' -c \'this.msg.indexOf("boom") != -1\''); + p(' "CONDITION" must be legal JS code. `this` holds'); + p(' the log record. The TRACE, DEBUG, ... FATAL values'); + p(' are defined to help with comparing `this.level`.'); + p(' --strict Suppress all but legal Bunyan JSON log lines. By default'); + p(' non-JSON, and non-Bunyan lines are passed through.'); + p(''); + p('Output options:'); + p(' --pager Pipe output into `less` (or $PAGER if set), if'); + p(' stdout is a TTY. This overrides $BUNYAN_NO_PAGER.'); + p(' Note: Paging is only supported on node >=0.8.'); + p(' --no-pager Do not pipe output into a pager.'); + p(' --color Colorize output. Defaults to try if output'); + p(' stream is a TTY.'); + p(' --no-color Force no coloring (e.g. terminal doesn\'t support it)'); + p(' -o, --output MODE'); + p(' Specify an output mode/format. One of'); + p(' long: (the default) pretty'); + p(' json: JSON output, 2-space indent'); + p(' json-N: JSON output, N-space indent, e.g. "json-4"'); + p(' bunyan: 0 indented JSON, bunyan\'s native format'); + p(' inspect: node.js `util.inspect` output'); + p(' short: like "long", but more concise'); + p(' simple: level, followed by "-" and then the message'); + p(' -j shortcut for `-o json`'); + p(' -0 shortcut for `-o bunyan`'); + p(''); + p('Environment Variables:'); + p(' BUNYAN_NO_COLOR Set to a non-empty value to force no output '); + p(' coloring. See "--no-color".'); + p(' BUNYAN_NO_PAGER Disable piping output to a pager. '); + p(' See "--no-pager".'); + p(''); + p('See for more complete docs.'); + p('Please report bugs to .'); + /* END JSSTYLED */ +} + +/* + * If the user specifies multiple input sources, we want to print out records + * from all sources in a single, chronologically ordered stream. To do this + * efficiently, we first assume that all records within each source are ordered + * already, so we need only keep track of the next record in each source and + * the time of the last record emitted. To avoid excess memory usage, we + * pause() streams that are ahead of others. + * + * 'streams' is an object indexed by source name (file name) which specifies: + * + * stream Actual stream object, so that we can pause and resume it. + * + * records Array of log records we've read, but not yet emitted. Each + * record includes 'line' (the raw line), 'rec' (the JSON + * record), and 'time' (the parsed time value). + * + * done Whether the stream has any more records to emit. + */ +var streams = {}; + +function gotRecord(file, line, rec, opts, stylize) +{ + var time = new Date(rec.time); + + streams[file]['records'].push({ line: line, rec: rec, time: time }); + emitNextRecord(opts, stylize); +} + +function filterRecord(rec, opts) +{ + if (opts.level && rec.level < opts.level) { + return false; + } + + if (opts.condFuncs) { + var recCopy = objCopy(rec); + for (var i = 0; i < opts.condFuncs.length; i++) { + var pass = opts.condFuncs[i].call(recCopy); + if (!pass) + return false; + } + } else if (opts.condVm) { + for (var i = 0; i < opts.condVm.length; i++) { + var pass = opts.condVm[i].runInNewContext(rec); + if (!pass) + return false; + } + } + + return true; +} + +function emitNextRecord(opts, stylize) +{ + var ofile, ready, minfile, rec; + + for (;;) { + /* + * Take a first pass through the input streams to see if we have a + * record from all of them. If not, we'll pause any streams for + * which we do already have a record (to avoid consuming excess + * memory) and then wait until we have records from the others + * before emitting the next record. + * + * As part of the same pass, we look for the earliest record + * we have not yet emitted. + */ + minfile = undefined; + ready = true; + for (ofile in streams) { + + if (streams[ofile].stream === null || + (!streams[ofile].done && streams[ofile].records.length === 0)) { + ready = false; + break; + } + + if (streams[ofile].records.length > 0 && + (minfile === undefined || + streams[minfile].records[0].time > + streams[ofile].records[0].time)) { + minfile = ofile; + } + } + + if (!ready || minfile === undefined) { + for (ofile in streams) { + if (!streams[ofile].stream || streams[ofile].done) + continue; + + if (streams[ofile].records.length > 0) { + if (!streams[ofile].paused) { + streams[ofile].paused = true; + streams[ofile].stream.pause(); + } + } else if (streams[ofile].paused) { + streams[ofile].paused = false; + streams[ofile].stream.resume(); + } + } + + return; + } + + /* + * Emit the next record for 'minfile', and invoke ourselves again to + * make sure we emit as many records as we can right now. + */ + rec = streams[minfile].records.shift(); + emitRecord(rec.rec, rec.line, opts, stylize); + } +} + +/** + * Return a function for the given JS code that returns. + * + * If no 'return' in the given javascript snippet, then assume we are a single + * statement and wrap in 'return (...)'. This is for convenience for short + * '-c ...' snippets. + */ +function funcWithReturnFromSnippet(js) { + // auto-"return" + if (js.indexOf('return') === -1) { + if (js.substring(js.length - 1) === ';') { + js = js.substring(0, js.length - 1); + } + js = 'return (' + js + ')'; + } + + // Expose level definitions to condition func context + var varDefs = []; + Object.keys(upperNameFromLevel).forEach(function (lvl) { + varDefs.push(format('var %s = %d;', + upperNameFromLevel[lvl], lvl)); + }); + varDefs = varDefs.join('\n') + '\n'; + + return (new Function(varDefs + js)); +} + +/** + * Parse the command-line options and arguments into an object. + * + * { + * 'args': [...] // arguments + * 'help': true, // true if '-h' option given + * // etc. + * } + * + * @return {Object} The parsed options. `.args` is the argument list. + * @throws {Error} If there is an error parsing argv. + */ +function parseArgv(argv) { + var parsed = { + args: [], + help: false, + color: null, + paginate: null, + outputMode: OM_LONG, + jsonIndent: 2, + level: null, + strict: false, + timeFormat: TIME_UTC // one of the TIME_ constants + }; + + // Turn '-iH' into '-i -H', except for argument-accepting options. + var args = argv.slice(2); // drop ['node', 'scriptname'] + var newArgs = []; + var optTakesArg = {'d': true, 'o': true, 'c': true, 'l': true, 'p': true}; + for (var i = 0; i < args.length; i++) { + if (args[i].charAt(0) === '-' && args[i].charAt(1) !== '-' && + args[i].length > 2) + { + var splitOpts = args[i].slice(1).split(''); + for (var j = 0; j < splitOpts.length; j++) { + newArgs.push('-' + splitOpts[j]); + if (optTakesArg[splitOpts[j]]) { + var optArg = splitOpts.slice(j+1).join(''); + if (optArg.length) { + newArgs.push(optArg); + } + break; + } + } + } else { + newArgs.push(args[i]); + } + } + args = newArgs; + + // Expose level definitions to condition vm context + var condDefines = []; + Object.keys(upperNameFromLevel).forEach(function (lvl) { + condDefines.push( + format('Object.prototype.%s = %s;', upperNameFromLevel[lvl], lvl)); + }); + condDefines = condDefines.join('\n') + '\n'; + + var endOfOptions = false; + while (args.length > 0) { + var arg = args.shift(); + switch (arg) { + case '--': + endOfOptions = true; + break; + case '-h': // display help and exit + case '--help': + parsed.help = true; + break; + case '--version': + parsed.version = true; + break; + case '--strict': + parsed.strict = true; + break; + case '--color': + parsed.color = true; + break; + case '--no-color': + parsed.color = false; + break; + case '--pager': + parsed.paginate = true; + break; + case '--no-pager': + parsed.paginate = false; + break; + case '-o': + case '--output': + var name = args.shift(); + var idx = name.lastIndexOf('-'); + if (idx !== -1) { + var indentation = Number(name.slice(idx+1)); + if (! isNaN(indentation)) { + parsed.jsonIndent = indentation; + name = name.slice(0, idx); + } + } + parsed.outputMode = OM_FROM_NAME[name]; + if (parsed.outputMode === undefined) { + throw new Error('unknown output mode: "'+name+'"'); + } + break; + case '-j': // output with JSON.stringify + parsed.outputMode = OM_JSON; + break; + case '-0': + parsed.outputMode = OM_BUNYAN; + break; + case '--time': + var timeArg = args.shift(); + switch (timeArg) { + case 'utc': + parsed.timeFormat = TIME_UTC; + break + case undefined: + throw new Error('missing argument to "--time"'); + default: + throw new Error(format('invalid time format: "%s"', + timeArg)); + } + break; + case '-l': + case '--level': + var levelArg = args.shift(); + var level = +(levelArg); + if (isNaN(level)) { + level = +levelFromName[levelArg.toLowerCase()]; + } + if (isNaN(level)) { + throw new Error('unknown level value: "'+levelArg+'"'); + } + parsed.level = level; + break; + case '-c': + case '--condition': + gUsingConditionOpts = true; + var condition = args.shift(); + if (Boolean(process.env.BUNYAN_EXEC && + process.env.BUNYAN_EXEC === 'vm')) + { + parsed.condVm = parsed.condVm || []; + var scriptName = 'bunyan-condition-'+parsed.condVm.length; + var code = condDefines + condition; + var script; + try { + script = vm.createScript(code, scriptName); + } catch (complErr) { + throw new Error(format('illegal CONDITION code: %s\n' + + ' CONDITION script:\n' + + '%s\n' + + ' Error:\n' + + '%s', + complErr, indent(code), indent(complErr.stack))); + } + + // Ensure this is a reasonably safe CONDITION. + try { + script.runInNewContext(minValidRecord); + } catch (condErr) { + throw new Error(format( + /* JSSTYLED */ + 'CONDITION code cannot safely filter a minimal Bunyan log record\n' + + ' CONDITION script:\n' + + '%s\n' + + ' Minimal Bunyan log record:\n' + + '%s\n' + + ' Filter error:\n' + + '%s', + indent(code), + indent(JSON.stringify(minValidRecord, null, 2)), + indent(condErr.stack) + )); + } + parsed.condVm.push(script); + } else { + parsed.condFuncs = parsed.condFuncs || []; + parsed.condFuncs.push(funcWithReturnFromSnippet(condition)); + } + break; + default: // arguments + if (!endOfOptions && arg.length > 0 && arg[0] === '-') { + throw new Error('unknown option "'+arg+'"'); + } + parsed.args.push(arg); + break; + } + } + //TODO: '--' handling and error on a first arg that looks like an option. + + return parsed; +} + + +function isInteger(s) { + return (s.search(/^-?[0-9]+$/) == 0); +} + + +// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics +// Suggested colors (some are unreadable in common cases): +// - Good: cyan, yellow (limited use), bold, green, magenta, red +// - Bad: blue (not visible on cmd.exe), grey (same color as background on +// Solarized Dark theme from , see +// issue #160) +var colors = { + 'bold' : [1, 22], + 'italic' : [3, 23], + 'underline' : [4, 24], + 'inverse' : [7, 27], + 'white' : [37, 39], + 'grey' : [90, 39], + 'black' : [30, 39], + 'blue' : [34, 39], + 'cyan' : [36, 39], + 'green' : [32, 39], + 'magenta' : [35, 39], + 'red' : [31, 39], + 'yellow' : [33, 39] +}; + +function stylizeWithColor(str, color) { + if (!str) + return ''; + var codes = colors[color]; + if (codes) { + return '\x33[' + codes[0] + 'm' + str + + '\x33[' + codes[1] + 'm'; + } else { + return str; + } +} + +function stylizeWithoutColor(str, color) { + return str; +} + + +/** + * Is this a valid Bunyan log record. + */ +function isValidRecord(rec) { + if (rec.v == null || + rec.level == null || + rec.name == null || + rec.hostname == null || + rec.pid == null || + rec.time == null || + rec.msg == null) { + // Not valid Bunyan log. + return false; + } else { + return true; + } +} +var minValidRecord = { + v: 0, //TODO: get this from bunyan.LOG_VERSION + level: INFO, + name: 'name', + hostname: 'hostname', + pid: 123, + time: Date.now(), + msg: 'msg' +}; + + +/** + * Parses the given log line and either emits it right away (for invalid + * records) or enqueues it for emitting later when it's the next line to show. + */ +function handleLogLine(file, line, opts, stylize) { + if (exiting) { + _selfTrace('warn: handleLogLine called while exiting'); + return; + } + + currLine = line; // intentionally global + + // Emit non-JSON lines immediately. + var rec; + if (!line) { + if (!opts.strict) emit(line + '\n'); + return; + } else if (line[0] !== '{') { + if (!opts.strict) emit(line + '\n'); // not JSON + return; + } else { + try { + rec = JSON.parse(line); + } catch (e) { + if (!opts.strict) emit(line + '\n'); + return; + } + } + + if (!isValidRecord(rec)) { + if (!opts.strict) emit(line + '\n'); + return; + } + + if (!filterRecord(rec, opts)) + return; + + if (file === null) + return emitRecord(rec, line, opts, stylize); + + return gotRecord(file, line, rec, opts, stylize); +} + +/** + * Print out a single result, considering input options. + */ +function emitRecord(rec, line, opts, stylize) { + var short = false; + + switch (opts.outputMode) { + case OM_SHORT: + short = true; + /* jsl:fall-thru */ + + case OM_LONG: + // [time] LEVEL: name[/comp]/pid on hostname (src): msg* (extras...) + // msg* + // -- + // long and multi-line extras + // ... + // If 'msg' is single-line, then it goes in the top line. + // If 'req', show the request. + // If 'res', show the response. + // If 'err' and 'err.stack' then show that. + if (!isValidRecord(rec)) { + return emit(line + '\n'); + } + + delete rec.v; + + // Time. + var time; + if (!short && opts.timeFormat === TIME_UTC) { + // Fast default path: We assume the raw `rec.time` is a UTC time + // in ISO 8601 format (per spec). + time = '[' + rec.time + ']'; + } else if (opts.timeFormat === TIME_UTC) { + time = rec.time.substr(11); + } + time = stylize(time, 'none'); + delete rec.time; + + var nameStr = rec.name; + delete rec.name; + + if (rec.component) { + nameStr += '/' + rec.component; + } + delete rec.component; + + if (!short) + nameStr += '/' + rec.pid; + delete rec.pid; + + var level = (upperPaddedNameFromLevel[rec.level] || 'LVL' + rec.level); + if (opts.color) { + var colorFromLevel = { + 10: 'white', // TRACE + 20: 'yellow', // DEBUG + 30: 'cyan', // INFO + 40: 'magenta', // WARN + 50: 'red', // ERROR + 60: 'inverse', // FATAL + }; + level = stylize(level, colorFromLevel[rec.level]); + } + delete rec.level; + + var src = ''; + if (rec.src && rec.src.file) { + var s = rec.src; + if (s.func) { + src = format(' (%s:%d in %s)', s.file, s.line, s.func); + } else { + src = format(' (%s:%d)', s.file, s.line); + } + src = stylize(src, 'green'); + } + delete rec.src; + + var hostname = rec.hostname; + delete rec.hostname; + + var extras = []; + var details = []; + + if (rec.req_id) { + extras.push('req_id=' + rec.req_id); + } + delete rec.req_id; + + var onelineMsg; + if (rec.msg.indexOf('\n') !== -1) { + onelineMsg = ''; + details.push(indent(stylize(rec.msg, 'cyan'))); + } else { + onelineMsg = ' ' + stylize(rec.msg, 'cyan'); + } + delete rec.msg; + + if (rec.req && typeof (rec.req) === 'object') { + var req = rec.req; + delete rec.req; + var headers = req.headers; + if (!headers) { + headers = ''; + } else if (typeof (headers) === 'string') { + headers = '\n' + headers; + } else if (typeof (headers) === 'object') { + headers = '\n' + Object.keys(headers).map(function (h) { + return h + ': ' + headers[h]; + }).join('\n'); + } + var s = format('%s %s HTTP/%s%s', req.method, + req.url, + req.httpVersion || '1.1', + headers + ); + delete req.url; + delete req.method; + delete req.httpVersion; + delete req.headers; + if (req.body) { + s += '\n\n' + (typeof (req.body) === 'object' + ? JSON.stringify(req.body, null, 2) : req.body); + delete req.body; + } + if (req.trailers && Object.keys(req.trailers) > 0) { + s += '\n' + Object.keys(req.trailers).map(function (t) { + return t + ': ' + req.trailers[t]; + }).join('\n'); + } + delete req.trailers; + details.push(indent(s)); + // E.g. for extra 'foo' field on 'req', add 'req.foo' at + // top-level. This *does* have the potential to stomp on a + // literal 'req.foo' key. + Object.keys(req).forEach(function (k) { + rec['req.' + k] = req[k]; + }) + } + + if (rec.client_req && typeof (rec.client_req) === 'object') { + var client_req = rec.client_req; + delete rec.client_req; + + var headers = client_req.headers; + delete client_req.headers; + + var s = format('%s %s HTTP/%s%s', + client_req.method, + client_req.url, + client_req.httpVersion || '1.1', + (headers ? + '\n' + Object.keys(headers).map( + function (h) { + return h + ': ' + headers[h]; + }).join('\n') : + '')); + delete client_req.method; + delete client_req.url; + delete client_req.httpVersion; + + if (client_req.body) { + s += '\n\n' + (typeof (client_req.body) === 'object' ? + JSON.stringify(client_req.body, null, 2) : + client_req.body); + delete client_req.body; + } + // E.g. for extra 'foo' field on 'client_req', add + // 'client_req.foo' at top-level. This *does* have the potential + // to stomp on a literal 'client_req.foo' key. + Object.keys(client_req).forEach(function (k) { + rec['client_req.' + k] = client_req[k]; + }); + details.push(indent(s)); + } + + function _res(res) { + var s = ''; + + /* + * Handle `res.header` or `res.headers` as either a string or + * an object of header key/value pairs. Prefer `res.header` if set, + * because that's what Bunyan's own `res` serializer specifies, + * because that's the value in Node.js's core HTTP server response + * implementation that has all the implicit headers. + * + * Note: `res.header` (string) typically includes the 'HTTP/1.1 ...' + * status line. + */ + var headerTypes = {string: true, object: true}; + var headers; + var headersStr = ''; + var headersHaveStatusLine = false; + if (res.header && headerTypes[typeof (res.header)]) { + headers = res.header; + delete res.header; + } else if (res.headers && headerTypes[typeof (res.headers)]) { + headers = res.headers; + delete res.headers; + } + if (headers === undefined) { + /* pass through */ + } else if (typeof (headers) === 'string') { + headersStr = headers.trimRight(); // Trim the CRLF. + if (headersStr.slice(0, 5) === 'HTTP/') { + headersHaveStatusLine = true; + } + } else { + headersStr += Object.keys(headers).map( + function (h) { return h + ': ' + headers[h]; }).join('\n'); + } + + /* + * Add a 'HTTP/1.1 ...' status line if the headers didn't already + * include it. + */ + if (!headersHaveStatusLine && res.statusCode !== undefined) { + s += format('HTTP/1.1 %s %s\n', res.statusCode, + http.STATUS_CODES[res.statusCode]); + } + delete res.statusCode; + s += headersStr; + + if (res.body !== undefined) { + var body = (typeof (res.body) === 'object' + ? JSON.stringify(res.body, null, 2) : res.body); + if (body.length > 0) { s += '\n\n' + body }; + delete res.body; + } else { + s = s.trimRight(); + } + if (res.trailer) { + s += '\n' + res.trailer; + } + delete res.trailer; + if (s) { + details.push(indent(s)); + } + // E.g. for extra 'foo' field on 'res', add 'res.foo' at + // top-level. This *does* have the potential to stomp on a + // literal 'res.foo' key. + Object.keys(res).forEach(function (k) { + rec['res.' + k] = res[k]; + }); + } + + if (rec.res && typeof (rec.res) === 'object') { + _res(rec.res); + delete rec.res; + } + if (rec.client_res && typeof (rec.client_res) === 'object') { + _res(rec.client_res); + delete rec.client_res; + } + + if (rec.err && rec.err.stack) { + var err = rec.err + if (typeof (err.stack) !== 'string') { + details.push(indent(err.stack.toString())); + } else { + details.push(indent(err.stack)); + } + delete err.message; + delete err.name; + delete err.stack; + // E.g. for extra 'foo' field on 'err', add 'err.foo' at + // top-level. This *does* have the potential to stomp on a + // literal 'err.foo' key. + Object.keys(err).forEach(function (k) { + rec['err.' + k] = err[k]; + }) + delete rec.err; + } + + var leftover = Object.keys(rec); + for (var i = 0; i < leftover.length; i++) { + var key = leftover[i]; + var value = rec[key]; + var stringified = false; + if (typeof (value) !== 'string') { + value = JSON.stringify(value, null, 2); + stringified = true; + } + if (value.indexOf('\n') !== -1 || value.length > 50) { + details.push(indent(key + ': ' + value)); + } else if (!stringified && (value.indexOf(' ') != -1 || + value.length === 0)) + { + extras.push(key + '=' + JSON.stringify(value)); + } else { + extras.push(key + '=' + value); + } + } + + extras = stylize( + (extras.length ? ' (' + extras.join(', ') + ')' : ''), 'none'); + details = stylize( + (details.length ? details.join('\n --\n') + '\n' : ''), 'none'); + if (!short) + emit(format('%s %s: %s on %s%s:%s%s\n%s', + time, + level, + nameStr, + hostname || '', + src, + onelineMsg, + extras, + details)); + else + emit(format('%s %s %s:%s%s\n%s', + time, + level, + nameStr, + onelineMsg, + extras, + details)); + break; + + case OM_INSPECT: + emit(util.inspect(rec, false, Infinity, true) + '\n'); + break; + + case OM_BUNYAN: + emit(JSON.stringify(rec, null, 0) + '\n'); + break; + + case OM_JSON: + emit(JSON.stringify(rec, null, opts.jsonIndent) + '\n'); + break; + + case OM_SIMPLE: + /* JSSTYLED */ + // + if (!isValidRecord(rec)) { + return emit(line + '\n'); + } + emit(format('%s - %s\n', + upperNameFromLevel[rec.level] || 'LVL' + rec.level, + rec.msg)); + break; + default: + throw new Error('unknown output mode: '+opts.outputMode); + } +} + + +function emit(s) { + try { + stdout.write(s); + } catch (writeErr) { + _selfTrace('exception from stdout.write:', writeErr) + // Handle any exceptions in stdout writing in `stdout.on('error', ...)`. + } +} + + +/** + * Process all input from stdin. + * + * @params opts {Object} Bunyan options object. + * @param stylize {Function} Output stylize function to use. + * @param callback {Function} `function ()` + */ +function processStdin(opts, stylize, callback) { + var leftover = ''; // Left-over partial line from last chunk. + var stdin = process.stdin; + stdin.resume(); + stdin.setEncoding('utf8'); + stdin.on('data', function (chunk) { + var lines = chunk.split(/\r\n|\n/); + var length = lines.length; + if (length === 1) { + leftover += lines[0]; + return; + } + + if (length > 1) { + handleLogLine(null, leftover + lines[0], opts, stylize); + } + leftover = lines.pop(); + length -= 1; + for (var i = 1; i < length; i++) { + handleLogLine(null, lines[i], opts, stylize); + } + }); + stdin.on('end', function () { + if (leftover) { + handleLogLine(null, leftover, opts, stylize); + leftover = ''; + } + callback(); + }); +} + + +/** + * Process all input from the given log file. + * + * @param file {String} Log file path to process. + * @params opts {Object} Bunyan options object. + * @param stylize {Function} Output stylize function to use. + * @param callback {Function} `function ()` + */ +function processFile(file, opts, stylize, callback) { + var stream = fs.createReadStream(file); + if (/\.gz$/.test(file)) { + stream = stream.pipe(createGunzip()); + } + // Manually decode streams - lazy load here as per node/lib/fs.js + var decoder = new StringDecoder('utf8'); + + streams[file].stream = stream; + + stream.on('error', function (err) { + streams[file].done = true; + callback(err); + }); + + var leftover = ''; // Left-over partial line from last chunk. + stream.on('data', function (data) { + if (exiting) { + _selfTrace('stop reading file "%s" because exiting', file); + stream.destroy(); + return; + } + + var chunk = decoder.write(data); + if (!chunk.length) { + return; + } + var lines = chunk.split(/\r\n|\n/); + var length = lines.length; + if (length === 1) { + leftover += lines[0]; + return; + } + + if (length > 1) { + handleLogLine(file, leftover + lines[0], opts, stylize); + } + leftover = lines.pop(); + length -= 1; + for (var i = 1; i < length; i++) { + handleLogLine(file, lines[i], opts, stylize); + } + }); + + stream.on('end', function () { + streams[file].done = true; + if (leftover) { + handleLogLine(file, leftover, opts, stylize); + leftover = ''; + } else { + emitNextRecord(opts, stylize); + } + callback(); + }); +} + + +/** + * From node async module. + */ +/* BEGIN JSSTYLED */ +function asyncForEach(arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + arr.forEach(function (x) { + iterator(x, function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed === arr.length) { + callback(); + } + } + }); + }); +}; +/* END JSSTYLED */ + + + +/** + * Cleanup and exit properly. + * + * Warning: this doesn't necessarily stop processing, i.e. process exit + * might be delayed. It is up to the caller to ensure that no subsequent + * bunyan processing is done after calling this. + * + * @param code {Number} exit code. + * @param signal {String} Optional signal name, if this was exitting because + * of a signal. + */ +function cleanupAndExit(code, signal) { + // Guard one call. + if (exiting) { + return; + } + exiting = true; + _selfTrace('cleanupAndExit(%s, %s)', code, signal); + + // Clear possibly interrupted ANSI code (issue #59). + if (usingAnsiCodes) { + stdout.write('\x33[0m'); + } + + if (pager) { + // Let pager know that output is done, then wait for pager to exit. + pager.removeListener('exit', onPrematurePagerExit); + pager.on('exit', function onPagerExit(pagerCode) { + _selfTrace('pager exit -> process.exit(%s)', pagerCode || code); + process.exit(pagerCode || code); + }); + stdout.end(); + } else if (code) { + // Non-zero exit: Something is wrong. We are very likely still + // processing log records -- i.e. we have open handles -- so we need + // a hard stop (aka `process.exit`). + _selfTrace('process.exit(%s)', code); + process.exit(code); + } else { + // Zero exit: This should be a "normal" exit, for which we want to + // flush stdout/stderr. + process.exit(0); + } +} + + + +//---- 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) { + function _indent(s) { + var lines = s.split(/\r?\n/); + for (var i = 0; i < lines.length; i++) { + lines[i] = '* ' + lines[i]; + } + return lines.join('\n'); + } + + var title = encodeURIComponent(format( + 'Bunyan %s crashed: %s', getVersion(), String(err))); + var e = console.error; + e('```'); + e('* The Bunyan CLI crashed!'); + e('*'); + if (err.name === 'ReferenceError' && gUsingConditionOpts) { + /* BEGIN JSSTYLED */ + e('* This crash was due to a "ReferenceError", which is often the result of given'); + e('* `-c CONDITION` code that doesn\'t guard against undefined values. If that is'); + /* END JSSTYLED */ + e('* not the problem:'); + e('*'); + } + e('* Please report this issue and include the details below:'); + e('*'); + e('* https://github.com/trentm/node-bunyan/issues/new?title=%s', title); + e('*'); + e('* * *'); + e('* platform:', process.platform); + e('* node version:', process.version); + e('* bunyan version:', getVersion()); + e('* argv: %j', process.argv); + e('* log line: %j', currLine); + e('* stack:'); + e(_indent(err.stack)); + e('```'); + process.exit(1); +}); + + +// Early termination of the pager: just stop. +function onPrematurePagerExit(pagerCode) { + _selfTrace('premature pager exit'); + // 'pager' and 'stdout' are intentionally global. + pager = null; + stdout.end() + stdout = process.stdout; + cleanupAndExit(pagerCode); +} + + +function main(argv) { + try { + var opts = parseArgv(argv); + } catch (e) { + warn('bunyan: error: %s', e.message); + cleanupAndExit(1); + return; + } + if (opts.help) { + printHelp(); + return; + } + if (opts.version) { + console.log('bunyan ' + getVersion()); + return; + } + if (opts.color === null) { + if (process.env.BUNYAN_NO_COLOR && + process.env.BUNYAN_NO_COLOR.length > 0) { + opts.color = false; + } else { + opts.color = process.stdout.isTTY; + } + } + usingAnsiCodes = opts.color; // intentionally global + var stylize = (opts.color ? stylizeWithColor : stylizeWithoutColor); + + // Pager. + var paginate = ( + process.stdout.isTTY && + process.stdin.isTTY && + opts.args.length > 0 && // Don't page if no file args to process. + process.platform !== 'win32' && + (nodeVer[0] > 0 || nodeVer[1] >= 8) && + (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'; + /* JSSTYLED */ + 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'; + } + _selfTrace('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; + pager.on('exit', onPrematurePagerExit); + } + + // Stdout error handling. (Couldn't setup until `stdout` was determined.) + stdout.on('error', function (err) { + _selfTrace('stdout error event: %s, exiting=%s', err, exiting); + if (exiting) { + return; + } else if (err.code === 'EPIPE') { + cleanupAndExit(0); + } else { + warn('bunyan: error on output stream: %s', err); + cleanupAndExit(1); + } + }); + + var retval = 0; + if (opts.args.length > 0) { + var files = opts.args; + files.forEach(function (file) { + streams[file] = { stream: null, records: [], done: false } + }); + asyncForEach(files, + function (file, next) { + processFile(file, opts, stylize, function (err) { + if (err) { + warn('bunyan: %s', err.message); + retval += 1; + } + next(); + }); + }, + function (err) { + if (err) { + warn('bunyan: unexpected error: %s', err.stack || err); + cleanupAndExit(1); + } else { + cleanupAndExit(retval); + } + } + ); + } else { + processStdin(opts, stylize, function () { + cleanupAndExit(retval); + }); + } +} + +main(process.argv); diff --git a/lib/bunyan.js b/lib/bunyan.js deleted file mode 100644 index 573cfd0..0000000 --- a/lib/bunyan.js +++ /dev/null @@ -1,1585 +0,0 @@ -/** - * Copyright (c) 2017 Trent Mick. - * Copyright (c) 2017 Joyent Inc. - * - * The bunyan logging library for node.js. - * - * -*- mode: js -*- - * vim: expandtab:ts=4:sw=4 - */ - -/* - * Bunyan log format version. This becomes the 'v' field on all log records. - * This will be incremented if there is any backward incompatible change to - * the log record format. Details will be in 'CHANGES.md' (the change log). - */ -var LOG_VERSION = 0; - - -var xxx = function xxx(s) { // internal dev/debug logging - var args = ['XX' + 'X: '+s].concat( - Array.prototype.slice.call(arguments, 1)); - console.error.apply(this, args); -}; -var xxx = function xxx() {}; // comment out to turn on debug logging - - -/* - * Runtime environment notes: - * - * Bunyan is intended to run in a number of runtime environments. Here are - * some notes on differences for those envs and how the code copes. - * - * - node.js: The primary target environment. - * - NW.js: http://nwjs.io/ An *app* environment that feels like both a - * node env -- it has node-like globals (`process`, `global`) and - * browser-like globals (`window`, `navigator`). My *understanding* is that - * bunyan can operate as if this is vanilla node.js. - * - browser: Failing the above, we sniff using the `window` global - * . - * - browserify: http://browserify.org/ A browser-targetting bundler of - * node.js deps. The runtime is a browser env, so can't use fs access, - * etc. Browserify's build looks for `require()` imports - * to bundle. For some imports it won't be able to handle, we "hide" - * from browserify with `require('frobshizzle' + '')`. - * - Other? Please open issues if things are broken. - */ -var runtimeEnv; -if (typeof (process) !== 'undefined' && process.versions) { - if (process.versions.nw) { - runtimeEnv = 'nw'; - } else if (process.versions.node) { - runtimeEnv = 'node'; - } -} -if (!runtimeEnv && typeof (window) !== 'undefined' && - window.window === window) { - runtimeEnv = 'browser'; -} -if (!runtimeEnv) { - throw new Error('unknown runtime environment'); -} - - -var os, fs; -if (runtimeEnv === 'browser') { - os = { - hostname: function () { - return window.location.host; - } - }; - fs = {}; -} else { - os = require('os'); - fs = require('fs'); -} -var util = require('util'); -var assert = require('assert'); -var EventEmitter = require('events').EventEmitter; -var stream = require('stream'); - -try { - var safeJsonStringify = require('safe-json-stringify'); -} catch (e) { - safeJsonStringify = null; -} -if (process.env.BUNYAN_TEST_NO_SAFE_JSON_STRINGIFY) { - safeJsonStringify = null; -} - -// The 'mv' module is required for rotating-file stream support. -try { - var mv = require('mv-lite' + ''); -} catch (e) { - mv = null; -} - -try { - var sourceMapSupport = require('source-map-support' + ''); -} catch (_) { - sourceMapSupport = null; -} - - -//---- Internal support stuff - -/** - * A shallow copy of an object. Bunyan logging attempts to never cause - * exceptions, so this function attempts to handle non-objects gracefully. - */ -function objCopy(obj) { - if (obj == null) { // null or undefined - return obj; - } else if (Array.isArray(obj)) { - return obj.slice(); - } else if (typeof (obj) === 'object') { - var copy = {}; - Object.keys(obj).forEach(function (k) { - copy[k] = obj[k]; - }); - return copy; - } else { - return obj; - } -} - -var format = util.format; -if (!format) { - // If node < 0.6, then use its `util.format`: - // : - var inspect = util.inspect; - var formatRegExp = /%[sdj%]/g; - format = function format(f) { - if (typeof (f) !== 'string') { - var objects = []; - for (var i = 0; i < arguments.length; i++) { - objects.push(inspect(arguments[i])); - } - return objects.join(' '); - } - - var i = 1; - var args = arguments; - var len = args.length; - var str = String(f).replace(formatRegExp, function (x) { - if (i >= len) - return x; - switch (x) { - case '%s': return String(args[i++]); - case '%d': return Number(args[i++]); - case '%j': return fastAndSafeJsonStringify(args[i++]); - case '%%': return '%'; - default: - return x; - } - }); - for (var x = args[i]; i < len; x = args[++i]) { - if (x === null || typeof (x) !== 'object') { - str += ' ' + x; - } else { - str += ' ' + inspect(x); - } - } - return str; - }; -} - - -/** - * Gather some caller info 3 stack levels up. - * See . - */ -function getCaller3Info() { - if (this === undefined) { - // Cannot access caller info in 'strict' mode. - return; - } - var obj = {}; - var saveLimit = Error.stackTraceLimit; - var savePrepare = Error.prepareStackTrace; - Error.stackTraceLimit = 3; - - Error.prepareStackTrace = function (_, stack) { - var caller = stack[2]; - if (sourceMapSupport) { - caller = sourceMapSupport.wrapCallSite(caller); - } - obj.file = caller.getFileName(); - obj.line = caller.getLineNumber(); - var func = caller.getFunctionName(); - if (func) - obj.func = func; - }; - Error.captureStackTrace(this, getCaller3Info); - this.stack; - - Error.stackTraceLimit = saveLimit; - Error.prepareStackTrace = savePrepare; - return obj; -} - - -function _indent(s, indent) { - if (!indent) indent = ' '; - var lines = s.split(/\r?\n/g); - return indent + lines.join('\n' + indent); -} - - -/** - * Warn about an bunyan processing error. - * - * @param msg {String} Message with which to warn. - * @param dedupKey {String} Optional. A short string key for this warning to - * have its warning only printed once. - */ -function _warn(msg, dedupKey) { - assert.ok(msg); - if (dedupKey) { - if (_warned[dedupKey]) { - return; - } - _warned[dedupKey] = true; - } - process.stderr.write(msg + '\n'); -} -function _haveWarned(dedupKey) { - return _warned[dedupKey]; -} -var _warned = {}; - - -function ConsoleRawStream() {} -ConsoleRawStream.prototype.write = function (rec) { - if (rec.level < INFO) { - console.log(rec); - } else if (rec.level < WARN) { - console.info(rec); - } else if (rec.level < ERROR) { - console.warn(rec); - } else { - console.error(rec); - } -}; - - -//---- Levels - -var TRACE = 10; -var DEBUG = 20; -var INFO = 30; -var WARN = 40; -var ERROR = 50; -var FATAL = 60; - -var levelFromName = { - 'trace': TRACE, - 'debug': DEBUG, - 'info': INFO, - 'warn': WARN, - 'error': ERROR, - 'fatal': FATAL -}; -var nameFromLevel = {}; -Object.keys(levelFromName).forEach(function (name) { - nameFromLevel[levelFromName[name]] = name; -}); - -/** - * Resolve a level number, name (upper or lowercase) to a level number value. - * - * @param nameOrNum {String|Number} A level name (case-insensitive) or positive - * integer level. - * @api public - */ -function resolveLevel(nameOrNum) { - var level; - var type = typeof (nameOrNum); - if (type === 'string') { - level = levelFromName[nameOrNum.toLowerCase()]; - if (!level) { - throw new Error(format('unknown level name: "%s"', nameOrNum)); - } - } else if (type !== 'number') { - throw new TypeError(format('cannot resolve level: invalid arg (%s):', - type, nameOrNum)); - } else if (nameOrNum < 0 || Math.floor(nameOrNum) !== nameOrNum) { - throw new TypeError(format('level is not a positive integer: %s', - nameOrNum)); - } else { - level = nameOrNum; - } - return level; -} - - -function isWritable(obj) { - if (obj instanceof stream.Writable) { - return true; - } - return typeof (obj.write) === 'function'; -} - - -//---- Logger class - -/** - * Create a Logger instance. - * - * @param options {Object} See documentation for full details. At minimum - * this must include a 'name' string key. Configuration keys: - * - `streams`: specify the logger output streams. This is an array of - * objects with these fields: - * - `type`: The stream type. See README.md for full details. - * Often this is implied by the other fields. Examples are - * 'file', 'stream' and "raw". - * - `level`: Defaults to 'info'. - * - `path` or `stream`: The specify the file path or writeable - * stream to which log records are written. E.g. - * `stream: process.stdout`. - * - `closeOnExit` (boolean): Optional. Default is true for a - * 'file' stream when `path` is given, false otherwise. - * See README.md for full details. - * - `level`: set the level for a single output stream (cannot be used - * with `streams`) - * - `stream`: the output stream for a logger with just one, e.g. - * `process.stdout` (cannot be used with `streams`) - * - `serializers`: object mapping log record field names to - * serializing functions. See README.md for details. - * - `src`: Boolean (default false). Set true to enable 'src' automatic - * field with log call source info. - * All other keys are log record fields. - * - * An alternative *internal* call signature is used for creating a child: - * new Logger(, [, ]); - * - * @param _childSimple (Boolean) An assertion that the given `_childOptions` - * (a) only add fields (no config) and (b) no serialization handling is - * required for them. IOW, this is a fast path for frequent child - * creation. - */ -function Logger(options, _childOptions, _childSimple) { - xxx('Logger start:', options) - if (!(this instanceof Logger)) { - return new Logger(options, _childOptions); - } - - // Input arg validation. - var parent; - if (_childOptions !== undefined) { - parent = options; - options = _childOptions; - if (!(parent instanceof Logger)) { - throw new TypeError( - 'invalid Logger creation: do not pass a second arg'); - } - } - if (!options) { - throw new TypeError('options (object) is required'); - } - if (!parent) { - if (!options.name) { - throw new TypeError('options.name (string) is required'); - } - } else { - if (options.name) { - throw new TypeError( - 'invalid options.name: child cannot set logger name'); - } - } - if (options.stream && options.streams) { - throw new TypeError('cannot mix "streams" and "stream" options'); - } - if (options.streams && !Array.isArray(options.streams)) { - throw new TypeError('invalid options.streams: must be an array') - } - if (options.serializers && (typeof (options.serializers) !== 'object' || - Array.isArray(options.serializers))) { - throw new TypeError('invalid options.serializers: must be an object') - } - - EventEmitter.call(this); - - // Fast path for simple child creation. - if (parent && _childSimple) { - // `_isSimpleChild` is a signal to stream close handling that this child - // owns none of its streams. - this._isSimpleChild = true; - - this._level = parent._level; - this.streams = parent.streams; - this.serializers = parent.serializers; - this.src = parent.src; - var fields = this.fields = {}; - var parentFieldNames = Object.keys(parent.fields); - for (var i = 0; i < parentFieldNames.length; i++) { - var name = parentFieldNames[i]; - fields[name] = parent.fields[name]; - } - var names = Object.keys(options); - for (var i = 0; i < names.length; i++) { - var name = names[i]; - fields[name] = options[name]; - } - return; - } - - // Start values. - var self = this; - if (parent) { - this._level = parent._level; - this.streams = []; - for (var i = 0; i < parent.streams.length; i++) { - var s = objCopy(parent.streams[i]); - s.closeOnExit = false; // Don't own parent stream. - this.streams.push(s); - } - this.serializers = objCopy(parent.serializers); - this.src = parent.src; - this.fields = objCopy(parent.fields); - if (options.level) { - this.level(options.level); - } - } else { - this._level = Number.POSITIVE_INFINITY; - this.streams = []; - this.serializers = null; - this.src = false; - this.fields = {}; - } - - // Handle *config* options (i.e. options that are not just plain data - // for log records). - if (options.stream) { - self.addStream({ - type: 'stream', - stream: options.stream, - closeOnExit: false, - level: options.level - }); - } else if (options.streams) { - options.streams.forEach(function (s) { - self.addStream(s, options.level); - }); - } else if (parent && options.level) { - this.level(options.level); - } else if (!parent) { - if (runtimeEnv === 'browser') { - /* - * In the browser we'll be emitting to console.log by default. - * Any console.log worth its salt these days can nicely render - * and introspect objects (e.g. the Firefox and Chrome console) - * so let's emit the raw log record. Are there browsers for which - * that breaks things? - */ - self.addStream({ - type: 'raw', - stream: new ConsoleRawStream(), - closeOnExit: false, - level: options.level - }); - } else { - self.addStream({ - type: 'stream', - stream: process.stdout, - closeOnExit: false, - level: options.level - }); - } - } - if (options.serializers) { - self.addSerializers(options.serializers); - } - if (options.src) { - this.src = true; - } - xxx('Logger: ', self) - - // Fields. - // These are the default fields for log records (minus the attributes - // removed in this constructor). To allow storing raw log records - // (unrendered), `this.fields` must never be mutated. Create a copy for - // any changes. - var fields = objCopy(options); - delete fields.stream; - delete fields.level; - delete fields.streams; - delete fields.serializers; - delete fields.src; - if (this.serializers) { - this._applySerializers(fields); - } - if (!fields.hostname && !self.fields.hostname) { - fields.hostname = os.hostname(); - } - if (!fields.pid) { - fields.pid = process.pid; - } - Object.keys(fields).forEach(function (k) { - self.fields[k] = fields[k]; - }); -} - -util.inherits(Logger, EventEmitter); - - -/** - * Add a stream - * - * @param stream {Object}. Object with these fields: - * - `type`: The stream type. See README.md for full details. - * Often this is implied by the other fields. Examples are - * 'file', 'stream' and "raw". - * - `path` or `stream`: The specify the file path or writeable - * stream to which log records are written. E.g. - * `stream: process.stdout`. - * - `level`: Optional. Falls back to `defaultLevel`. - * - `closeOnExit` (boolean): Optional. Default is true for a - * 'file' stream when `path` is given, false otherwise. - * See README.md for full details. - * @param defaultLevel {Number|String} Optional. A level to use if - * `stream.level` is not set. If neither is given, this defaults to INFO. - */ -Logger.prototype.addStream = function addStream(s, defaultLevel) { - var self = this; - if (defaultLevel === null || defaultLevel === undefined) { - defaultLevel = INFO; - } - - s = objCopy(s); - - // Implicit 'type' from other args. - if (!s.type) { - if (s.stream) { - s.type = 'stream'; - } else if (s.path) { - s.type = 'file' - } - } - s.raw = (s.type === 'raw'); // PERF: Allow for faster check in `_emit`. - - if (s.level !== undefined) { - s.level = resolveLevel(s.level); - } else { - s.level = resolveLevel(defaultLevel); - } - if (s.level < self._level) { - self._level = s.level; - } - - switch (s.type) { - case 'stream': - assert.ok(isWritable(s.stream), - '"stream" stream is not writable: ' + util.inspect(s.stream)); - - if (!s.closeOnExit) { - s.closeOnExit = false; - } - break; - case 'file': - if (s.reemitErrorEvents === undefined) { - s.reemitErrorEvents = true; - } - if (!s.stream) { - s.stream = fs.createWriteStream(s.path, - {flags: 'a', encoding: 'utf8'}); - if (!s.closeOnExit) { - s.closeOnExit = true; - } - } else { - if (!s.closeOnExit) { - s.closeOnExit = false; - } - } - break; - case 'rotating-file': - assert.ok(!s.stream, - '"rotating-file" stream should not give a "stream"'); - assert.ok(s.path); - assert.ok(mv, '"rotating-file" stream type is not supported: ' - + 'missing "mv" module'); - s.stream = new RotatingFileStream(s); - if (!s.closeOnExit) { - s.closeOnExit = true; - } - break; - case 'raw': - if (!s.closeOnExit) { - s.closeOnExit = false; - } - break; - default: - throw new TypeError('unknown stream type "' + s.type + '"'); - } - - if (s.reemitErrorEvents && typeof (s.stream.on) === 'function') { - // TODO: When we have `.close()`, it should remove event - // listeners to not leak Logger instances. - s.stream.on('error', function onStreamError(err) { - self.emit('error', err, s); - }); - } - - self.streams.push(s); - delete self.haveNonRawStreams; // reset -} - - -/** - * Add serializers - * - * @param serializers {Object} Optional. Object mapping log record field names - * to serializing functions. See README.md for details. - */ -Logger.prototype.addSerializers = function addSerializers(serializers) { - var self = this; - - if (!self.serializers) { - self.serializers = {}; - } - Object.keys(serializers).forEach(function (field) { - var serializer = serializers[field]; - if (typeof (serializer) !== 'function') { - throw new TypeError(format( - 'invalid serializer for "%s" field: must be a function', - field)); - } else { - self.serializers[field] = serializer; - } - }); -} - - - -/** - * Create a child logger, typically to add a few log record fields. - * - * This can be useful when passing a logger to a sub-component, e.g. a - * 'wuzzle' component of your service: - * - * var wuzzleLog = log.child({component: 'wuzzle'}) - * var wuzzle = new Wuzzle({..., log: wuzzleLog}) - * - * Then log records from the wuzzle code will have the same structure as - * the app log, *plus the component='wuzzle' field*. - * - * @param options {Object} Optional. Set of options to apply to the child. - * All of the same options for a new Logger apply here. Notes: - * - The parent's streams are inherited and cannot be removed in this - * call. Any given `streams` are *added* to the set inherited from - * the parent. - * - The parent's serializers are inherited, though can effectively be - * overwritten by using duplicate keys. - * - Can use `level` to set the level of the streams inherited from - * the parent. The level for the parent is NOT affected. - * @param simple {Boolean} Optional. Set to true to assert that `options` - * (a) only add fields (no config) and (b) no serialization handling is - * required for them. IOW, this is a fast path for frequent child - * creation. See 'tools/timechild.js' for numbers. - */ -Logger.prototype.child = function (options, simple) { - return new (this.constructor)(this, options || {}, simple); -} - - -/** - * A convenience method to reopen 'file' streams on a logger. This can be - * useful with external log rotation utilities that move and re-open log files - * (e.g. logrotate on Linux, logadm on SmartOS/Illumos). Those utilities - * typically have rotation options to copy-and-truncate the log file, but - * you may not want to use that. An alternative is to do this in your - * application: - * - * var log = bunyan.createLogger(...); - * ... - * process.on('SIGUSR2', function () { - * log.reopenFileStreams(); - * }); - * ... - * - * See . - */ -Logger.prototype.reopenFileStreams = function () { - var self = this; - self.streams.forEach(function (s) { - if (s.type === 'file') { - if (s.stream) { - // Not sure if typically would want this, or more immediate - // `s.stream.destroy()`. - s.stream.end(); - s.stream.destroySoon(); - delete s.stream; - } - s.stream = fs.createWriteStream(s.path, - {flags: 'a', encoding: 'utf8'}); - s.stream.on('error', function (err) { - self.emit('error', err, s); - }); - } - }); -}; - - -/* BEGIN JSSTYLED */ -/** - * Close this logger. - * - * This closes streams (that it owns, as per 'endOnClose' attributes on - * streams), etc. Typically you **don't** need to bother calling this. -Logger.prototype.close = function () { - if (this._closed) { - return; - } - if (!this._isSimpleChild) { - self.streams.forEach(function (s) { - if (s.endOnClose) { - xxx('closing stream s:', s); - s.stream.end(); - s.endOnClose = false; - } - }); - } - this._closed = true; -} - */ -/* END JSSTYLED */ - - -/** - * Get/set the level of all streams on this logger. - * - * Get Usage: - * // Returns the current log level (lowest level of all its streams). - * log.level() -> INFO - * - * Set Usage: - * log.level(INFO) // set all streams to level INFO - * log.level('info') // can use 'info' et al aliases - */ -Logger.prototype.level = function level(value) { - if (value === undefined) { - return this._level; - } - var newLevel = resolveLevel(value); - var len = this.streams.length; - for (var i = 0; i < len; i++) { - this.streams[i].level = newLevel; - } - this._level = newLevel; -} - - -/** - * Get/set the level of a particular stream on this logger. - * - * Get Usage: - * // Returns an array of the levels of each stream. - * log.levels() -> [TRACE, INFO] - * - * // Returns a level of the identified stream. - * log.levels(0) -> TRACE // level of stream at index 0 - * log.levels('foo') // level of stream with name 'foo' - * - * Set Usage: - * log.levels(0, INFO) // set level of stream 0 to INFO - * log.levels(0, 'info') // can use 'info' et al aliases - * log.levels('foo', WARN) // set stream named 'foo' to WARN - * - * Stream names: When streams are defined, they can optionally be given - * a name. For example, - * log = new Logger({ - * streams: [ - * { - * name: 'foo', - * path: '/var/log/my-service/foo.log' - * level: 'trace' - * }, - * ... - * - * @param name {String|Number} The stream index or name. - * @param value {Number|String} The level value (INFO) or alias ('info'). - * If not given, this is a 'get' operation. - * @throws {Error} If there is no stream with the given name. - */ -Logger.prototype.levels = function levels(name, value) { - if (name === undefined) { - assert.equal(value, undefined); - return this.streams.map( - function (s) { return s.level }); - } - var stream; - if (typeof (name) === 'number') { - stream = this.streams[name]; - if (stream === undefined) { - throw new Error('invalid stream index: ' + name); - } - } else { - var len = this.streams.length; - for (var i = 0; i < len; i++) { - var s = this.streams[i]; - if (s.name === name) { - stream = s; - break; - } - } - if (!stream) { - throw new Error(format('no stream with name "%s"', name)); - } - } - if (value === undefined) { - return stream.level; - } else { - var newLevel = resolveLevel(value); - stream.level = newLevel; - if (newLevel < this._level) { - this._level = newLevel; - } - } -} - - -/** - * Apply registered serializers to the appropriate keys in the given fields. - * - * Pre-condition: This is only called if there is at least one serializer. - * - * @param fields (Object) The log record fields. - * @param excludeFields (Object) Optional mapping of keys to `true` for - * keys to NOT apply a serializer. - */ -Logger.prototype._applySerializers = function (fields, excludeFields) { - var self = this; - - xxx('_applySerializers: excludeFields', excludeFields); - - // Check each serializer against these (presuming number of serializers - // is typically less than number of fields). - Object.keys(this.serializers).forEach(function (name) { - if (fields[name] === undefined || - (excludeFields && excludeFields[name])) - { - return; - } - xxx('_applySerializers; apply to "%s" key', name) - try { - fields[name] = self.serializers[name](fields[name]); - } catch (err) { - _warn(format('bunyan: ERROR: Exception thrown from the "%s" ' - + 'Bunyan serializer. This should never happen. This is a bug ' - + 'in that serializer function.\n%s', - name, err.stack || err)); - fields[name] = format('(Error in Bunyan log "%s" serializer ' - + 'broke field. See stderr for details.)', name); - } - }); -} - - -/** - * Emit a log record. - * - * @param rec {log record} - * @param noemit {Boolean} Optional. Set to true to skip emission - * and just return the JSON string. - */ -Logger.prototype._emit = function (rec, noemit) { - var i; - - // Lazily determine if this Logger has non-'raw' streams. If there are - // any, then we need to stringify the log record. - if (this.haveNonRawStreams === undefined) { - this.haveNonRawStreams = false; - for (i = 0; i < this.streams.length; i++) { - if (!this.streams[i].raw) { - this.haveNonRawStreams = true; - break; - } - } - } - - // Stringify the object (creates a warning str on error). - var str; - if (noemit || this.haveNonRawStreams) { - str = fastAndSafeJsonStringify(rec) + '\n'; - } - - if (noemit) - return str; - - var level = rec.level; - for (i = 0; i < this.streams.length; i++) { - var s = this.streams[i]; - if (s.level <= level) { - xxx('writing log rec "%s" to "%s" stream (%d <= %d): %j', - rec.msg, s.type, s.level, level, rec); - s.stream.write(s.raw ? rec : str); - } - }; - - return str; -} - - -/** - * Build a record object suitable for emitting from the arguments - * provided to the a log emitter. - */ -function mkRecord(log, minLevel, args) { - var excludeFields, fields, msgArgs; - if (args[0] instanceof Error) { - // `log.(err, ...)` - fields = { - // Use this Logger's err serializer, if defined. - err: (log.serializers && log.serializers.err - ? log.serializers.err(args[0]) - : Logger.stdSerializers.err(args[0])) - }; - excludeFields = {err: true}; - if (args.length === 1) { - msgArgs = [fields.err.message]; - } else { - msgArgs = args.slice(1); - } - } else if (typeof (args[0]) !== 'object' || Array.isArray(args[0])) { - // `log.(msg, ...)` - fields = null; - msgArgs = args.slice(); - } else if (Buffer.isBuffer(args[0])) { // `log.(buf, ...)` - // Almost certainly an error, show `inspect(buf)`. See bunyan - // issue #35. - fields = null; - msgArgs = args.slice(); - msgArgs[0] = util.inspect(msgArgs[0]); - } else { // `log.(fields, msg, ...)` - fields = args[0]; - if (fields && args.length === 1 && fields.err && - fields.err instanceof Error) - { - msgArgs = [fields.err.message]; - } else { - msgArgs = args.slice(1); - } - } - - // Build up the record object. - var rec = objCopy(log.fields); - var level = rec.level = minLevel; - var recFields = (fields ? objCopy(fields) : null); - if (recFields) { - if (log.serializers) { - log._applySerializers(recFields, excludeFields); - } - Object.keys(recFields).forEach(function (k) { - rec[k] = recFields[k]; - }); - } - rec.msg = format.apply(log, msgArgs); - if (!rec.time) { - rec.time = (new Date()); - } - // Get call source info - if (log.src && !rec.src) { - rec.src = getCaller3Info() - } - rec.v = LOG_VERSION; - - return rec; -}; - - -/** - * Build a log emitter function for level minLevel. I.e. this is the - * creator of `log.info`, `log.error`, etc. - */ -function mkLogEmitter(minLevel) { - return function () { - var log = this; - var str = null; - var rec = null; - - if (!this._emit) { - /* - * Show this invalid Bunyan usage warning *once*. - * - * See for - * an example of how this can happen. - */ - var dedupKey = 'unbound'; - if (!_haveWarned[dedupKey]) { - var caller = getCaller3Info(); - _warn(format('bunyan usage error: %s:%s: attempt to log ' - + 'with an unbound log method: `this` is: %s', - caller.file, caller.line, util.inspect(this)), - dedupKey); - } - return; - } else if (arguments.length === 0) { // `log.()` - return (this._level <= minLevel); - } - - var msgArgs = new Array(arguments.length); - for (var i = 0; i < msgArgs.length; ++i) { - msgArgs[i] = arguments[i]; - } - - if (this._level <= minLevel) { - rec = mkRecord(log, minLevel, msgArgs); - str = this._emit(rec); - } - } -} - - -/** - * The functions below log a record at a specific level. - * - * Usages: - * log.() -> boolean is-trace-enabled - * log.( err, [ msg, ...]) - * log.( msg, ...) - * log.( fields, msg, ...) - * - * where is the lowercase version of the log level. E.g.: - * - * log.info() - * - * @params fields {Object} Optional set of additional fields to log. - * @params msg {String} Log message. This can be followed by additional - * arguments that are handled like - * [util.format](http://nodejs.org/docs/latest/api/all.html#util.format). - */ -Logger.prototype.trace = mkLogEmitter(TRACE); -Logger.prototype.debug = mkLogEmitter(DEBUG); -Logger.prototype.info = mkLogEmitter(INFO); -Logger.prototype.warn = mkLogEmitter(WARN); -Logger.prototype.error = mkLogEmitter(ERROR); -Logger.prototype.fatal = mkLogEmitter(FATAL); - - - -//---- Standard serializers -// A serializer is a function that serializes a JavaScript object to a -// JSON representation for logging. There is a standard set of presumed -// interesting objects in node.js-land. - -Logger.stdSerializers = {}; - -// Serialize an HTTP request. -Logger.stdSerializers.req = function (req) { - if (!req || !req.connection) - return req; - return { - method: req.method, - url: req.url, - headers: req.headers, - remoteAddress: req.connection.remoteAddress, - remotePort: req.connection.remotePort - }; - // Trailers: Skipping for speed. If you need trailers in your app, then - // make a custom serializer. - //if (Object.keys(trailers).length > 0) { - // obj.trailers = req.trailers; - //} -}; - -// Serialize an HTTP response. -Logger.stdSerializers.res = function (res) { - if (!res || !res.statusCode) - return res; - return { - statusCode: res.statusCode, - header: res._header - } -}; - - -/* - * This function dumps long stack traces for exceptions having a cause() - * method. The error classes from - * [verror](https://github.com/davepacheco/node-verror) and - * [restify v2.0](https://github.com/mcavage/node-restify) are examples. - * - * Based on `dumpException` in - * https://github.com/davepacheco/node-extsprintf/blob/master/lib/extsprintf.js - */ -function getFullErrorStack(ex) -{ - var ret = ex.stack || ex.toString(); - if (ex.cause && typeof (ex.cause) === 'function') { - var cex = ex.cause(); - if (cex) { - ret += '\nCaused by: ' + getFullErrorStack(cex); - } - } - return (ret); -} - -// Serialize an Error object -// (Core error properties are enumerable in node 0.4, not in 0.6). -var errSerializer = Logger.stdSerializers.err = function (err) { - if (!err || !err.stack) - return err; - var obj = { - message: err.message, - name: err.name, - stack: getFullErrorStack(err), - code: err.code, - signal: err.signal - } - return obj; -}; - - -// A JSON stringifier that handles cycles safely - tracks seen values in a Set. -function safeCyclesSet() { - var seen = new Set(); - return function (key, val) { - if (!val || typeof (val) !== 'object') { - return val; - } - if (seen.has(val)) { - return '[Circular]'; - } - seen.add(val); - return val; - }; -} - -/** - * A JSON stringifier that handles cycles safely - tracks seen vals in an Array. - * - * Note: This approach has performance problems when dealing with large objects, - * see trentm/node-bunyan#445, but since this is the only option for node 0.10 - * and earlier (as Set was introduced in Node 0.12), it's used as a fallback - * when Set is not available. - */ -function safeCyclesArray() { - var seen = []; - return function (key, val) { - if (!val || typeof (val) !== 'object') { - return val; - } - if (seen.indexOf(val) !== -1) { - return '[Circular]'; - } - seen.push(val); - return val; - }; -} - -/** - * A JSON stringifier that handles cycles safely. - * - * Usage: JSON.stringify(obj, safeCycles()) - * - * Choose the best safe cycle function from what is available - see - * trentm/node-bunyan#445. - */ -var safeCycles = typeof (Set) !== 'undefined' ? safeCyclesSet : safeCyclesArray; - -/** - * A fast JSON.stringify that handles cycles and getter exceptions (when - * safeJsonStringify is installed). - * - * This function attempts to use the regular JSON.stringify for speed, but on - * error (e.g. JSON cycle detection exception) it falls back to safe stringify - * handlers that can deal with cycles and/or getter exceptions. - */ -function fastAndSafeJsonStringify(rec) { - try { - return JSON.stringify(rec); - } catch (ex) { - try { - return JSON.stringify(rec, safeCycles()); - } catch (e) { - if (safeJsonStringify) { - return safeJsonStringify(rec); - } else { - var dedupKey = e.stack.split(/\n/g, 3).join('\n'); - _warn('bunyan: ERROR: Exception in ' - + '`JSON.stringify(rec)`. You can install the ' - + '"safe-json-stringify" module to have Bunyan fallback ' - + 'to safer stringification. Record:\n' - + _indent(format('%s\n%s', util.inspect(rec), e.stack)), - dedupKey); - return format('(Exception in JSON.stringify(rec): %j. ' - + 'See stderr for details.)', e.message); - } - } - } -} - - -var RotatingFileStream = null; -if (mv) { - -RotatingFileStream = function RotatingFileStream(options) { - this.path = options.path; - - this.count = (options.count == null ? 10 : options.count); - assert.equal(typeof (this.count), 'number', - format('rotating-file stream "count" is not a number: %j (%s) in %j', - this.count, typeof (this.count), this)); - assert.ok(this.count >= 0, - format('rotating-file stream "count" is not >= 0: %j in %j', - this.count, this)); - - // Parse `options.period`. - if (options.period) { - // where scope is: - // h hours (at the start of the hour) - // d days (at the start of the day, i.e. just after midnight) - // w weeks (at the start of Sunday) - // m months (on the first of the month) - // y years (at the start of Jan 1st) - // with special values 'hourly' (1h), 'daily' (1d), "weekly" (1w), - // 'monthly' (1m) and 'yearly' (1y) - var period = { - 'hourly': '1h', - 'daily': '1d', - 'weekly': '1w', - 'monthly': '1m', - 'yearly': '1y' - }[options.period] || options.period; - var m = /^([1-9][0-9]*)([hdwmy]|ms)$/.exec(period); - if (!m) { - throw new Error(format('invalid period: "%s"', options.period)); - } - this.periodNum = Number(m[1]); - this.periodScope = m[2]; - } else { - this.periodNum = 1; - this.periodScope = 'd'; - } - - var lastModified = null; - try { - var fileInfo = fs.statSync(this.path); - lastModified = fileInfo.mtime.getTime(); - } - catch (err) { - // file doesn't exist - } - var rotateAfterOpen = false; - if (lastModified) { - var lastRotTime = this._calcRotTime(0); - if (lastModified < lastRotTime) { - rotateAfterOpen = true; - } - } - - // TODO: template support for backup files - // template: - // default is %P.%n - // '/var/log/archive/foo.log' -> foo.log.%n - // '/var/log/archive/foo.log.%n' - // codes: - // XXX support strftime codes (per node version of those) - // or whatever module. Pick non-colliding for extra - // codes - // %P `path` base value - // %n integer number of rotated log (1,2,3,...) - // %d datetime in YYYY-MM-DD_HH-MM-SS - // XXX what should default date format be? - // prior art? Want to avoid ':' in - // filenames (illegal on Windows for one). - - this.stream = fs.createWriteStream(this.path, - {flags: 'a', encoding: 'utf8'}); - - this.rotQueue = []; - this.rotating = false; - if (rotateAfterOpen) { - this._debug('rotateAfterOpen -> call rotate()'); - this.rotate(); - } else { - this._setupNextRot(); - } -} - -util.inherits(RotatingFileStream, EventEmitter); - -RotatingFileStream.prototype._debug = function () { - // Set this to `true` to add debug logging. - if (false) { - if (arguments.length === 0) { - return true; - } - var args = Array.prototype.slice.call(arguments); - args[0] = '[' + (new Date().toISOString()) + ', ' - + this.path + '] ' + args[0]; - console.log.apply(this, args); - } else { - return false; - } -}; - -RotatingFileStream.prototype._setupNextRot = function () { - this.rotAt = this._calcRotTime(1); - this._setRotationTimer(); -} - -RotatingFileStream.prototype._setRotationTimer = function () { - var self = this; - var delay = this.rotAt - Date.now(); - // Cap timeout to Node's max setTimeout, see - // . - var TIMEOUT_MAX = 2147483647; // 2^31-1 - if (delay > TIMEOUT_MAX) { - delay = TIMEOUT_MAX; - } - this.timeout = setTimeout( - function () { - self._debug('_setRotationTimer timeout -> call rotate()'); - self.rotate(); - }, - delay); - if (typeof (this.timeout.unref) === 'function') { - this.timeout.unref(); - } -} - -RotatingFileStream.prototype._calcRotTime = -function _calcRotTime(periodOffset) { - this._debug('_calcRotTime: %s%s', this.periodNum, this.periodScope); - var d = new Date(); - - this._debug(' now local: %s', d); - this._debug(' now utc: %s', d.toISOString()); - var rotAt; - switch (this.periodScope) { - case 'ms': - // Hidden millisecond period for debugging. - if (this.rotAt) { - rotAt = this.rotAt + this.periodNum * periodOffset; - } else { - rotAt = Date.now() + this.periodNum * periodOffset; - } - break; - case 'h': - if (this.rotAt) { - rotAt = this.rotAt + this.periodNum * 60 * 60 * 1000 * periodOffset; - } else { - // First time: top of the next hour. - rotAt = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), - d.getUTCDate(), d.getUTCHours() + periodOffset); - } - break; - case 'd': - if (this.rotAt) { - rotAt = this.rotAt + this.periodNum * 24 * 60 * 60 * 1000 - * periodOffset; - } else { - // First time: start of tomorrow (i.e. at the coming midnight) UTC. - rotAt = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), - d.getUTCDate() + periodOffset); - } - break; - case 'w': - // Currently, always on Sunday morning at 00:00:00 (UTC). - if (this.rotAt) { - rotAt = this.rotAt + this.periodNum * 7 * 24 * 60 * 60 * 1000 - * periodOffset; - } else { - // First time: this coming Sunday. - var dayOffset = (7 - d.getUTCDay()); - if (periodOffset < 1) { - dayOffset = -d.getUTCDay(); - } - if (periodOffset > 1 || periodOffset < -1) { - dayOffset += 7 * periodOffset; - } - rotAt = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), - d.getUTCDate() + dayOffset); - } - break; - case 'm': - if (this.rotAt) { - rotAt = Date.UTC(d.getUTCFullYear(), - d.getUTCMonth() + this.periodNum * periodOffset, 1); - } else { - // First time: the start of the next month. - rotAt = Date.UTC(d.getUTCFullYear(), - d.getUTCMonth() + periodOffset, 1); - } - break; - case 'y': - if (this.rotAt) { - rotAt = Date.UTC(d.getUTCFullYear() + this.periodNum * periodOffset, - 0, 1); - } else { - // First time: the start of the next year. - rotAt = Date.UTC(d.getUTCFullYear() + periodOffset, 0, 1); - } - break; - default: - assert.fail(format('invalid period scope: "%s"', this.periodScope)); - } - - if (this._debug()) { - this._debug(' **rotAt**: %s (utc: %s)', rotAt, - new Date(rotAt).toUTCString()); - var now = Date.now(); - this._debug(' now: %s (%sms == %smin == %sh to go)', - now, - rotAt - now, - (rotAt-now)/1000/60, - (rotAt-now)/1000/60/60); - } - return rotAt; -}; - -RotatingFileStream.prototype.rotate = function rotate() { - // XXX What about shutdown? - var self = this; - - // If rotation period is > ~25 days, we have to break into multiple - // setTimeout's. See . - if (self.rotAt && self.rotAt > Date.now()) { - return self._setRotationTimer(); - } - - this._debug('rotate'); - if (self.rotating) { - throw new TypeError('cannot start a rotation when already rotating'); - } - self.rotating = true; - - self.stream.end(); // XXX can do moves sync after this? test at high rate - - function del() { - var toDel = self.path + '.' + String(n - 1); - if (n === 0) { - toDel = self.path; - } - n -= 1; - self._debug(' rm %s', toDel); - fs.unlink(toDel, function (delErr) { - //XXX handle err other than not exists - moves(); - }); - } - - function moves() { - if (self.count === 0 || n < 0) { - return finish(); - } - var before = self.path; - var after = self.path + '.' + String(n); - if (n > 0) { - before += '.' + String(n - 1); - } - n -= 1; - fs.exists(before, function (exists) { - if (!exists) { - moves(); - } else { - self._debug(' mv %s %s', before, after); - mv(before, after, function (mvErr) { - if (mvErr) { - self.emit('error', mvErr); - finish(); // XXX finish here? - } else { - moves(); - } - }); - } - }) - } - - function finish() { - self._debug(' open %s', self.path); - self.stream = fs.createWriteStream(self.path, - {flags: 'a', encoding: 'utf8'}); - var q = self.rotQueue, len = q.length; - for (var i = 0; i < len; i++) { - self.stream.write(q[i]); - } - self.rotQueue = []; - self.rotating = false; - self.emit('drain'); - self._setupNextRot(); - } - - var n = this.count; - del(); -}; - -RotatingFileStream.prototype.write = function write(s) { - if (this.rotating) { - this.rotQueue.push(s); - return false; - } else { - return this.stream.write(s); - } -}; - -RotatingFileStream.prototype.end = function end(s) { - this.stream.end(); -}; - -RotatingFileStream.prototype.destroy = function destroy(s) { - this.stream.destroy(); -}; - -RotatingFileStream.prototype.destroySoon = function destroySoon(s) { - this.stream.destroySoon(); -}; - -} /* if (mv) */ - - - -/** - * RingBuffer is a Writable Stream that just stores the last N records in - * memory. - * - * @param options {Object}, with the following fields: - * - * - limit: number of records to keep in memory - */ -function RingBuffer(options) { - this.limit = options && options.limit ? options.limit : 100; - this.writable = true; - this.records = []; - EventEmitter.call(this); -} - -util.inherits(RingBuffer, EventEmitter); - -RingBuffer.prototype.write = function (record) { - if (!this.writable) - throw (new Error('RingBuffer has been ended already')); - - this.records.push(record); - - if (this.records.length > this.limit) - this.records.shift(); - - return (true); -}; - -RingBuffer.prototype.end = function () { - if (arguments.length > 0) - this.write.apply(this, Array.prototype.slice.call(arguments)); - this.writable = false; -}; - -RingBuffer.prototype.destroy = function () { - this.writable = false; - this.emit('close'); -}; - -RingBuffer.prototype.destroySoon = function () { - this.destroy(); -}; - - -//---- Exports - -module.exports = Logger; - -module.exports.TRACE = TRACE; -module.exports.DEBUG = DEBUG; -module.exports.INFO = INFO; -module.exports.WARN = WARN; -module.exports.ERROR = ERROR; -module.exports.FATAL = FATAL; -module.exports.resolveLevel = resolveLevel; -module.exports.levelFromName = levelFromName; -module.exports.nameFromLevel = nameFromLevel; - -module.exports.VERSION = require('../package.json').version; -module.exports.LOG_VERSION = LOG_VERSION; - -module.exports.createLogger = function createLogger(options) { - return new Logger(options); -}; - -module.exports.RingBuffer = RingBuffer; -module.exports.RotatingFileStream = RotatingFileStream; - -// Useful for custom `type == 'raw'` streams that may do JSON stringification -// of log records themselves. Usage: -// var str = JSON.stringify(rec, bunyan.safeCycles()); -module.exports.safeCycles = safeCycles; diff --git a/lib/bunyan.mjs b/lib/bunyan.mjs new file mode 100644 index 0000000..bbc35a5 --- /dev/null +++ b/lib/bunyan.mjs @@ -0,0 +1,1128 @@ +/** + * Copyright (c) 2017 Trent Mick. + * Copyright (c) 2017 Joyent Inc. + * + * The bunyan logging library for node.js. + * + * -*- mode: js -*- + * vim: expandtab:ts=4:sw=4 + */ + +import os from 'os' +import fs from 'fs' +import util from 'util' +import path from 'path' +import assert from 'assert' +import events from 'events' +import stream from 'stream' +import { fileURLToPath } from 'url' +import safeJson from './safe-json.mjs' + +const EventEmitter = events.EventEmitter + +/* + * Bunyan log format version. This becomes the 'v' field on all log records. + * This will be incremented if there is any backward incompatible change to + * the log record format. Details will be in 'CHANGES.md' (the change log). + */ +const LOG_VERSION = 0; + + +var xxx = function xxx(s) { // internal dev/debug logging + var args = ['XX' + 'X: '+s].concat( + Array.prototype.slice.call(arguments, 1)); + console.error.apply(this, args); +}; +var xxx = function xxx() {}; // comment out to turn on debug logging + +//---- Internal support stuff + +/** + * A shallow copy of an object. Bunyan logging attempts to never cause + * exceptions, so this function attempts to handle non-objects gracefully. + */ +function objCopy(obj) { + if (obj == null) { // null or undefined + return obj; + } else if (Array.isArray(obj)) { + return obj.slice(); + } else if (typeof (obj) === 'object') { + var copy = {}; + Object.keys(obj).forEach(function (k) { + copy[k] = obj[k]; + }); + return copy; + } else { + return obj; + } +} + +var format = util.format; +if (!format) { + // If node < 0.6, then use its `util.format`: + // : + var inspect = util.inspect; + var formatRegExp = /%[sdj%]/g; + format = function format(f) { + if (typeof (f) !== 'string') { + var objects = []; + for (var i = 0; i < arguments.length; i++) { + objects.push(inspect(arguments[i])); + } + return objects.join(' '); + } + + var i = 1; + var args = arguments; + var len = args.length; + var str = String(f).replace(formatRegExp, function (x) { + if (i >= len) + return x; + switch (x) { + case '%s': return String(args[i++]); + case '%d': return Number(args[i++]); + case '%j': return fastAndSafeJsonStringify(args[i++]); + case '%%': return '%'; + default: + return x; + } + }); + for (var x = args[i]; i < len; x = args[++i]) { + if (x === null || typeof (x) !== 'object') { + str += ' ' + x; + } else { + str += ' ' + inspect(x); + } + } + return str; + }; +} + + +function _indent(s, indent) { + if (!indent) indent = ' '; + var lines = s.split(/\r?\n/g); + return indent + lines.join('\n' + indent); +} + + +/** + * Warn about an bunyan processing error. + * + * @param msg {String} Message with which to warn. + * @param dedupKey {String} Optional. A short string key for this warning to + * have its warning only printed once. + */ +function _warn(msg, dedupKey) { + assert.ok(msg); + if (dedupKey) { + if (_warned[dedupKey]) { + return; + } + _warned[dedupKey] = true; + } + process.stderr.write(msg + '\n'); +} +function _haveWarned(dedupKey) { + return _warned[dedupKey]; +} +var _warned = {}; + + +function ConsoleRawStream() {} +ConsoleRawStream.prototype.write = function (rec) { + if (rec.level < INFO) { + console.log(rec); + } else if (rec.level < WARN) { + console.info(rec); + } else if (rec.level < ERROR) { + console.warn(rec); + } else { + console.error(rec); + } +}; + + +//---- Levels + +var TRACE = 10; +var DEBUG = 20; +var INFO = 30; +var WARN = 40; +var ERROR = 50; +var FATAL = 60; + +var levelFromName = { + 'trace': TRACE, + 'debug': DEBUG, + 'info': INFO, + 'warn': WARN, + 'error': ERROR, + 'fatal': FATAL +}; +var nameFromLevel = {}; +Object.keys(levelFromName).forEach(function (name) { + nameFromLevel[levelFromName[name]] = name; +}); + +/** + * Resolve a level number, name (upper or lowercase) to a level number value. + * + * @param nameOrNum {String|Number} A level name (case-insensitive) or positive + * integer level. + * @api public + */ +function resolveLevel(nameOrNum) { + var level; + var type = typeof (nameOrNum); + if (type === 'string') { + level = levelFromName[nameOrNum.toLowerCase()]; + if (!level) { + throw new Error(format('unknown level name: "%s"', nameOrNum)); + } + } else if (type !== 'number') { + throw new TypeError(format('cannot resolve level: invalid arg (%s):', + type, nameOrNum)); + } else if (nameOrNum < 0 || Math.floor(nameOrNum) !== nameOrNum) { + throw new TypeError(format('level is not a positive integer: %s', + nameOrNum)); + } else { + level = nameOrNum; + } + return level; +} + + +function isWritable(obj) { + if (obj instanceof stream.Writable) { + return true; + } + return typeof (obj.write) === 'function'; +} + + +//---- Logger class + +/** + * Create a Logger instance. + * + * @param options {Object} See documentation for full details. At minimum + * this must include a 'name' string key. Configuration keys: + * - `streams`: specify the logger output streams. This is an array of + * objects with these fields: + * - `type`: The stream type. See README.md for full details. + * Often this is implied by the other fields. Examples are + * 'file', 'stream' and "raw". + * - `level`: Defaults to 'info'. + * - `path` or `stream`: The specify the file path or writeable + * stream to which log records are written. E.g. + * `stream: process.stdout`. + * - `closeOnExit` (boolean): Optional. Default is true for a + * 'file' stream when `path` is given, false otherwise. + * See README.md for full details. + * - `level`: set the level for a single output stream (cannot be used + * with `streams`) + * - `stream`: the output stream for a logger with just one, e.g. + * `process.stdout` (cannot be used with `streams`) + * - `serializers`: object mapping log record field names to + * serializing functions. See README.md for details. + * - `src`: Boolean (default false). Set true to enable 'src' automatic + * field with log call source info. + * All other keys are log record fields. + * + * An alternative *internal* call signature is used for creating a child: + * new Logger(, [, ]); + * + * @param _childSimple (Boolean) An assertion that the given `_childOptions` + * (a) only add fields (no config) and (b) no serialization handling is + * required for them. IOW, this is a fast path for frequent child + * creation. + */ +function Logger(options, _childOptions, _childSimple) { + xxx('Logger start:', options) + if (!(this instanceof Logger)) { + return new Logger(options, _childOptions); + } + + // Input arg validation. + var parent; + if (_childOptions !== undefined) { + parent = options; + options = _childOptions; + if (!(parent instanceof Logger)) { + throw new TypeError( + 'invalid Logger creation: do not pass a second arg'); + } + } + if (!options) { + throw new TypeError('options (object) is required'); + } + if (!parent) { + if (!options.name) { + throw new TypeError('options.name (string) is required'); + } + } else { + if (options.name) { + throw new TypeError( + 'invalid options.name: child cannot set logger name'); + } + } + if (options.stream && options.streams) { + throw new TypeError('cannot mix "streams" and "stream" options'); + } + if (options.streams && !Array.isArray(options.streams)) { + throw new TypeError('invalid options.streams: must be an array') + } + if (options.serializers && (typeof (options.serializers) !== 'object' || + Array.isArray(options.serializers))) { + throw new TypeError('invalid options.serializers: must be an object') + } + + EventEmitter.call(this); + + // Fast path for simple child creation. + if (parent && _childSimple) { + // `_isSimpleChild` is a signal to stream close handling that this child + // owns none of its streams. + this._isSimpleChild = true; + + this._level = parent._level; + this.streams = parent.streams; + this.serializers = parent.serializers; + var fields = this.fields = {}; + var parentFieldNames = Object.keys(parent.fields); + for (var i = 0; i < parentFieldNames.length; i++) { + var name = parentFieldNames[i]; + fields[name] = parent.fields[name]; + } + var names = Object.keys(options); + for (var i = 0; i < names.length; i++) { + var name = names[i]; + fields[name] = options[name]; + } + return; + } + + // Start values. + var self = this; + if (parent) { + this._level = parent._level; + this.streams = []; + for (var i = 0; i < parent.streams.length; i++) { + var s = objCopy(parent.streams[i]); + s.closeOnExit = false; // Don't own parent stream. + this.streams.push(s); + } + this.serializers = objCopy(parent.serializers); + this.fields = objCopy(parent.fields); + if (options.level) { + this.level(options.level); + } + } else { + this._level = Number.POSITIVE_INFINITY; + this.streams = []; + this.serializers = null; + this.fields = {}; + } + + // Handle *config* options (i.e. options that are not just plain data + // for log records). + if (options.stream) { + self.addStream({ + type: 'stream', + stream: options.stream, + closeOnExit: false, + level: options.level + }); + } else if (options.streams) { + options.streams.forEach(function (s) { + self.addStream(s, options.level); + }); + } else if (parent && options.level) { + this.level(options.level); + } else if (!parent) { + self.addStream({ + type: 'stream', + stream: process.stdout, + closeOnExit: false, + level: options.level + }); + } + if (options.serializers) { + self.addSerializers(options.serializers); + } + xxx('Logger: ', self) + + // Fields. + // These are the default fields for log records (minus the attributes + // removed in this constructor). To allow storing raw log records + // (unrendered), `this.fields` must never be mutated. Create a copy for + // any changes. + var fields = objCopy(options); + delete fields.stream; + delete fields.level; + delete fields.streams; + delete fields.serializers; + if (this.serializers) { + this._applySerializers(fields); + } + if (!fields.hostname && !self.fields.hostname) { + fields.hostname = os.hostname(); + } + if (!fields.pid) { + fields.pid = process.pid; + } + Object.keys(fields).forEach(function (k) { + self.fields[k] = fields[k]; + }); +} + +util.inherits(Logger, EventEmitter); + + +/** + * Add a stream + * + * @param stream {Object}. Object with these fields: + * - `type`: The stream type. See README.md for full details. + * Often this is implied by the other fields. Examples are + * 'file', 'stream' and "raw". + * - `path` or `stream`: The specify the file path or writeable + * stream to which log records are written. E.g. + * `stream: process.stdout`. + * - `level`: Optional. Falls back to `defaultLevel`. + * - `closeOnExit` (boolean): Optional. Default is true for a + * 'file' stream when `path` is given, false otherwise. + * See README.md for full details. + * @param defaultLevel {Number|String} Optional. A level to use if + * `stream.level` is not set. If neither is given, this defaults to INFO. + */ +Logger.prototype.addStream = function addStream(s, defaultLevel) { + var self = this; + if (defaultLevel === null || defaultLevel === undefined) { + defaultLevel = INFO; + } + + s = objCopy(s); + + // Implicit 'type' from other args. + if (!s.type) { + if (s.stream) { + s.type = 'stream'; + } else if (s.path) { + s.type = 'file' + } + } + s.raw = (s.type === 'raw'); // PERF: Allow for faster check in `_emit`. + + if (s.level !== undefined) { + s.level = resolveLevel(s.level); + } else { + s.level = resolveLevel(defaultLevel); + } + if (s.level < self._level) { + self._level = s.level; + } + + switch (s.type) { + case 'stream': + assert.ok(isWritable(s.stream), + '"stream" stream is not writable: ' + util.inspect(s.stream)); + + if (!s.closeOnExit) { + s.closeOnExit = false; + } + break; + case 'file': + if (s.reemitErrorEvents === undefined) { + s.reemitErrorEvents = true; + } + if (!s.stream) { + s.stream = fs.createWriteStream(s.path, + {flags: 'a', encoding: 'utf8'}); + if (!s.closeOnExit) { + s.closeOnExit = true; + } + } else { + if (!s.closeOnExit) { + s.closeOnExit = false; + } + } + break; + case 'raw': + if (!s.closeOnExit) { + s.closeOnExit = false; + } + break; + default: + throw new TypeError('unknown stream type "' + s.type + '"'); + } + + if (s.reemitErrorEvents && typeof (s.stream.on) === 'function') { + // TODO: When we have `.close()`, it should remove event + // listeners to not leak Logger instances. + s.stream.on('error', function onStreamError(err) { + self.emit('error', err, s); + }); + } + + self.streams.push(s); + delete self.haveNonRawStreams; // reset +} + + +/** + * Add serializers + * + * @param serializers {Object} Optional. Object mapping log record field names + * to serializing functions. See README.md for details. + */ +Logger.prototype.addSerializers = function addSerializers(serializers) { + var self = this; + + if (!self.serializers) { + self.serializers = {}; + } + Object.keys(serializers).forEach(function (field) { + var serializer = serializers[field]; + if (typeof (serializer) !== 'function') { + throw new TypeError(format( + 'invalid serializer for "%s" field: must be a function', + field)); + } else { + self.serializers[field] = serializer; + } + }); +} + + + +/** + * Create a child logger, typically to add a few log record fields. + * + * This can be useful when passing a logger to a sub-component, e.g. a + * 'wuzzle' component of your service: + * + * var wuzzleLog = log.child({component: 'wuzzle'}) + * var wuzzle = new Wuzzle({..., log: wuzzleLog}) + * + * Then log records from the wuzzle code will have the same structure as + * the app log, *plus the component='wuzzle' field*. + * + * @param options {Object} Optional. Set of options to apply to the child. + * All of the same options for a new Logger apply here. Notes: + * - The parent's streams are inherited and cannot be removed in this + * call. Any given `streams` are *added* to the set inherited from + * the parent. + * - The parent's serializers are inherited, though can effectively be + * overwritten by using duplicate keys. + * - Can use `level` to set the level of the streams inherited from + * the parent. The level for the parent is NOT affected. + * @param simple {Boolean} Optional. Set to true to assert that `options` + * (a) only add fields (no config) and (b) no serialization handling is + * required for them. IOW, this is a fast path for frequent child + * creation. See 'tools/timechild.js' for numbers. + */ +Logger.prototype.child = function (options, simple) { + return new (this.constructor)(this, options || {}, simple); +} + + +/** + * A convenience method to reopen 'file' streams on a logger. This can be + * useful with external log rotation utilities that move and re-open log files + * (e.g. logrotate on Linux, logadm on SmartOS/Illumos). Those utilities + * typically have rotation options to copy-and-truncate the log file, but + * you may not want to use that. An alternative is to do this in your + * application: + * + * var log = bunyan.createLogger(...); + * ... + * process.on('SIGUSR2', function () { + * log.reopenFileStreams(); + * }); + * ... + * + * See . + */ +Logger.prototype.reopenFileStreams = function () { + var self = this; + self.streams.forEach(function (s) { + if (s.type === 'file') { + if (s.stream) { + // Not sure if typically would want this, or more immediate + // `s.stream.destroy()`. + s.stream.end(); + s.stream.destroySoon(); + delete s.stream; + } + s.stream = fs.createWriteStream(s.path, + {flags: 'a', encoding: 'utf8'}); + s.stream.on('error', function (err) { + self.emit('error', err, s); + }); + } + }); +}; + + +/* BEGIN JSSTYLED */ +/** + * Close this logger. + * + * This closes streams (that it owns, as per 'endOnClose' attributes on + * streams), etc. Typically you **don't** need to bother calling this. +Logger.prototype.close = function () { + if (this._closed) { + return; + } + if (!this._isSimpleChild) { + self.streams.forEach(function (s) { + if (s.endOnClose) { + xxx('closing stream s:', s); + s.stream.end(); + s.endOnClose = false; + } + }); + } + this._closed = true; +} + */ +/* END JSSTYLED */ + + +/** + * Get/set the level of all streams on this logger. + * + * Get Usage: + * // Returns the current log level (lowest level of all its streams). + * log.level() -> INFO + * + * Set Usage: + * log.level(INFO) // set all streams to level INFO + * log.level('info') // can use 'info' et al aliases + */ +Logger.prototype.level = function level(value) { + if (value === undefined) { + return this._level; + } + var newLevel = resolveLevel(value); + var len = this.streams.length; + for (var i = 0; i < len; i++) { + this.streams[i].level = newLevel; + } + this._level = newLevel; +} + + +/** + * Get/set the level of a particular stream on this logger. + * + * Get Usage: + * // Returns an array of the levels of each stream. + * log.levels() -> [TRACE, INFO] + * + * // Returns a level of the identified stream. + * log.levels(0) -> TRACE // level of stream at index 0 + * log.levels('foo') // level of stream with name 'foo' + * + * Set Usage: + * log.levels(0, INFO) // set level of stream 0 to INFO + * log.levels(0, 'info') // can use 'info' et al aliases + * log.levels('foo', WARN) // set stream named 'foo' to WARN + * + * Stream names: When streams are defined, they can optionally be given + * a name. For example, + * log = new Logger({ + * streams: [ + * { + * name: 'foo', + * path: '/var/log/my-service/foo.log' + * level: 'trace' + * }, + * ... + * + * @param name {String|Number} The stream index or name. + * @param value {Number|String} The level value (INFO) or alias ('info'). + * If not given, this is a 'get' operation. + * @throws {Error} If there is no stream with the given name. + */ +Logger.prototype.levels = function levels(name, value) { + if (name === undefined) { + assert.equal(value, undefined); + return this.streams.map( + function (s) { return s.level }); + } + var stream; + if (typeof (name) === 'number') { + stream = this.streams[name]; + if (stream === undefined) { + throw new Error('invalid stream index: ' + name); + } + } else { + var len = this.streams.length; + for (var i = 0; i < len; i++) { + var s = this.streams[i]; + if (s.name === name) { + stream = s; + break; + } + } + if (!stream) { + throw new Error(format('no stream with name "%s"', name)); + } + } + if (value === undefined) { + return stream.level; + } else { + var newLevel = resolveLevel(value); + stream.level = newLevel; + if (newLevel < this._level) { + this._level = newLevel; + } + } +} + + +/** + * Apply registered serializers to the appropriate keys in the given fields. + * + * Pre-condition: This is only called if there is at least one serializer. + * + * @param fields (Object) The log record fields. + * @param excludeFields (Object) Optional mapping of keys to `true` for + * keys to NOT apply a serializer. + */ +Logger.prototype._applySerializers = function (fields, excludeFields) { + var self = this; + + xxx('_applySerializers: excludeFields', excludeFields); + + // Check each serializer against these (presuming number of serializers + // is typically less than number of fields). + Object.keys(this.serializers).forEach(function (name) { + if (fields[name] === undefined || + (excludeFields && excludeFields[name])) + { + return; + } + xxx('_applySerializers; apply to "%s" key', name) + try { + fields[name] = self.serializers[name](fields[name]); + } catch (err) { + _warn(format('bunyan: ERROR: Exception thrown from the "%s" ' + + 'Bunyan serializer. This should never happen. This is a bug ' + + 'in that serializer function.\n%s', + name, err.stack || err)); + fields[name] = format('(Error in Bunyan log "%s" serializer ' + + 'broke field. See stderr for details.)', name); + } + }); +} + + +/** + * Emit a log record. + * + * @param rec {log record} + * @param noemit {Boolean} Optional. Set to true to skip emission + * and just return the JSON string. + */ +Logger.prototype._emit = function (rec, noemit) { + var i; + + // Lazily determine if this Logger has non-'raw' streams. If there are + // any, then we need to stringify the log record. + if (this.haveNonRawStreams === undefined) { + this.haveNonRawStreams = false; + for (i = 0; i < this.streams.length; i++) { + if (!this.streams[i].raw) { + this.haveNonRawStreams = true; + break; + } + } + } + + // Stringify the object (creates a warning str on error). + var str; + if (noemit || this.haveNonRawStreams) { + str = fastAndSafeJsonStringify(rec) + '\n'; + } + + if (noemit) + return str; + + var level = rec.level; + for (i = 0; i < this.streams.length; i++) { + var s = this.streams[i]; + if (s.level <= level) { + xxx('writing log rec "%s" to "%s" stream (%d <= %d): %j', + rec.msg, s.type, s.level, level, rec); + s.stream.write(s.raw ? rec : str); + } + }; + + return str; +} + + +/** + * Build a record object suitable for emitting from the arguments + * provided to the a log emitter. + */ +function mkRecord(log, minLevel, args) { + var excludeFields, fields, msgArgs; + if (args[0] instanceof Error) { + // `log.(err, ...)` + fields = { + // Use this Logger's err serializer, if defined. + err: (log.serializers && log.serializers.err + ? log.serializers.err(args[0]) + : Logger.stdSerializers.err(args[0])) + }; + excludeFields = {err: true}; + if (args.length === 1) { + msgArgs = [fields.err.message]; + } else { + msgArgs = args.slice(1); + } + } else if (typeof (args[0]) !== 'object' || Array.isArray(args[0])) { + // `log.(msg, ...)` + fields = null; + msgArgs = args.slice(); + } else if (Buffer.isBuffer(args[0])) { // `log.(buf, ...)` + // Almost certainly an error, show `inspect(buf)`. See bunyan + // issue #35. + fields = null; + msgArgs = args.slice(); + msgArgs[0] = util.inspect(msgArgs[0]); + } else { // `log.(fields, msg, ...)` + fields = args[0]; + if (fields && args.length === 1 && fields.err && + fields.err instanceof Error) + { + msgArgs = [fields.err.message]; + } else { + msgArgs = args.slice(1); + } + } + + // Build up the record object. + var rec = objCopy(log.fields); + var level = rec.level = minLevel; + var recFields = (fields ? objCopy(fields) : null); + if (recFields) { + if (log.serializers) { + log._applySerializers(recFields, excludeFields); + } + Object.keys(recFields).forEach(function (k) { + rec[k] = recFields[k]; + }); + } + rec.msg = format.apply(log, msgArgs); + if (!rec.time) { + rec.time = (new Date()); + } + rec.v = LOG_VERSION; + + return rec; +}; + + +/** + * Build a log emitter function for level minLevel. I.e. this is the + * creator of `log.info`, `log.error`, etc. + */ +function mkLogEmitter(minLevel) { + return function () { + var log = this; + var str = null; + var rec = null; + + if (!this._emit) { + /* + * Show this invalid Bunyan usage warning *once*. + * + * See for + * an example of how this can happen. + */ + var dedupKey = 'unbound'; + if (!_haveWarned[dedupKey]) { + _warn(format('bunyan usage error: Attempt to log ' + + 'with an unbound log method: `this` is: %s', util.inspect(this)), + dedupKey); + } + return; + } else if (arguments.length === 0) { // `log.()` + return (this._level <= minLevel); + } + + var msgArgs = new Array(arguments.length); + for (var i = 0; i < msgArgs.length; ++i) { + msgArgs[i] = arguments[i]; + } + + if (this._level <= minLevel) { + rec = mkRecord(log, minLevel, msgArgs); + str = this._emit(rec); + } + } +} + + +/** + * The functions below log a record at a specific level. + * + * Usages: + * log.() -> boolean is-trace-enabled + * log.( err, [ msg, ...]) + * log.( msg, ...) + * log.( fields, msg, ...) + * + * where is the lowercase version of the log level. E.g.: + * + * log.info() + * + * @params fields {Object} Optional set of additional fields to log. + * @params msg {String} Log message. This can be followed by additional + * arguments that are handled like + * [util.format](http://nodejs.org/docs/latest/api/all.html#util.format). + */ +Logger.prototype.trace = mkLogEmitter(TRACE); +Logger.prototype.debug = mkLogEmitter(DEBUG); +Logger.prototype.info = mkLogEmitter(INFO); +Logger.prototype.warn = mkLogEmitter(WARN); +Logger.prototype.error = mkLogEmitter(ERROR); +Logger.prototype.fatal = mkLogEmitter(FATAL); + + + +//---- Standard serializers +// A serializer is a function that serializes a JavaScript object to a +// JSON representation for logging. There is a standard set of presumed +// interesting objects in node.js-land. + +Logger.stdSerializers = {}; + +// Serialize an HTTP request. +Logger.stdSerializers.req = function (req) { + if (!req || !req.connection) + return req; + return { + method: req.method, + url: req.url, + headers: req.headers, + remoteAddress: req.connection.remoteAddress, + remotePort: req.connection.remotePort + }; + // Trailers: Skipping for speed. If you need trailers in your app, then + // make a custom serializer. + //if (Object.keys(trailers).length > 0) { + // obj.trailers = req.trailers; + //} +}; + +// Serialize an HTTP response. +Logger.stdSerializers.res = function (res) { + if (!res || !res.statusCode) + return res; + return { + statusCode: res.statusCode, + header: res._header + } +}; + + +/* + * This function dumps long stack traces for exceptions having a cause() + * method. The error classes from + * [verror](https://github.com/davepacheco/node-verror) and + * [restify v2.0](https://github.com/mcavage/node-restify) are examples. + * + * Based on `dumpException` in + * https://github.com/davepacheco/node-extsprintf/blob/master/lib/extsprintf.js + */ +function getFullErrorStack(ex) +{ + var ret = ex.stack || ex.toString(); + if (ex.cause && typeof (ex.cause) === 'function') { + var cex = ex.cause(); + if (cex) { + ret += '\nCaused by: ' + getFullErrorStack(cex); + } + } + return (ret); +} + +// Serialize an Error object +// (Core error properties are enumerable in node 0.4, not in 0.6). +Logger.stdSerializers.err = function (err) { + if (!err || !err.stack) + return err; + var obj = { + message: err.message, + name: err.name, + stack: getFullErrorStack(err), + code: err.code, + signal: err.signal + } + return obj; +}; + + +// A JSON stringifier that handles cycles safely - tracks seen values in a Set. +function safeCyclesSet() { + var seen = new Set(); + return function (key, val) { + if (!val || typeof (val) !== 'object') { + return val; + } + if (seen.has(val)) { + return '[Circular]'; + } + seen.add(val); + return val; + }; +} + +/** + * A JSON stringifier that handles cycles safely - tracks seen vals in an Array. + * + * Note: This approach has performance problems when dealing with large objects, + * see trentm/node-bunyan#445, but since this is the only option for node 0.10 + * and earlier (as Set was introduced in Node 0.12), it's used as a fallback + * when Set is not available. + */ +function safeCyclesArray() { + var seen = []; + return function (key, val) { + if (!val || typeof (val) !== 'object') { + return val; + } + if (seen.indexOf(val) !== -1) { + return '[Circular]'; + } + seen.push(val); + return val; + }; +} + +/** + * A JSON stringifier that handles cycles safely. + * + * Usage: JSON.stringify(obj, safeCycles()) + * + * Choose the best safe cycle function from what is available - see + * trentm/node-bunyan#445. + */ +var safeCycles = typeof (Set) !== 'undefined' ? safeCyclesSet : safeCyclesArray; + +/** + * A fast JSON.stringify that handles cycles and getter exceptions (when + * safeJsonStringify is installed). + * + * This function attempts to use the regular JSON.stringify for speed, but on + * error (e.g. JSON cycle detection exception) it falls back to safe stringify + * handlers that can deal with cycles and/or getter exceptions. + */ +function fastAndSafeJsonStringify(rec) { + try { + return JSON.stringify(rec); + } catch (ex) { + try { + return JSON.stringify(rec, safeCycles()); + } catch (e) { + if (!process.env.BUNYAN_TEST_NO_SAFE_JSON_STRINGIFY) { + return safeJson(rec); + } else { + var dedupKey = e.stack.split(/\n/g, 3).join('\n'); + _warn('bunyan: ERROR: Exception in ' + + '`JSON.stringify(rec)`. You can install the ' + + '"safe-json-stringify" module to have Bunyan fallback ' + + 'to safer stringification. Record:\n' + + _indent(format('%s\n%s', util.inspect(rec), e.stack)), + dedupKey); + return format('(Exception in JSON.stringify(rec): %j. ' + + 'See stderr for details.)', e.message); + } + } + } +} + + +/** + * RingBuffer is a Writable Stream that just stores the last N records in + * memory. + * + * @param options {Object}, with the following fields: + * + * - limit: number of records to keep in memory + */ +function RingBuffer(options) { + this.limit = options && options.limit ? options.limit : 100; + this.writable = true; + this.records = []; + EventEmitter.call(this); +} + +util.inherits(RingBuffer, EventEmitter); + +RingBuffer.prototype.write = function (record) { + if (!this.writable) + throw (new Error('RingBuffer has been ended already')); + + this.records.push(record); + + if (this.records.length > this.limit) + this.records.shift(); + + return (true); +}; + +RingBuffer.prototype.end = function () { + if (arguments.length > 0) + this.write.apply(this, Array.prototype.slice.call(arguments)); + this.writable = false; +}; + +RingBuffer.prototype.destroy = function () { + this.writable = false; + this.emit('close'); +}; + +RingBuffer.prototype.destroySoon = function () { + this.destroy(); +}; + + +let __dirname = path.dirname(fileURLToPath(import.meta.url)) +let pckg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'))) +const version = pckg.version + +//---- Exports + +Logger.TRACE = TRACE; +Logger.DEBUG = DEBUG; +Logger.INFO = INFO; +Logger.WARN = WARN; +Logger.ERROR = ERROR; +Logger.FATAL = FATAL; +Logger.resolveLevel = resolveLevel; +Logger.levelFromName = levelFromName; +Logger.nameFromLevel = nameFromLevel; + +Logger.VERSION = version; +Logger.LOG_VERSION = LOG_VERSION; + +Logger.createLogger = function createLogger(options) { + return new Logger(options); +}; + +Logger.RingBuffer = RingBuffer; + +// Useful for custom `type == 'raw'` streams that may do JSON stringification +// of log records themselves. Usage: +// var str = JSON.stringify(rec, bunyan.safeCycles()); +Logger.safeCycles = safeCycles; + +export default Logger diff --git a/lib/safe-json.mjs b/lib/safe-json.mjs new file mode 100644 index 0000000..75626a8 --- /dev/null +++ b/lib/safe-json.mjs @@ -0,0 +1,69 @@ +var hasProp = Object.prototype.hasOwnProperty + +function throwsMessage(err) { + return '[Throws: ' + (err ? err.message : '?') + ']' +} + +function safeGetValueFromPropertyOnObject(obj, property) { + if (hasProp.call(obj, property)) { + try { + return obj[property] + } + catch (err) { + return throwsMessage(err) + } + } + + return obj[property] +} + +function ensureProperties(obj) { + var seen = new WeakMap() + + function visit(obj) { + if (obj === null || typeof obj !== 'object') { + return obj + } + + if (seen.has(obj)) { + return '[Circular]' + } + + seen.set(obj, true) + + if (typeof obj.toJSON === 'function') { + try { + var fResult = visit(obj.toJSON()) + seen.delete(obj) + return fResult + } catch(err) { + seen.delete(obj) + return throwsMessage(err) + } + } + + if (Array.isArray(obj)) { + var aResult = obj.map(visit) + seen.delete(obj) + return aResult + } + + var result = Object.keys(obj).reduce(function(result, prop) { + // prevent faulty defined getter properties + result[prop] = visit(safeGetValueFromPropertyOnObject(obj, prop)) + return result + }, {}) + seen.delete(obj) + return result + } + + return visit(obj) +} + +function safeJson(data, replacer, space) { + return JSON.stringify(ensureProperties(data), replacer, space) +} + +safeJson.ensureProperties = ensureProperties + +export default safeJson diff --git a/package.json b/package.json index 484f677..a192e71 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,16 @@ { "name": "bunyan-lite", - "version": "1.0.1", + "version": "1.1.0", "description": "a lite version of bunyan, a JSON logging library for node.js services without dtrace or moment", "author": "Jonatan Nilsson (https://nfp.is)", "main": "./lib/bunyan.js", "bin": { - "bunyan": "./bin/bunyan" + "bunyan": "./bin/bunyan.mjs" }, "repository": { "type": "git", "url": "git://github.com/nfp-projects/node-bunyan-lite.git" }, - "engines": [ - "node >=0.10.0" - ], "keywords": [ "log", "logging", @@ -23,19 +20,16 @@ ], "license": "MIT", "dependencies": {}, - "// mv": "required for RotatingFileStream", - "optionalDependencies": { - "mv-lite": "~1", - "safe-json-stringify": "~1" - }, "devDependencies": { - "nodeunit": "0.9", - "ben": "0.0.0", - "markdown-toc": "0.12.x", - "verror": "1.3.3", - "vasync": "1.4.3" + "eltro": "^1.0.2" }, "scripts": { - "test": "make test" - } + "test": "eltro test/**/*.test.mjs -r dot" + }, + "files": [ + "LICENSE.txt", + "README.md", + "bin", + "lib" + ] } diff --git a/test/add-stream.test.js b/test/add-stream.test.js deleted file mode 100644 index 757274f..0000000 --- a/test/add-stream.test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2016 Trent Mick. All rights reserved. - * - * Test stream adding. - */ - -var bunyan = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var test = tap4nodeunit.test; - - -test('non-writables passed as stream', function (t) { - var things = ['process.stdout', {}]; - things.forEach(function (thing) { - function createLogger() { - bunyan.createLogger({ - name: 'foo', - stream: thing - }); - } - t.throws(createLogger, - /stream is not writable/, - '"stream" stream is not writable'); - }) - t.end(); -}); - -test('proper stream', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - stream: process.stdout - }); - t.ok('should not throw'); - t.end(); -}); diff --git a/test/add-stream.test.mjs b/test/add-stream.test.mjs new file mode 100644 index 0000000..f050d9a --- /dev/null +++ b/test/add-stream.test.mjs @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016 Trent Mick. All rights reserved. + * + * Test stream adding. + */ +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' + +t.test('non-writables passed as stream', function () { + var things = ['process.stdout', {}]; + things.forEach(function (thing) { + function createLogger() { + bunyan.createLogger({ + name: 'foo', + stream: thing + }); + } + assert.throws(createLogger, + /stream is not writable/); + }) +}); + +t.test('proper stream', function () { + var log = bunyan.createLogger({ + name: 'foo', + stream: process.stdout + }); +}); diff --git a/test/buffer.test.js b/test/buffer.test.js deleted file mode 100644 index 53c6376..0000000 --- a/test/buffer.test.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2012 Trent Mick. All rights reserved. - * Copyright (c) 2012 Joyent Inc. All rights reserved. - * - * Test logging with (accidental) usage of buffers. - */ - -var util = require('util'), - inspect = util.inspect, - format = util.format; -var bunyan = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - - -function Catcher() { - this.records = []; -} -Catcher.prototype.write = function (record) { - this.records.push(record); -} - -var catcher = new Catcher(); -var log = new bunyan.createLogger({ - name: 'buffer.test', - streams: [ - { - type: 'raw', - stream: catcher, - level: 'trace' - } - ] -}); - - -test('log.info(BUFFER)', function (t) { - var b = new Buffer('foo'); - - ['trace', - 'debug', - 'info', - 'warn', - 'error', - 'fatal'].forEach(function (lvl) { - log[lvl].call(log, b); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, inspect(b), - format('log.%s msg is inspect(BUFFER)', lvl)); - t.ok(rec['0'] === undefined, - 'no "0" array index key in record: ' + inspect(rec['0'])); - t.ok(rec['parent'] === undefined, - 'no "parent" array index key in record: ' + inspect(rec['parent'])); - - log[lvl].call(log, b, 'bar'); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, inspect(b) + ' bar', format( - 'log.%s(BUFFER, "bar") msg is inspect(BUFFER) + " bar"', lvl)); - }); - - t.end(); -}); - - -//test('log.info({buf: BUFFER})', function (t) { -// var b = new Buffer('foo'); -// -// // Really there isn't much Bunyan can do here. See -// // . An unwelcome hack would -// // be to monkey-patch in Buffer.toJSON. Bletch. -// log.info({buf: b}, 'my message'); -// var rec = catcher.records[catcher.records.length - 1]; -// -// t.end(); -//}); diff --git a/test/buffer.test.mjs b/test/buffer.test.mjs new file mode 100644 index 0000000..c770b3f --- /dev/null +++ b/test/buffer.test.mjs @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012 Trent Mick. All rights reserved. + * Copyright (c) 2012 Joyent Inc. All rights reserved. + * + * Test logging with (accidental) usage of buffers. + */ + +import util from 'util' +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' + +const inspect = util.inspect +const format = util.format + + +function Catcher() { + this.records = [] +} +Catcher.prototype.write = function (record) { + this.records.push(record) +} + +let catcher = new Catcher() +let log = new bunyan.createLogger({ + name: 'buffer.test', + streams: [ + { + type: 'raw', + stream: catcher, + level: 'trace' + } + ] +}) + + +t.test('log.info(BUFFER)', function () { + let b = Buffer.from('foo') + + let testLevels = ['trace', + 'debug', + 'info', + 'warn', + 'error', + 'fatal'] + + testLevels.forEach(function (lvl) { + log[lvl].call(log, b) + let rec = catcher.records[catcher.records.length - 1] + assert.strictEqual(rec.msg, inspect(b), + format('log.%s msg is inspect(BUFFER)', lvl)) + assert.ok(rec['0'] === undefined, + 'no "0" array index key in record: ' + inspect(rec['0'])) + assert.ok(rec['parent'] === undefined, + 'no "parent" array index key in record: ' + inspect(rec['parent'])) + + log[lvl].call(log, b, 'bar') + rec = catcher.records[catcher.records.length - 1] + assert.strictEqual(rec.msg, inspect(b) + ' bar', format( + 'log.%s(BUFFER, "bar") msg is inspect(BUFFER) + " bar"', lvl)) + }) +}) + + +//test('log.info({buf: BUFFER})', function (t) { +// let b = new Buffer('foo') +// +// // Really there isn't much Bunyan can do here. See +// // . An unwelcome hack would +// // be to monkey-patch in Buffer.toJSON. Bletch. +// log.info({buf: b}, 'my message') +// let rec = catcher.records[catcher.records.length - 1] +// +// t.end() +//}) diff --git a/test/child-behaviour.test.js b/test/child-behaviour.test.mjs similarity index 51% rename from test/child-behaviour.test.js rename to test/child-behaviour.test.mjs index ddc2896..80d717a 100644 --- a/test/child-behaviour.test.js +++ b/test/child-behaviour.test.mjs @@ -4,16 +4,8 @@ * Test some `.child(...)` behaviour. */ -var bunyan = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' function CapturingStream(recs) { @@ -24,10 +16,9 @@ CapturingStream.prototype.write = function (rec) { } - -test('child can add stream', function (t) { - var dadStream = new CapturingStream(); - var dad = bunyan.createLogger({ +t.test('child can add stream', function () { + let dadStream = new CapturingStream(); + let dad = bunyan.createLogger({ name: 'surname', streams: [ { type: 'raw', @@ -36,8 +27,8 @@ test('child can add stream', function (t) { } ] }); - var sonStream = new CapturingStream(); - var son = dad.child({ + let sonStream = new CapturingStream(); + let son = dad.child({ component: 'son', streams: [ { type: 'raw', @@ -50,21 +41,20 @@ test('child can add stream', function (t) { dad.debug('debug from dad'); son.debug('debug from son'); - var rec; - t.equal(dadStream.recs.length, 1); + let rec; + assert.equal(dadStream.recs.length, 1); rec = dadStream.recs[0]; - t.equal(rec.msg, 'info from dad'); - t.equal(sonStream.recs.length, 1); + assert.equal(rec.msg, 'info from dad'); + assert.equal(sonStream.recs.length, 1); rec = sonStream.recs[0]; - t.equal(rec.msg, 'debug from son'); + assert.equal(rec.msg, 'debug from son'); - t.end(); }); -test('child can set level of inherited streams', function (t) { - var dadStream = new CapturingStream(); - var dad = bunyan.createLogger({ +t.test('child can set level of inherited streams', function () { + let dadStream = new CapturingStream(); + let dad = bunyan.createLogger({ name: 'surname', streams: [ { type: 'raw', @@ -75,7 +65,7 @@ test('child can set level of inherited streams', function (t) { // Intention here is that the inherited `dadStream` logs at 'debug' level // for the son. - var son = dad.child({ + let son = dad.child({ component: 'son', level: 'debug' }); @@ -84,20 +74,19 @@ test('child can set level of inherited streams', function (t) { dad.debug('debug from dad'); son.debug('debug from son'); - var rec; - t.equal(dadStream.recs.length, 2); + let rec; + assert.equal(dadStream.recs.length, 2); rec = dadStream.recs[0]; - t.equal(rec.msg, 'info from dad'); + assert.equal(rec.msg, 'info from dad'); rec = dadStream.recs[1]; - t.equal(rec.msg, 'debug from son'); + assert.equal(rec.msg, 'debug from son'); - t.end(); }); -test('child can set level of inherited streams and add streams', function (t) { - var dadStream = new CapturingStream(); - var dad = bunyan.createLogger({ +t.test('child can set level of inherited streams and add streams', function () { + let dadStream = new CapturingStream(); + let dad = bunyan.createLogger({ name: 'surname', streams: [ { type: 'raw', @@ -108,8 +97,8 @@ test('child can set level of inherited streams and add streams', function (t) { // Intention here is that the inherited `dadStream` logs at 'debug' level // for the son. - var sonStream = new CapturingStream(); - var son = dad.child({ + let sonStream = new CapturingStream(); + let son = dad.child({ component: 'son', level: 'trace', streams: [ { @@ -124,21 +113,20 @@ test('child can set level of inherited streams and add streams', function (t) { son.trace('trace from son'); son.debug('debug from son'); - t.equal(dadStream.recs.length, 3); - t.equal(dadStream.recs[0].msg, 'info from dad'); - t.equal(dadStream.recs[1].msg, 'trace from son'); - t.equal(dadStream.recs[2].msg, 'debug from son'); + assert.equal(dadStream.recs.length, 3); + assert.equal(dadStream.recs[0].msg, 'info from dad'); + assert.equal(dadStream.recs[1].msg, 'trace from son'); + assert.equal(dadStream.recs[2].msg, 'debug from son'); - t.equal(sonStream.recs.length, 1); - t.equal(sonStream.recs[0].msg, 'debug from son'); + assert.equal(sonStream.recs.length, 1); + assert.equal(sonStream.recs[0].msg, 'debug from son'); - t.end(); }); // issue #291 -test('child should not lose parent "hostname"', function (t) { - var stream = new CapturingStream(); - var dad = bunyan.createLogger({ +t.test('child should not lose parent "hostname"', function () { + let stream = new CapturingStream(); + let dad = bunyan.createLogger({ name: 'hostname-test', hostname: 'bar0', streams: [ { @@ -147,15 +135,14 @@ test('child should not lose parent "hostname"', function (t) { level: 'info' } ] }); - var son = dad.child({component: 'son'}); + let son = dad.child({component: 'son'}); dad.info('HI'); son.info('hi'); - t.equal(stream.recs.length, 2); - t.equal(stream.recs[0].hostname, 'bar0'); - t.equal(stream.recs[1].hostname, 'bar0'); - t.equal(stream.recs[1].component, 'son'); + assert.equal(stream.recs.length, 2); + assert.equal(stream.recs[0].hostname, 'bar0'); + assert.equal(stream.recs[1].hostname, 'bar0'); + assert.equal(stream.recs[1].component, 'son'); - t.end(); }); diff --git a/test/cli-client-req.test.js b/test/cli-client-req.test.js deleted file mode 100644 index 8ef4a09..0000000 --- a/test/cli-client-req.test.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2017, Trent Mick. - * - * Test the bunyan CLI's handling of the "client_req" field. - * "client_req" is a common-ish Bunyan log field from restify-clients. See: - * // JSSTYLED - * https://github.com/restify/clients/blob/85374f87db9f4469de2605b6b15632b317cc12be/lib/helpers/bunyan.js#L213 - */ - -var exec = require('child_process').exec; -var fs = require('fs'); -var path = require('path'); -var _ = require('util').format; -var vasync = require('vasync'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -// ---- globals - -var BUNYAN = path.resolve(__dirname, '../bin/bunyan'); - - -// ---- tests - -test('client_req extra newlines, client_res={} (pull #252)', function (t) { - var expect = [ - /* BEGIN JSSTYLED */ - '[2016-02-10T07:28:40.510Z] TRACE: aclientreq/23280 on danger0.local: request sent', - ' GET /--ping HTTP/1.1', - '[2016-02-10T07:28:41.419Z] TRACE: aclientreq/23280 on danger0.local: Response received', - ' HTTP/1.1 200 OK', - ' request-id: e8a5a700-cfc7-11e5-a3dc-3b85d20f26ef', - ' content-type: application/json' - /* END JSSTYLED */ - ].join('\n') + '\n'; - exec(_('%s %s/corpus/clientreqres.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - t.end(); - }); -}); - - -test('client_req.address is not used for Host header in 2.x (issue #504)', - function (t) { - exec(_('%s %s/corpus/client-req-with-address.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, [ - // JSSTYLED - '[2017-05-12T23:59:15.877Z] TRACE: minfo/66266 on sharptooth.local: request sent (client_req.address=127.0.0.1)', - ' HEAD /dap/stor HTTP/1.1', - ' accept: application/json, */*', - ' host: foo.example.com', - ' date: Fri, 12 May 2017 23:59:15 GMT', - '' - ].join('\n')); - t.end(); - }); -}); diff --git a/test/cli-client-req.test.mjs b/test/cli-client-req.test.mjs new file mode 100644 index 0000000..bbd4f87 --- /dev/null +++ b/test/cli-client-req.test.mjs @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017, Trent Mick. + * + * Test the bunyan CLI's handling of the "client_req" field. + * "client_req" is a common-ish Bunyan log field from restify-clients. See: + * // JSSTYLED + * https://github.com/restify/clients/blob/85374f87db9f4469de2605b6b15632b317cc12be/lib/helpers/bunyan.js#L213 + */ + +import { exec, dirname } from './helper.mjs' +import { Eltro as t, assert} from 'eltro' + +// ---- tests + +t.test('client_req extra newlines, client_res={} (pull #252)', async function () { + const expected = [ + /* BEGIN JSSTYLED */ + '[2016-02-10T07:28:40.510Z] TRACE: aclientreq/23280 on danger0.local: request sent', + ' GET /--ping HTTP/1.1', + '[2016-02-10T07:28:41.419Z] TRACE: aclientreq/23280 on danger0.local: Response received', + ' HTTP/1.1 200 OK', + ' request-id: e8a5a700-cfc7-11e5-a3dc-3b85d20f26ef', + ' content-type: application/json' + /* END JSSTYLED */ + ].join('\n') + '\n'; + + let res = await exec(dirname('/corpus/clientreqres.log')) + assert.strictEqual(res.stdout, expected) +}); + + +t.test('client_req.address is not used for Host header in 2.x (issue #504)', async function () { + const expected = [ + // JSSTYLED + '[2017-05-12T23:59:15.877Z] TRACE: minfo/66266 on sharptooth.local: request sent (client_req.address=127.0.0.1)', + ' HEAD /dap/stor HTTP/1.1', + ' accept: application/json, */*', + ' host: foo.example.com', + ' date: Fri, 12 May 2017 23:59:15 GMT', + '' + ].join('\n') + + let res = await exec(dirname('/corpus/client-req-with-address.log')) + assert.strictEqual(res.stdout, expected) +}); diff --git a/test/cli-res.test.js b/test/cli-res.test.js deleted file mode 100644 index 75cb5dd..0000000 --- a/test/cli-res.test.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2017, Trent Mick. - * - * Test the bunyan CLI's handling of the "res" field. - */ - -var exec = require('child_process').exec; -var fs = require('fs'); -var path = require('path'); -var _ = require('util').format; -var vasync = require('vasync'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -// ---- globals - -var BUNYAN = path.resolve(__dirname, '../bin/bunyan'); - - -// ---- tests - -test('res with "header" string (issue #444)', function (t) { - var expect = [ - /* BEGIN JSSTYLED */ - '[2017-08-02T22:37:34.798Z] INFO: res-header/76488 on danger0.local: response sent', - ' HTTP/1.1 200 OK', - ' Foo: bar', - ' Date: Wed, 02 Aug 2017 22:37:34 GMT', - ' Connection: keep-alive', - ' Content-Length: 21' - /* END JSSTYLED */ - ].join('\n') + '\n'; - exec(_('%s %s/corpus/res-header.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - t.end(); - }); -}); - -test('res without "header"', function (t) { - var expect = [ - /* BEGIN JSSTYLED */ - '[2017-08-02T22:37:34.798Z] INFO: res-header/76488 on danger0.local: response sent', - ' HTTP/1.1 200 OK' - /* END JSSTYLED */ - ].join('\n') + '\n'; - exec(_('%s %s/corpus/res-without-header.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - t.end(); - }); -}); diff --git a/test/cli-res.test.mjs b/test/cli-res.test.mjs new file mode 100644 index 0000000..6e0abdb --- /dev/null +++ b/test/cli-res.test.mjs @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2017, Trent Mick. + * + * Test the bunyan CLI's handling of the "res" field. + */ + +import { exec, dirname } from './helper.mjs' +import { Eltro as t, assert} from 'eltro' + +// ---- tests + +t.test('res with "header" string (issue #444)', async function () { + const expected = [ + /* BEGIN JSSTYLED */ + '[2017-08-02T22:37:34.798Z] INFO: res-header/76488 on danger0.local: response sent', + ' HTTP/1.1 200 OK', + ' Foo: bar', + ' Date: Wed, 02 Aug 2017 22:37:34 GMT', + ' Connection: keep-alive', + ' Content-Length: 21' + /* END JSSTYLED */ + ].join('\n') + '\n'; + + let res = await exec(dirname('/corpus/res-header.log')) + assert.strictEqual(res.stdout, expected) +}); + +t.test('res without "header"', async function () { + const expected = [ + /* BEGIN JSSTYLED */ + '[2017-08-02T22:37:34.798Z] INFO: res-header/76488 on danger0.local: response sent', + ' HTTP/1.1 200 OK' + /* END JSSTYLED */ + ].join('\n') + '\n'; + + let res = await exec(dirname('/corpus/res-without-header.log')) + assert.strictEqual(res.stdout, expected) +}); diff --git a/test/cli.test.js b/test/cli.test.js deleted file mode 100644 index d6b0ff1..0000000 --- a/test/cli.test.js +++ /dev/null @@ -1,489 +0,0 @@ -/* - * Copyright (c) 2015 Trent Mick. All rights reserved. - * - * Test the `bunyan` CLI. - */ - -var p = console.warn; -var exec = require('child_process').exec; -var fs = require('fs'); -var path = require('path'); -var _ = require('util').format; -var vasync = require('vasync'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -// ---- globals - -var BUNYAN = path.resolve(__dirname, '../bin/bunyan'); - - -// ---- support stuff - -/** - * Copies over all keys in `from` to `to`, or - * to a new object if `to` is not given. - */ -function objCopy(from, to) { - if (to === undefined) { - to = {}; - } - for (var k in from) { - to[k] = from[k]; - } - return to; -} - - -// ---- tests - -test('--version', function (t) { - var version = require('../package.json').version; - exec(BUNYAN + ' --version', function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, 'bunyan ' + version + '\n'); - t.end(); - }); -}); - -test('--help', function (t) { - exec(BUNYAN + ' --help', function (err, stdout, stderr) { - t.ifError(err) - t.ok(stdout.indexOf('General options:') !== -1); - t.end(); - }); -}); - -test('-h', function (t) { - exec(BUNYAN + ' -h', function (err, stdout, stderr) { - t.ifError(err) - t.ok(stdout.indexOf('General options:') !== -1); - t.end(); - }); -}); - -test('--bogus', function (t) { - exec(BUNYAN + ' --bogus', function (err, stdout, stderr) { - t.ok(err, 'should error out') - t.equal(err.code, 1, '... with exit code 1') - t.end(); - }); -}); - -test('simple.log', function (t) { - exec(_('%s %s/corpus/simple.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, - '[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: ' - + 'My message\n'); - t.end(); - }); -}); -test('cat simple.log', function (t) { - exec(_('cat %s/corpus/simple.log | %s', __dirname, BUNYAN), - function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, - /* JSSTYLED */ - '[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message\n'); - t.end(); - } - ); -}); - -test('time: simple.log utc long', function (t) { - exec(_('%s -o long --time utc %s/corpus/simple.log', BUNYAN, __dirname), - {env: process.env}, function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, - '[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: ' - + 'My message\n'); - t.end(); - }); -}); -test('time: simple.log utc short', function (t) { - exec(_('%s -o short %s/corpus/simple.log', BUNYAN, __dirname), - {env: process.env}, function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, - '22:56:52.856Z INFO myservice: ' - + 'My message\n'); - t.end(); - }); -}); - -test('simple.log with color', function (t) { - exec(_('%s --color %s/corpus/simple.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, - /* JSSTYLED */ - '[2012-02-08T22:56:52.856Z] \u001b[36m INFO\u001b[39m: myservice/123 on example.com: \u001b[36mMy message\u001b[39m\n\u001b[0m'); - t.end(); - }); -}); - -test('extrafield.log', function (t) { - exec(_('%s %s/corpus/extrafield.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, - '[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: ' - + 'My message (extra=field)\n'); - t.end(); - }); -}); -test('extrafield.log with color', function (t) { - exec(_('%s --color %s/corpus/extrafield.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, - '[2012-02-08T22:56:52.856Z] \u001b[36m INFO\u001b[39m: ' - + 'myservice/123 ' - + 'on example.com: \u001b[36mMy message\u001b[39m' - + ' (extra=field)\n\u001b[0m'); - t.end(); - }); -}); - -test('bogus.log', function (t) { - exec(_('%s %s/corpus/bogus.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, 'not a JSON line\n{"hi": "there"}\n'); - t.end(); - }); -}); - -test('bogus.log -j', function (t) { - exec(_('%s -j %s/corpus/bogus.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err) - t.equal(stdout, 'not a JSON line\n{"hi": "there"}\n'); - t.end(); - }); -}); - -test('all.log', function (t) { - exec(_('%s %s/corpus/all.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - // Just make sure don't blow up on this. - t.ifError(err) - t.end(); - }); -}); - -test('simple.log doesnotexist1.log doesnotexist2.log', function (t) { - exec(_('%s %s/corpus/simple.log doesnotexist1.log doesnotexist2.log', - BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ok(err) - t.equal(err.code, 2) - t.equal(stdout, - /* JSSTYLED */ - '[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message\n'); - // Note: node v0.6.10: - // ENOENT, no such file or directory 'asdf.log' - // but node v0.6.14: - // ENOENT, open 'asdf.log' - // io.js 2.2 (at least): - // ENOENT: no such file or directory, open 'doesnotexist1.log' - var matches = [ - /^bunyan: ENOENT.*?, open 'doesnotexist1.log'/m, - /^bunyan: ENOENT.*?, open 'doesnotexist2.log'/m, - ]; - matches.forEach(function (match) { - t.ok(match.test(stderr), 'stderr matches ' + match.toString()); - }); - t.end(); - } - ); -}); - -test('multiple logs', function (t) { - var cmd = _('%s %s/corpus/log1.log %s/corpus/log2.log', - BUNYAN, __dirname, __dirname); - exec(cmd, function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, [ - /* BEGIN JSSTYLED */ - '[2012-05-08T16:57:55.586Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T16:58:55.586Z] INFO: agent2/73267 on headnode: message\n', - '[2012-05-08T17:01:49.339Z] INFO: agent2/73267 on headnode: message\n', - '[2012-05-08T17:02:47.404Z] INFO: agent2/73267 on headnode: message\n', - '[2012-05-08T17:02:49.339Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T17:02:57.404Z] INFO: agent2/73267 on headnode: message\n', - '[2012-05-08T17:08:01.105Z] INFO: agent2/76156 on headnode: message\n', - /* END JSSTYLED */ - ].join('')); - t.end(); - }); -}); - -test('multiple logs, bunyan format', function (t) { - var cmd = _('%s -o bunyan %s/corpus/log1.log %s/corpus/log2.log', - BUNYAN, __dirname, __dirname); - exec(cmd, function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, [ - /* BEGIN JSSTYLED */ - '{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T16:57:55.586Z","v":0}', - '{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T16:58:55.586Z","v":0}', - '{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:01:49.339Z","v":0}', - '{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:47.404Z","v":0}', - '{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.339Z","v":0}', - '{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.404Z","v":0}', - '{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.404Z","v":0}', - '{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:57.404Z","v":0}', - '{"name":"agent2","pid":76156,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:08:01.105Z","v":0}', - '' - /* END JSSTYLED */ - ].join('\n')); - t.end(); - }); -}); - -test('log1.log.gz', function (t) { - exec(_('%s %s/corpus/log1.log.gz', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, [ - /* BEGIN JSSTYLED */ - '[2012-05-08T16:57:55.586Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T17:02:49.339Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', - /* END JSSTYLED */ - ].join('')); - t.end(); - }); -}); - -test('mixed text and gzip logs', function (t) { - var cmd = _('%s %s/corpus/log1.log.gz %s/corpus/log2.log', - BUNYAN, __dirname, __dirname); - exec(cmd, function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, [ - /* BEGIN JSSTYLED */ - '[2012-05-08T16:57:55.586Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T16:58:55.586Z] INFO: agent2/73267 on headnode: message\n', - '[2012-05-08T17:01:49.339Z] INFO: agent2/73267 on headnode: message\n', - '[2012-05-08T17:02:47.404Z] INFO: agent2/73267 on headnode: message\n', - '[2012-05-08T17:02:49.339Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', - '[2012-05-08T17:02:57.404Z] INFO: agent2/73267 on headnode: message\n', - '[2012-05-08T17:08:01.105Z] INFO: agent2/76156 on headnode: message\n', - /* END JSSTYLED */ - ].join('')); - t.end(); - }); -}); - -test('--level 40', function (t) { - expect = [ - /* BEGIN JSSTYLED */ - '# levels\n', - '[2012-02-08T22:56:53.856Z] WARN: myservice/123 on example.com: My message\n', - '[2012-02-08T22:56:54.856Z] ERROR: myservice/123 on example.com: My message\n', - '[2012-02-08T22:56:55.856Z] LVL55: myservice/123 on example.com: My message\n', - '[2012-02-08T22:56:56.856Z] FATAL: myservice/123 on example.com: My message\n', - '\n', - '# extra fields\n', - '\n', - '# bogus\n', - 'not a JSON line\n', - '{"hi": "there"}\n' - /* END JSSTYLED */ - ].join(''); - exec(_('%s -l 40 %s/corpus/all.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - exec(_('%s --level 40 %s/corpus/all.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - t.end(); - }); - }); -}); - -test('--condition "this.level === 10 && this.pid === 123"', function (t) { - var expect = [ - '# levels\n', - /* JSSTYLED */ - '[2012-02-08T22:56:50.856Z] TRACE: myservice/123 on example.com: My message\n', - '\n', - '# extra fields\n', - '\n', - '# bogus\n', - 'not a JSON line\n', - '{"hi": "there"}\n' - ].join(''); - var cmd = _('%s -c "this.level === 10 && this.pid === 123"' - + ' %s/corpus/all.log', BUNYAN, __dirname); - exec(cmd, function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - var cmd = _( - '%s --condition "this.level === 10 && this.pid === 123"' - + ' %s/corpus/all.log', BUNYAN, __dirname); - exec(cmd, function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - t.end(); - }); - }); -}); - -test('--condition "this.level === TRACE', function (t) { - var expect = [ - '# levels\n', - /* JSSTYLED */ - '[2012-02-08T22:56:50.856Z] TRACE: myservice/123 on example.com: My message\n', - '\n', - '# extra fields\n', - '\n', - '# bogus\n', - 'not a JSON line\n', - '{"hi": "there"}\n' - ].join(''); - var cmd = _('%s -c "this.level === TRACE" %s/corpus/all.log', - BUNYAN, __dirname); - exec(cmd, function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - t.done(); - }); -}); - -// multiple -test('multiple --conditions', function (t) { - var expect = [ - '# levels\n', - /* JSSTYLED */ - '[2012-02-08T22:56:53.856Z] WARN: myservice/123 on example.com: My message\n', - '\n', - '# extra fields\n', - '\n', - '# bogus\n', - 'not a JSON line\n', - '{"hi": "there"}\n' - ].join(''); - exec(_('%s %s/corpus/all.log -c "this.level === 40" -c "this.pid === 123"', - BUNYAN, __dirname), function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - t.end(); - }); -}); - -// https://github.com/trentm/node-bunyan/issues/30 -// -// One of the records in corpus/withreq.log has a 'req' -// field with no 'headers'. Ditto for the 'res' field. -test('robust req handling', function (t) { - var expect = [ - /* BEGIN JSSTYLED */ - '[2012-08-08T10:25:47.636Z] DEBUG: amon-master/12859 on 9724a190-27b6-4fd8-830b-a574f839c67d: headAgentProbes respond (req_id=cce79d15-ffc2-487c-a4e4-e940bdaac31e, route=HeadAgentProbes, contentMD5=11FxOYiYfpMxmANj4kGJzg==)', - '[2012-08-08T10:25:47.637Z] INFO: amon-master/12859 on 9724a190-27b6-4fd8-830b-a574f839c67d: HeadAgentProbes handled: 200 (req_id=cce79d15-ffc2-487c-a4e4-e940bdaac31e, audit=true, remoteAddress=10.2.207.2, remotePort=50394, latency=3, secure=false, _audit=true, req.version=*)', - ' HEAD /agentprobes?agent=ccf92af9-0b24-46b6-ab60-65095fdd3037 HTTP/1.1', - ' accept: application/json', - ' content-type: application/json', - ' host: 10.2.207.16', - ' connection: keep-alive', - ' --', - ' HTTP/1.1 200 OK', - ' content-md5: 11FxOYiYfpMxmANj4kGJzg==', - ' access-control-allow-origin: *', - ' access-control-allow-headers: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version', - ' access-control-allow-methods: HEAD', - ' access-control-expose-headers: X-Api-Version, X-Request-Id, X-Response-Time', - ' connection: Keep-Alive', - ' date: Wed, 08 Aug 2012 10:25:47 GMT', - ' server: Amon Master/1.0.0', - ' x-request-id: cce79d15-ffc2-487c-a4e4-e940bdaac31e', - ' x-response-time: 3', - ' --', - ' route: {', - ' "name": "HeadAgentProbes",', - ' "version": false', - ' }', - '[2012-08-08T10:25:47.637Z] INFO: amon-master/12859 on 9724a190-27b6-4fd8-830b-a574f839c67d: HeadAgentProbes handled: 200 (req_id=cce79d15-ffc2-487c-a4e4-e940bdaac31e, audit=true, remoteAddress=10.2.207.2, remotePort=50394, latency=3, secure=false, _audit=true, req.version=*)', - ' HEAD /agentprobes?agent=ccf92af9-0b24-46b6-ab60-65095fdd3037 HTTP/1.1', - ' --', - ' HTTP/1.1 200 OK', - ' --', - ' route: {', - ' "name": "HeadAgentProbes",', - ' "version": false', - ' }' - /* END JSSTYLED */ - ].join('\n') + '\n'; - exec(_('%s %s/corpus/withreq.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - t.end(); - }); -}); - -// Some past crashes from issues. -test('should not crash on corpus/old-crashers/*.log', function (t) { - var oldCrashers = fs.readdirSync( - path.resolve(__dirname, 'corpus/old-crashers')) - .filter(function (f) { return f.slice(-4) === '.log'; }); - vasync.forEachPipeline({ - inputs: oldCrashers, - func: function (logName, next) { - exec(_('%s %s/corpus/old-crashers/%s', BUNYAN, __dirname, logName), - function (err, stdout, stderr) { - next(err); - }); - } - }, function (err, results) { - t.ifError(err); - t.end(); - }); -}); - - -test('should only show nonempty response bodies', function (t) { - var expect = [ - /* BEGIN JSSTYLED */ - '[2016-02-10T07:28:41.419Z] INFO: myservice/123 on example.com: UnauthorizedError', - ' HTTP/1.1 401 Unauthorized', - ' content-type: text/plain', - ' date: Sat, 07 Mar 2015 06:58:43 GMT', - '[2016-02-10T07:28:41.419Z] INFO: myservice/123 on example.com: hello', - ' HTTP/1.1 200 OK', - ' content-type: text/plain', - ' content-length: 0', - ' date: Sat, 07 Mar 2015 06:58:43 GMT', - ' ', - ' hello', - '[2016-02-10T07:28:41.419Z] INFO: myservice/123 on example.com: UnauthorizedError', - ' HTTP/1.1 401 Unauthorized', - ' content-type: text/plain', - ' date: Sat, 07 Mar 2015 06:58:43 GMT' - /* END JSSTYLED */ - ].join('\n') + '\n'; - exec(_('%s %s/corpus/content-length-0-res.log', BUNYAN, __dirname), - function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, expect); - t.end(); - }); -}); diff --git a/test/cli.test.mjs b/test/cli.test.mjs new file mode 100644 index 0000000..b104a03 --- /dev/null +++ b/test/cli.test.mjs @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2015 Trent Mick. All rights reserved. + * + * Test the `bunyan` CLI. + */ + +import fs from 'fs' +import path from 'path' +import { exec, dirname } from './helper.mjs' +import { Eltro as t, assert} from 'eltro' + +// ---- assertable variables + +const catter = process.platform === 'win32' ? 'type' : 'cat' +const assertSimpleLog = '[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message\n' + +// ---- tests + +t.test('--version', async function () { + let pckg = JSON.parse(fs.readFileSync(dirname('/../package.json'))) + const version = pckg.version + + let res = await exec('--version') + assert.strictEqual(res.stdout, 'bunyan ' + version + '\n') +}); + +t.test('--help', async function () { + let res = await exec('--help') + assert.match(res.stdout, /General options:/) +}); + +t.test('-h', async function () { + let res = await exec('-h') + assert.match(res.stdout, /General options:/) +}); + +t.test('--bogus', async function () { + let err = await assert.isRejected(exec('--bogus')) + assert.strictEqual(err.code, 1) +}); + +t.test('simple.log', async function () { + let res = await exec(dirname('/corpus/simple.log')) + assert.strictEqual(res.stdout, assertSimpleLog) +}); + +t.test(`${catter} simple.log`, async function () { + let res = await exec('', `${catter} ${dirname('/corpus/simple.log')} | node `) + assert.strictEqual(res.stdout, assertSimpleLog) +}); + +t.test('time: simple.log utc long', async function () { + let res = await exec('-o long --time utc ' + dirname('/corpus/simple.log')) + assert.strictEqual(res.stdout, assertSimpleLog) +}); + +t.test('time: simple.log utc short', async function () { + let res = await exec('-o short ' + dirname('/corpus/simple.log')) + assert.strictEqual(res.stdout, '22:56:52.856Z INFO myservice: My message\n') +}); + +t.test('simple.log with color', async function () { + let res = await exec(dirname('/corpus/simple.log')) + assert.notMatch(res.stdout, /\[2012-02-08T22:56:52.856Z\] [^ ]+ INFO[^:]+:/) + res = await exec('--color ' + dirname('/corpus/simple.log')) + assert.match(res.stdout, /\[2012-02-08T22:56:52.856Z\] [^ ]+ INFO[^:]+:/) +}); + +t.test('extrafield.log', async function () { + let res = await exec(dirname('/corpus/extrafield.log')) + assert.strictEqual(res.stdout, '[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message (extra=field)\n') +}); + +t.test('extrafield.log with color', async function () { + let res = await exec(dirname('/corpus/extrafield.log')) + assert.notMatch(res.stdout, /My message[^ ]+ \(extra=field\)\n.+/) + res = await exec('--color ' + dirname('/corpus/extrafield.log')) + assert.match(res.stdout, /My message[^ ]+ \(extra=field\)\n.+/) +}); + +t.test('bogus.log', async function () { + let res = await exec(dirname('/corpus/bogus.log')) + assert.strictEqual(res.stdout, 'not a JSON line\n{"hi": "there"}\n') +}); + +t.test('bogus.log -j', async function () { + let res = await exec('-j ' + dirname('/corpus/bogus.log')) + assert.strictEqual(res.stdout, 'not a JSON line\n{"hi": "there"}\n') +}); + +t.test('all.log', async function () { + // Just make sure don't blow up on this. + await exec(dirname('/corpus/all.log')) +}); + +t.test('simple.log doesnotexist1.log doesnotexist2.log', async function () { + let res = await assert.isRejected(exec(dirname('/corpus/simple.log') + ' doesnotexist1.log doesnotexist2.log')) + assert.strictEqual(res.stdout, '[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message\n') + + // Note: node v0.6.10: + // ENOENT, no such file or directory 'asdf.log' + // but node v0.6.14: + // ENOENT, open 'asdf.log' + // io.js 2.2 (at least): + // ENOENT: no such file or directory, open 'doesnotexist1.log' + let matches = [ + /^bunyan: ENOENT.*?, open '.+doesnotexist1.log'/m, + /^bunyan: ENOENT.*?, open '.+doesnotexist2.log'/m, + ]; + matches.forEach(function (match) { + assert.match(res.stderr, match); + }); +}); + +t.test('multiple logs', async function () { + let res = await exec(dirname('/corpus/log1.log') + ' ' + dirname('/corpus/log2.log')) + assert.strictEqual(res.stdout, [ + '[2012-05-08T16:57:55.586Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T16:58:55.586Z] INFO: agent2/73267 on headnode: message\n', + '[2012-05-08T17:01:49.339Z] INFO: agent2/73267 on headnode: message\n', + '[2012-05-08T17:02:47.404Z] INFO: agent2/73267 on headnode: message\n', + '[2012-05-08T17:02:49.339Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T17:02:57.404Z] INFO: agent2/73267 on headnode: message\n', + '[2012-05-08T17:08:01.105Z] INFO: agent2/76156 on headnode: message\n', + ].join('')) +}); + +t.test('multiple logs, bunyan format', async function () { + let res = await exec('-o bunyan ' + dirname('/corpus/log1.log') + ' ' + dirname('/corpus/log2.log')) + assert.strictEqual(res.stdout, [ + '{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T16:57:55.586Z","v":0}\n', + '{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T16:58:55.586Z","v":0}\n', + '{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:01:49.339Z","v":0}\n', + '{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:47.404Z","v":0}\n', + '{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.339Z","v":0}\n', + '{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.404Z","v":0}\n', + '{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.404Z","v":0}\n', + '{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:57.404Z","v":0}\n', + '{"name":"agent2","pid":76156,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:08:01.105Z","v":0}\n', + ].join('')) +}); + +t.test('log1.log.gz', async function () { + let res = await exec(dirname('/corpus/log1.log.gz')) + assert.strictEqual(res.stdout, [ + '[2012-05-08T16:57:55.586Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T17:02:49.339Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', + ].join('')) +}); + +t.test('mixed text and gzip logs', async function () { + let res = await exec(dirname('/corpus/log1.log.gz') + ' ' + dirname('/corpus/log2.log')) + assert.strictEqual(res.stdout, [ + '[2012-05-08T16:57:55.586Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T16:58:55.586Z] INFO: agent2/73267 on headnode: message\n', + '[2012-05-08T17:01:49.339Z] INFO: agent2/73267 on headnode: message\n', + '[2012-05-08T17:02:47.404Z] INFO: agent2/73267 on headnode: message\n', + '[2012-05-08T17:02:49.339Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n', + '[2012-05-08T17:02:57.404Z] INFO: agent2/73267 on headnode: message\n', + '[2012-05-08T17:08:01.105Z] INFO: agent2/76156 on headnode: message\n', + ].join('')) +}); + +t.test('--level 40', async function () { + let res = await exec('-l 40 ' + dirname('/corpus/all.log')) + assert.strictEqual(res.stdout, [ + '# levels\n', + '[2012-02-08T22:56:53.856Z] WARN: myservice/123 on example.com: My message\n', + '[2012-02-08T22:56:54.856Z] ERROR: myservice/123 on example.com: My message\n', + '[2012-02-08T22:56:55.856Z] LVL55: myservice/123 on example.com: My message\n', + '[2012-02-08T22:56:56.856Z] FATAL: myservice/123 on example.com: My message\n', + '\n', + '# extra fields\n', + '\n', + '# bogus\n', + 'not a JSON line\n', + '{"hi": "there"}\n' + ].join('')) +}); + +t.test('--condition "this.level === 10 && this.pid === 123"', async function () { + let res = await exec('-c "this.level === 10 && this.pid === 123" ' + dirname('/corpus/all.log')) + assert.strictEqual(res.stdout, [ + '# levels\n', + '[2012-02-08T22:56:50.856Z] TRACE: myservice/123 on example.com: My message\n', + '\n', + '# extra fields\n', + '\n', + '# bogus\n', + 'not a JSON line\n', + '{"hi": "there"}\n' + ].join('')) +}); + +t.test('--condition "this.level === TRACE', async function () { + let res = await exec('-c "this.level === TRACE" ' + dirname('/corpus/all.log')) + assert.strictEqual(res.stdout, [ + '# levels\n', + '[2012-02-08T22:56:50.856Z] TRACE: myservice/123 on example.com: My message\n', + '\n', + '# extra fields\n', + '\n', + '# bogus\n', + 'not a JSON line\n', + '{"hi": "there"}\n' + ].join('')) +}); + +t.test('multiple --conditions', async function () { + let res = await exec(dirname('/corpus/all.log') + ' -c "this.level === 40" -c "this.pid === 123"') + assert.strictEqual(res.stdout, [ + '# levels\n', + '[2012-02-08T22:56:53.856Z] WARN: myservice/123 on example.com: My message\n', + '\n', + '# extra fields\n', + '\n', + '# bogus\n', + 'not a JSON line\n', + '{"hi": "there"}\n' + ].join('')) +}); + +// https://github.com/trentm/node-bunyan/issues/30 +// +// One of the records in corpus/withreq.log has a 'req' +// field with no 'headers'. Ditto for the 'res' field. +t.test('robust req handling', async function () { + let res = await exec(dirname('/corpus/withreq.log')) + assert.strictEqual(res.stdout, [ + '[2012-08-08T10:25:47.636Z] DEBUG: amon-master/12859 on 9724a190-27b6-4fd8-830b-a574f839c67d: headAgentProbes respond (req_id=cce79d15-ffc2-487c-a4e4-e940bdaac31e, route=HeadAgentProbes, contentMD5=11FxOYiYfpMxmANj4kGJzg==)', + '[2012-08-08T10:25:47.637Z] INFO: amon-master/12859 on 9724a190-27b6-4fd8-830b-a574f839c67d: HeadAgentProbes handled: 200 (req_id=cce79d15-ffc2-487c-a4e4-e940bdaac31e, audit=true, remoteAddress=10.2.207.2, remotePort=50394, latency=3, secure=false, _audit=true, req.version=*)', + ' HEAD /agentprobes?agent=ccf92af9-0b24-46b6-ab60-65095fdd3037 HTTP/1.1', + ' accept: application/json', + ' content-type: application/json', + ' host: 10.2.207.16', + ' connection: keep-alive', + ' --', + ' HTTP/1.1 200 OK', + ' content-md5: 11FxOYiYfpMxmANj4kGJzg==', + ' access-control-allow-origin: *', + ' access-control-allow-headers: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version', + ' access-control-allow-methods: HEAD', + ' access-control-expose-headers: X-Api-Version, X-Request-Id, X-Response-Time', + ' connection: Keep-Alive', + ' date: Wed, 08 Aug 2012 10:25:47 GMT', + ' server: Amon Master/1.0.0', + ' x-request-id: cce79d15-ffc2-487c-a4e4-e940bdaac31e', + ' x-response-time: 3', + ' --', + ' route: {', + ' "name": "HeadAgentProbes",', + ' "version": false', + ' }', + '[2012-08-08T10:25:47.637Z] INFO: amon-master/12859 on 9724a190-27b6-4fd8-830b-a574f839c67d: HeadAgentProbes handled: 200 (req_id=cce79d15-ffc2-487c-a4e4-e940bdaac31e, audit=true, remoteAddress=10.2.207.2, remotePort=50394, latency=3, secure=false, _audit=true, req.version=*)', + ' HEAD /agentprobes?agent=ccf92af9-0b24-46b6-ab60-65095fdd3037 HTTP/1.1', + ' --', + ' HTTP/1.1 200 OK', + ' --', + ' route: {', + ' "name": "HeadAgentProbes",', + ' "version": false', + ' }' + ].join('\n') + '\n') +}); + +// Some past crashes from issues. +t.test('should not crash on corpus/old-crashers/*.log', async function () { + let oldCrashers = fs.readdirSync( + path.resolve(dirname('/corpus/old-crashers'))) + .filter(function (f) { return f.slice(-4) === '.log'; }); + + await Promise.all(oldCrashers.map(function(logFile) { + return exec(dirname('/corpus/old-crashers/' + logFile)) + })) +}); + +t.test('should only show nonempty response bodies', async function () { + let res = await exec(dirname('/corpus/content-length-0-res.log')) + assert.strictEqual(res.stdout, [ + '[2016-02-10T07:28:41.419Z] INFO: myservice/123 on example.com: UnauthorizedError', + ' HTTP/1.1 401 Unauthorized', + ' content-type: text/plain', + ' date: Sat, 07 Mar 2015 06:58:43 GMT', + '[2016-02-10T07:28:41.419Z] INFO: myservice/123 on example.com: hello', + ' HTTP/1.1 200 OK', + ' content-type: text/plain', + ' content-length: 0', + ' date: Sat, 07 Mar 2015 06:58:43 GMT', + ' ', + ' hello', + '[2016-02-10T07:28:41.419Z] INFO: myservice/123 on example.com: UnauthorizedError', + ' HTTP/1.1 401 Unauthorized', + ' content-type: text/plain', + ' date: Sat, 07 Mar 2015 06:58:43 GMT' + ].join('\n') + '\n'); +}); diff --git a/test/ctor.test.js b/test/ctor.test.js deleted file mode 100644 index f47cb9d..0000000 --- a/test/ctor.test.js +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2012 Trent Mick. All rights reserved. - * - * Test type checking on creation of the Logger. - */ - -var bunyan = require('../lib/bunyan'), - Logger = bunyan; - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - - -test('ensure Logger creation options', function (t) { - t.throws(function () { new Logger(); }, - /options \(object\) is required/, - 'no options should throw'); - - t.throws(function () { new Logger({}); }, - /options\.name \(string\) is required/, - 'no options.name should throw'); - - t.doesNotThrow(function () { new Logger({name: 'foo'}); }, - 'just options.name should be sufficient'); - - var options = {name: 'foo', stream: process.stdout, streams: []}; - t.throws(function () { new Logger(options); }, - /* JSSTYLED */ - /cannot mix "streams" and "stream" options/, - 'cannot use "stream" and "streams"'); - - // https://github.com/trentm/node-bunyan/issues/3 - options = {name: 'foo', streams: {}}; - t.throws(function () { new Logger(options); }, - /invalid options.streams: must be an array/, - '"streams" must be an array'); - - options = {name: 'foo', serializers: 'a string'}; - t.throws(function () { new Logger(options); }, - /invalid options.serializers: must be an object/, - '"serializers" cannot be a string'); - - options = {name: 'foo', serializers: [1, 2, 3]}; - t.throws(function () { new Logger(options); }, - /invalid options.serializers: must be an object/, - '"serializers" cannot be an array'); - - t.end(); -}); - - -test('ensure Logger constructor is safe without new', function (t) { - t.doesNotThrow(function () { Logger({name: 'foo'}); }, - 'constructor should call self with new if necessary'); - - t.end(); -}); - - -test('ensure Logger creation options (createLogger)', function (t) { - t.throws(function () { bunyan.createLogger(); }, - /options \(object\) is required/, - 'no options should throw'); - - t.throws(function () { bunyan.createLogger({}); }, - /options\.name \(string\) is required/, - 'no options.name should throw'); - - t.doesNotThrow(function () { bunyan.createLogger({name: 'foo'}); }, - 'just options.name should be sufficient'); - - var options = {name: 'foo', stream: process.stdout, streams: []}; - t.throws(function () { bunyan.createLogger(options); }, - /* JSSTYLED */ - /cannot mix "streams" and "stream" options/, - 'cannot use "stream" and "streams"'); - - // https://github.com/trentm/node-bunyan/issues/3 - options = {name: 'foo', streams: {}}; - t.throws(function () { bunyan.createLogger(options); }, - /invalid options.streams: must be an array/, - '"streams" must be an array'); - - options = {name: 'foo', serializers: 'a string'}; - t.throws(function () { bunyan.createLogger(options); }, - /invalid options.serializers: must be an object/, - '"serializers" cannot be a string'); - - options = {name: 'foo', serializers: [1, 2, 3]}; - t.throws(function () { bunyan.createLogger(options); }, - /invalid options.serializers: must be an object/, - '"serializers" cannot be an array'); - - t.end(); -}); - - -test('ensure Logger child() options', function (t) { - var log = new Logger({name: 'foo'}); - - t.doesNotThrow(function () { log.child(); }, - 'no options should be fine'); - - t.doesNotThrow(function () { log.child({}); }, - 'empty options should be fine too'); - - t.throws(function () { log.child({name: 'foo'}); }, - /invalid options.name: child cannot set logger name/, - 'child cannot change name'); - - var options = {stream: process.stdout, streams: []}; - t.throws(function () { log.child(options); }, - /* JSSTYLED */ - /cannot mix "streams" and "stream" options/, - 'cannot use "stream" and "streams"'); - - // https://github.com/trentm/node-bunyan/issues/3 - options = {streams: {}}; - t.throws(function () { log.child(options); }, - /invalid options.streams: must be an array/, - '"streams" must be an array'); - - options = {serializers: 'a string'}; - t.throws(function () { log.child(options); }, - /invalid options.serializers: must be an object/, - '"serializers" cannot be a string'); - - options = {serializers: [1, 2, 3]}; - t.throws(function () { log.child(options); }, - /invalid options.serializers: must be an object/, - '"serializers" cannot be an array'); - - t.end(); -}); - - -test('ensure Logger() rejects non-Logger parents', function (t) { - var dad = new Logger({name: 'dad', streams: []}); - - t.throws(function () { new Logger({}, {}); }, - /invalid Logger creation: do not pass a second arg/, - 'Logger arguments must be valid'); - - t.doesNotThrow(function () { new Logger(dad, {}); }, - 'Logger allows Logger instance as parent'); - - t.end(); -}); diff --git a/test/ctor.test.mjs b/test/ctor.test.mjs new file mode 100644 index 0000000..40eee9d --- /dev/null +++ b/test/ctor.test.mjs @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2012 Trent Mick. All rights reserved. + * + * Test type checking on creation of the Logger. + */ +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' + +t.test('ensure Logger creation options', function () { + assert.throws(function () { new bunyan(); }, + /options \(object\) is required/, + 'no options should throw'); + + assert.throws(function () { new bunyan({}); }, + /options\.name \(string\) is required/, + 'no options.name should throw'); + + new bunyan({name: 'foo'}); + + let options = {name: 'foo', stream: process.stdout, streams: []}; + assert.throws(function () { new bunyan(options); }, + /cannot mix "streams" and "stream" options/, // JSSTYLED + 'cannot use "stream" and "streams"'); + + // https://github.com/trentm/node-bunyan/issues/3 + options = {name: 'foo', streams: {}}; + assert.throws(function () { new bunyan(options); }, + /invalid options.streams: must be an array/, + '"streams" must be an array'); + + options = {name: 'foo', serializers: 'a string'}; + assert.throws(function () { new bunyan(options); }, + /invalid options.serializers: must be an object/, + '"serializers" cannot be a string'); + + options = {name: 'foo', serializers: [1, 2, 3]}; + assert.throws(function () { new bunyan(options); }, + /invalid options.serializers: must be an object/, + '"serializers" cannot be an array'); +}); + + +t.test('ensure Logger constructor is safe without new', function () { + bunyan({name: 'foo'}) +}); + + +t.test('ensure Logger creation options (createLogger)', function () { + assert.throws(function () { bunyan.createLogger(); }, + /options \(object\) is required/, + 'no options should throw'); + + assert.throws(function () { bunyan.createLogger({}); }, + /options\.name \(string\) is required/, + 'no options.name should throw'); + + bunyan.createLogger({name: 'foo'}); + + let options = {name: 'foo', stream: process.stdout, streams: []}; + assert.throws(function () { bunyan.createLogger(options); }, + /cannot mix "streams" and "stream" options/, // JSSTYLED + 'cannot use "stream" and "streams"'); + + // https://github.com/trentm/node-bunyan/issues/3 + options = {name: 'foo', streams: {}}; + assert.throws(function () { bunyan.createLogger(options); }, + /invalid options.streams: must be an array/, + '"streams" must be an array'); + + options = {name: 'foo', serializers: 'a string'}; + assert.throws(function () { bunyan.createLogger(options); }, + /invalid options.serializers: must be an object/, + '"serializers" cannot be a string'); + + options = {name: 'foo', serializers: [1, 2, 3]}; + assert.throws(function () { bunyan.createLogger(options); }, + /invalid options.serializers: must be an object/, + '"serializers" cannot be an array'); +}); + + +t.test('ensure Logger child() options', function () { + let log = new bunyan({name: 'foo'}); + + log.child(); + + log.child({}); + + assert.throws(function () { log.child({name: 'foo'}); }, + /invalid options.name: child cannot set logger name/, + 'child cannot change name'); + + let options = {stream: process.stdout, streams: []}; + assert.throws(function () { log.child(options); }, + /cannot mix "streams" and "stream" options/, // JSSTYLED + 'cannot use "stream" and "streams"'); + + // https://github.com/trentm/node-bunyan/issues/3 + options = {streams: {}}; + assert.throws(function () { log.child(options); }, + /invalid options.streams: must be an array/, + '"streams" must be an array'); + + options = {serializers: 'a string'}; + assert.throws(function () { log.child(options); }, + /invalid options.serializers: must be an object/, + '"serializers" cannot be a string'); + + options = {serializers: [1, 2, 3]}; + assert.throws(function () { log.child(options); }, + /invalid options.serializers: must be an object/, + '"serializers" cannot be an array'); +}); + + +t.test('ensure Logger() rejects non-Logger parents', function () { + let dad = new bunyan({name: 'dad', streams: []}); + + assert.throws(function () { new bunyan({}, {}); }, + /invalid Logger creation: do not pass a second arg/, + 'Logger arguments must be valid'); + + new bunyan(dad, {}); +}); diff --git a/test/cycles.test.js b/test/cycles.test.js deleted file mode 100644 index 3f1cda6..0000000 --- a/test/cycles.test.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2012 Trent Mick. All rights reserved. - * - * Make sure cycles are safe. - */ - -var Logger = require('../lib/bunyan.js'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -var Stream = require('stream').Stream; -var outstr = new Stream; -outstr.writable = true; -var output = []; -outstr.write = function (c) { - output.push(JSON.parse(c + '')); -}; -outstr.end = function (c) { - if (c) this.write(c); - this.emit('end'); -}; - -// these are lacking a few fields that will probably never match -var expect = - [ - { - 'name': 'blammo', - 'level': 30, - 'msg': 'bango { bang: \'boom\', KABOOM: [Circular] }', - 'v': 0 - }, - { - 'name': 'blammo', - 'level': 30, - 'msg': 'kaboom { bang: \'boom\', KABOOM: [Circular] }', - 'v': 0 - }, - { - 'name': 'blammo', - 'level': 30, - 'bang': 'boom', - 'KABOOM': { - 'bang': 'boom', - 'KABOOM': '[Circular]' - }, - 'msg': '', - 'v': 0 - } - ]; - -var log = new Logger({ - name: 'blammo', - streams: [ - { - type: 'stream', - level: 'info', - stream: outstr - } - ] -}); - -test('cycles', function (t) { - outstr.on('end', function () { - output.forEach(function (o, i) { - // Drop variable parts for comparison. - delete o.hostname; - delete o.pid; - delete o.time; - // Hack object/dict comparison: JSONify. - t.equal(JSON.stringify(o), JSON.stringify(expect[i]), - 'log item ' + i + ' matches'); - }); - t.end(); - }); - - var obj = { bang: 'boom' }; - obj.KABOOM = obj; - log.info('bango', obj); - log.info('kaboom', obj.KABOOM); - log.info(obj); - outstr.end(); - t.ok('did not throw'); -}); diff --git a/test/cycles.test.mjs b/test/cycles.test.mjs new file mode 100644 index 0000000..7513b5b --- /dev/null +++ b/test/cycles.test.mjs @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2012 Trent Mick. All rights reserved. + * + * Make sure cycles are safe. + */ + +import { Stream } from 'stream' +import { Eltro as t, assert} from 'eltro' +import Logger from '../lib/bunyan.mjs' + +var outstr = new Stream; +outstr.writable = true; +var output = []; +outstr.write = function (c) { + output.push(JSON.parse(c + '')); +}; +outstr.end = function (c) { + if (c) this.write(c); + this.emit('end'); +}; + +// these are lacking a few fields that will probably never match +var expect = [ + { + 'name': 'blammo', + 'level': 30, + 'msg': 'bango { bang: \'boom\', KABOOM: [Circular *1] }', + 'v': 0 + }, + { + 'name': 'blammo', + 'level': 30, + 'msg': 'kaboom { bang: \'boom\', KABOOM: [Circular *1] }', + 'v': 0 + }, + { + 'name': 'blammo', + 'level': 30, + 'bang': 'boom', + 'KABOOM': { + 'bang': 'boom', + 'KABOOM': '[Circular]' + }, + 'msg': '', + 'v': 0 + } +]; + +var log = new Logger({ + name: 'blammo', + streams: [ + { + type: 'stream', + level: 'info', + stream: outstr + } + ] +}); + +t.test('cycles', function (cb) { + outstr.on('end', function () { + output.forEach(function (o, i) { + // Drop variable parts for comparison. + delete o.hostname; + delete o.pid; + delete o.time; + // Hack object/dict comparison: JSONify. + try { + assert.strictEqual(JSON.stringify(o), JSON.stringify(expect[i])) + } catch (err) { + cb(err) + } + }); + cb() + }); + + var obj = { bang: 'boom' }; + obj.KABOOM = obj; + log.info('bango', obj); + log.info('kaboom', obj.KABOOM); + log.info(obj); + outstr.end(); +}); diff --git a/test/error-event.test.js b/test/error-event.test.js deleted file mode 100644 index 2318d42..0000000 --- a/test/error-event.test.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2016 Trent Mick - * - * Test emission and handling of 'error' event in a logger with a 'path' - * stream. - */ - -var EventEmitter = require('events').EventEmitter; -var util = require('util'); - -var bunyan = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -var BOGUS_PATH = '/this/path/is/bogus.log'; - -test('error event on file stream (reemitErrorEvents=undefined)', function (t) { - var log = bunyan.createLogger( - {name: 'error-event-1', streams: [ {path: BOGUS_PATH} ]}); - log.on('error', function (err, stream) { - t.ok(err, 'got err in error event: ' + err); - t.equal(err.code, 'ENOENT', 'error code is ENOENT'); - t.ok(stream, 'got a stream argument'); - t.equal(stream.path, BOGUS_PATH); - t.equal(stream.type, 'file'); - t.end(); - }); - log.info('info log message'); -}); - -test('error event on file stream (reemitErrorEvents=true)', function (t) { - var log = bunyan.createLogger({ - name: 'error-event-2', - streams: [ { - path: BOGUS_PATH, - reemitErrorEvents: true - } ] - }); - log.on('error', function (err, stream) { - t.ok(err, 'got err in error event: ' + err); - t.equal(err.code, 'ENOENT', 'error code is ENOENT'); - t.ok(stream, 'got a stream argument'); - t.equal(stream.path, BOGUS_PATH); - t.equal(stream.type, 'file'); - t.end(); - }); - log.info('info log message'); -}); - -test('error event on file stream (reemitErrorEvents=false)', - function (t) { - var log = bunyan.createLogger({ - name: 'error-event-3', - streams: [ { - path: BOGUS_PATH, - reemitErrorEvents: false - } ] - }); - // Hack into the underlying created file stream to catch the error event. - log.streams[0].stream.on('error', function (err) { - t.ok(err, 'got error event on the file stream'); - t.end(); - }); - log.on('error', function (err, stream) { - t.fail('should not have gotten error event on logger'); - t.end(); - }); - log.info('info log message'); -}); - - -function MyErroringStream() {} -util.inherits(MyErroringStream, EventEmitter); -MyErroringStream.prototype.write = function (rec) { - this.emit('error', new Error('boom')); -} - -test('error event on raw stream (reemitErrorEvents=undefined)', function (t) { - var estream = new MyErroringStream(); - var log = bunyan.createLogger({ - name: 'error-event-raw', - streams: [ - { - stream: estream, - type: 'raw' - } - ] - }); - estream.on('error', function (err) { - t.ok(err, 'got error event on the raw stream'); - t.end(); - }); - log.on('error', function (err, stream) { - t.fail('should not have gotten error event on logger'); - t.end(); - }); - log.info('info log message'); -}); - -test('error event on raw stream (reemitErrorEvents=false)', function (t) { - var estream = new MyErroringStream(); - var log = bunyan.createLogger({ - name: 'error-event-raw', - streams: [ - { - stream: estream, - type: 'raw', - reemitErrorEvents: false - } - ] - }); - estream.on('error', function (err) { - t.ok(err, 'got error event on the raw stream'); - t.end(); - }); - log.on('error', function (err, stream) { - t.fail('should not have gotten error event on logger'); - t.end(); - }); - log.info('info log message'); -}); - -test('error event on raw stream (reemitErrorEvents=true)', function (t) { - var estream = new MyErroringStream(); - var log = bunyan.createLogger({ - name: 'error-event-raw', - streams: [ - { - stream: estream, - type: 'raw', - reemitErrorEvents: true - } - ] - }); - log.on('error', function (err, stream) { - t.ok(err, 'got err in error event: ' + err); - t.equal(err.message, 'boom'); - t.ok(stream, 'got a stream argument'); - t.ok(stream.stream instanceof MyErroringStream); - t.equal(stream.type, 'raw'); - t.end(); - }); - log.info('info log message'); -}); diff --git a/test/error-event.test.mjs b/test/error-event.test.mjs new file mode 100644 index 0000000..6ae130d --- /dev/null +++ b/test/error-event.test.mjs @@ -0,0 +1,164 @@ +/* + * Copyright 2016 Trent Mick + * + * Test emission and handling of 'error' event in a logger with a 'path' + * stream. + */ + +import { EventEmitter } from 'events' +import util from 'util' +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' + + +var BOGUS_PATH = '/this/path/is/bogus.log'; + +t.test('error event on file stream (reemitErrorEvents=undefined)', function (cb) { + var log = bunyan.createLogger( + {name: 'error-event-1', streams: [ {path: BOGUS_PATH} ]}); + log.on('error', function (err, stream) { + try { + assert.ok(err, 'got err in error event: ' + err); + assert.strictEqual(err.code, 'ENOENT', 'error code is ENOENT'); + assert.ok(stream, 'got a stream argument'); + assert.strictEqual(stream.path, BOGUS_PATH); + assert.strictEqual(stream.type, 'file'); + cb() + } catch (err) { + cb(err) + } + }); + log.info('info log message'); +}); + +t.test('error event on file stream (reemitErrorEvents=true)', function (cb) { + var log = bunyan.createLogger({ + name: 'error-event-2', + streams: [ { + path: BOGUS_PATH, + reemitErrorEvents: true + } ] + }); + log.on('error', function (err, stream) { + try { + assert.ok(err, 'got err in error event: ' + err); + assert.strictEqual(err.code, 'ENOENT', 'error code is ENOENT'); + assert.ok(stream, 'got a stream argument'); + assert.strictEqual(stream.path, BOGUS_PATH); + assert.strictEqual(stream.type, 'file'); + cb() + } catch(err) { + cb(err) + } + }); + log.info('info log message'); +}); + +t.test('error event on file stream (reemitErrorEvents=false)', + function (cb) { + var log = bunyan.createLogger({ + name: 'error-event-3', + streams: [ { + path: BOGUS_PATH, + reemitErrorEvents: false + } ] + }); + // Hack into the underlying created file stream to catch the error event. + log.streams[0].stream.on('error', function (err) { + try { + assert.ok(err, 'got error event on the file stream'); + cb() + } catch (err) { + cb(err) + } + }); + log.on('error', function (err, stream) { + cb('should not have gotten error event on logger') + }); + log.info('info log message'); +}); + + +function MyErroringStream() {} +util.inherits(MyErroringStream, EventEmitter); +MyErroringStream.prototype.write = function (rec) { + this.emit('error', new Error('boom')); +} + +t.test('error event on raw stream (reemitErrorEvents=undefined)', function (cb) { + var estream = new MyErroringStream(); + var log = bunyan.createLogger({ + name: 'error-event-raw', + streams: [ + { + stream: estream, + type: 'raw' + } + ] + }); + estream.on('error', function (err) { + try { + assert.ok(err, 'got error event on the raw stream'); + cb() + } catch (err) { + cb(err) + } + }); + log.on('error', function (err, stream) { + cb('should not have gotten error event on logger'); + }); + log.info('info log message'); +}); + +t.test('error event on raw stream (reemitErrorEvents=false)', function (cb) { + var estream = new MyErroringStream(); + var log = bunyan.createLogger({ + name: 'error-event-raw', + streams: [ + { + stream: estream, + type: 'raw', + reemitErrorEvents: false + } + ] + }); + estream.on('error', function (err) { + try { + assert.ok(err, 'got error event on the raw stream'); + cb() + } catch (err) { + cb(err) + } + }); + log.on('error', function (err, stream) { + cb('should not have gotten error event on logger'); + }); + log.info('info log message'); +}); + +t.test('error event on raw stream (reemitErrorEvents=true)', function (cb) { + var estream = new MyErroringStream(); + var log = bunyan.createLogger({ + name: 'error-event-raw', + streams: [ + { + stream: estream, + type: 'raw', + reemitErrorEvents: true + } + ] + }); + log.on('error', function (err, stream) { + try { + assert.ok(err, 'got err in error event: ' + err); + assert.strictEqual(err.message, 'boom'); + assert.ok(stream, 'got a stream argument'); + assert.ok(stream.stream instanceof MyErroringStream); + assert.strictEqual(stream.type, 'raw'); + cb() + } catch (err) { + cb(err) + } + }); + log.info('info log message'); +}); diff --git a/test/helper.mjs b/test/helper.mjs new file mode 100644 index 0000000..d8c7581 --- /dev/null +++ b/test/helper.mjs @@ -0,0 +1,29 @@ +import { exec as ex } from 'child_process' +import path from 'path' +import { fileURLToPath } from 'url' + +let __dirname = path.dirname(fileURLToPath(import.meta.url)) +var BUNYAN = path.resolve(__dirname, '../bin/bunyan.mjs'); + +export function exec(parameter, prefix = 'node', bunnyboy = BUNYAN) { + let command = `${prefix} ${bunnyboy} ${parameter}` + return new Promise(function(res, rej) { + ex(command, + function (err, stdout, stderr) { + if (err) { + err.stdout = stdout + err.stderr = stderr + return rej(err) + } + res({ + stdout, + stderr, + }) + } + ) + }) +} + +export function dirname(file) { + return path.resolve(__dirname + file) +} diff --git a/test/level.test.js b/test/level.test.js deleted file mode 100644 index ab619f2..0000000 --- a/test/level.test.js +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2014 Trent Mick. All rights reserved. - * - * Test the `log.level(...)`. - */ - -var util = require('util'), - format = util.format, - inspect = util.inspect; -var p = console.log; - -var bunyan = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -// ---- test boolean `log.()` calls - -var log1 = bunyan.createLogger({ - name: 'log1', - streams: [ - { - path: __dirname + '/level.test.log1.log', - level: 'info' - } - ] -}); - - -test('log.level() -> level num', function (t) { - t.equal(log1.level(), bunyan.INFO); - t.end(); -}); - -test('log.level()', function (t) { - log1.level(bunyan.DEBUG); - t.equal(log1.level(), bunyan.DEBUG); - t.end(); -}); - -test('log.level()', function (t) { - log1.level(10); - t.equal(log1.level(), bunyan.TRACE); - t.end(); -}); - -test('log.level()', function (t) { - log1.level('error'); - t.equal(log1.level(), bunyan.ERROR); - t.end(); -}); - -// A trick to turn logging off. -// See . -test('log.level(FATAL + 1)', function (t) { - log1.level(bunyan.FATAL + 1); - t.equal(log1.level(), bunyan.FATAL + 1); - t.end(); -}); - -test('log.level()', function (t) { - log1.level(0); - t.equal(log1.level(), 0); - log1.level(Number.MAX_VALUE); - t.equal(log1.level(), Number.MAX_VALUE); - log1.level(Infinity); - t.equal(log1.level(), Infinity); - t.end(); -}); - -test('log.level()', function (t) { - t.throws(function () { - var log = bunyan.createLogger({name: 'invalid', level: 'booga'}); - // JSSTYLED - }, /unknown level name: "booga"/); - t.throws(function () { - var log = bunyan.createLogger({name: 'invalid', level: []}); - }, /cannot resolve level: invalid arg \(object\): \[\]/); - t.throws(function () { - var log = bunyan.createLogger({name: 'invalid', level: true}); - }, /cannot resolve level: invalid arg \(boolean\): true/); - t.throws(function () { - var log = bunyan.createLogger({name: 'invalid', level: -1}); - }, /level is not a positive integer: -1/); - t.throws(function () { - var log = bunyan.createLogger({name: 'invalid', level: 3.14}); - }, /level is not a positive integer: 3.14/); - t.throws(function () { - var log = bunyan.createLogger({name: 'invalid', level: -Infinity}); - }, /level is not a positive integer: -Infinity/); - t.end(); -}); diff --git a/test/level.test.mjs b/test/level.test.mjs new file mode 100644 index 0000000..ab104a3 --- /dev/null +++ b/test/level.test.mjs @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2014 Trent Mick. All rights reserved. + * + * Test the `log.level(...)`. + */ + +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' +import { dirname } from './helper.mjs' + + +// ---- test boolean `log.()` calls + +var log1 = bunyan.createLogger({ + name: 'log1', + streams: [ + { + level: 'info', + stream: process.stdout, + } + ] +}); + + +t.test('log.level() -> level num', function () { + assert.strictEqual(log1.level(), bunyan.INFO); +}); + +t.test('log.level()', function () { + log1.level(bunyan.DEBUG); + assert.strictEqual(log1.level(), bunyan.DEBUG); +}); + +t.test('log.level()', function () { + log1.level(10); + assert.strictEqual(log1.level(), bunyan.TRACE); +}); + +t.test('log.level()', function () { + log1.level('error'); + assert.strictEqual(log1.level(), bunyan.ERROR); +}); + +// A trick to turn logging off. +// See . +t.test('log.level(FATAL + 1)', function () { + log1.level(bunyan.FATAL + 1); + assert.strictEqual(log1.level(), bunyan.FATAL + 1); +}); + +t.test('log.level()', function () { + log1.level(0); + assert.strictEqual(log1.level(), 0); + log1.level(Number.MAX_VALUE); + assert.strictEqual(log1.level(), Number.MAX_VALUE); + log1.level(Infinity); + assert.strictEqual(log1.level(), Infinity); +}); + +t.test('log.level()', function () { + assert.throws(function () { + bunyan.createLogger({name: 'invalid', level: 'booga'}); + }, /unknown level name: "booga"/); + assert.throws(function () { + bunyan.createLogger({name: 'invalid', level: []}); + }, /cannot resolve level: invalid arg \(object\): \[\]/); + assert.throws(function () { + bunyan.createLogger({name: 'invalid', level: true}); + }, /cannot resolve level: invalid arg \(boolean\): true/); + assert.throws(function () { + bunyan.createLogger({name: 'invalid', level: -1}); + }, /level is not a positive integer: -1/); + assert.throws(function () { + bunyan.createLogger({name: 'invalid', level: 3.14}); + }, /level is not a positive integer: 3.14/); + assert.throws(function () { + bunyan.createLogger({name: 'invalid', level: -Infinity}); + }, /level is not a positive integer: -Infinity/); +}); diff --git a/test/log.test.js b/test/log.test.js deleted file mode 100644 index 6441463..0000000 --- a/test/log.test.js +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright (c) 2012 Trent Mick. All rights reserved. - * - * Test the `log.trace(...)`, `log.debug(...)`, ..., `log.fatal(...)` API. - */ - -var util = require('util'), - format = util.format, - inspect = util.inspect; -var p = console.log; - -var bunyan = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -// ---- test boolean `log.()` calls - -var log1 = bunyan.createLogger({ - name: 'log1', - streams: [ - { - path: __dirname + '/log.test.log1.log', - level: 'info' - } - ] -}); - -var log2 = bunyan.createLogger({ - name: 'log2', - streams: [ - { - path: __dirname + '/log.test.log2a.log', - level: 'error' - }, - { - path: __dirname + '/log.test.log2b.log', - level: 'debug' - } - ] -}) - -test('log.LEVEL() -> boolean', function (t) { - t.equal(log1.trace(), false, 'log1.trace() is false') - t.equal(log1.debug(), false) - t.equal(log1.info(), true) - t.equal(log1.warn(), true) - t.equal(log1.error(), true) - t.equal(log1.fatal(), true) - - // Level is the *lowest* level of all streams. - t.equal(log2.trace(), false) - t.equal(log2.debug(), true) - t.equal(log2.info(), true) - t.equal(log2.warn(), true) - t.equal(log2.error(), true) - t.equal(log2.fatal(), true) - t.end(); -}); - - -// ---- test `log.(...)` calls which various input types - -function Catcher() { - this.records = []; -} -Catcher.prototype.write = function (record) { - this.records.push(record); -} -var catcher = new Catcher(); -var log3 = new bunyan.createLogger({ - name: 'log3', - streams: [ - { - type: 'raw', - stream: catcher, - level: 'trace' - } - ] -}); - -var names = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; -var fields = {one: 'un'}; - -test('log.info(undefined, )', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, undefined, 'some message'); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, 'undefined \'some message\'', - format('log.%s msg is "some message"', lvl)); - }); - t.end(); -}); - -test('log.info(, undefined)', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, fields, undefined); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, 'undefined', - format('log.%s msg: expect "undefined", got %j', lvl, rec.msg)); - t.equal(rec.one, 'un'); - }); - t.end(); -}); - -test('log.info(null, )', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, null, 'some message'); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, 'some message', - format('log.%s msg is "some message"', lvl)); - }); - t.end(); -}); - -test('log.info(, null)', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, fields, null); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, 'null', - format('log.%s msg: expect "null", got %j', lvl, rec.msg)); - t.equal(rec.one, 'un'); - }); - t.end(); -}); - -test('log.info()', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, 'some message'); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, 'some message', - format('log.%s msg is "some message"', lvl)); - }); - t.end(); -}); - -test('log.info(, )', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, fields, 'some message'); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, 'some message', - format('log.%s msg: got %j', lvl, rec.msg)); - t.equal(rec.one, 'un'); - }); - t.end(); -}); - -test('log.info()', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, true); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, 'true', - format('log.%s msg is "true"', lvl)); - }); - t.end(); -}); - -test('log.info(, )', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, fields, true); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, 'true', - format('log.%s msg: got %j', lvl, rec.msg)); - t.equal(rec.one, 'un'); - }); - t.end(); -}); - -test('log.info()', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, 3.14); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, '3.14', - format('log.%s msg: got %j', lvl, rec.msg)); - }); - t.end(); -}); - -test('log.info(, )', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, fields, 3.14); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, '3.14', - format('log.%s msg: got %j', lvl, rec.msg)); - t.equal(rec.one, 'un'); - }); - t.end(); -}); - -test('log.info()', function (t) { - var func = function func1() {}; - names.forEach(function (lvl) { - log3[lvl].call(log3, func); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, '[Function: func1]', - format('log.%s msg: got %j', lvl, rec.msg)); - }); - t.end(); -}); - -test('log.info(, )', function (t) { - var func = function func2() {}; - names.forEach(function (lvl) { - log3[lvl].call(log3, fields, func); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, '[Function: func2]', - format('log.%s msg: got %j', lvl, rec.msg)); - t.equal(rec.one, 'un'); - }); - t.end(); -}); - -test('log.info()', function (t) { - var arr = ['a', 1, {two: 'deux'}]; - names.forEach(function (lvl) { - log3[lvl].call(log3, arr); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, format(arr), - format('log.%s msg: got %j', lvl, rec.msg)); - }); - t.end(); -}); - -test('log.info(, )', function (t) { - var arr = ['a', 1, {two: 'deux'}]; - names.forEach(function (lvl) { - log3[lvl].call(log3, fields, arr); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, format(arr), - format('log.%s msg: got %j', lvl, rec.msg)); - t.equal(rec.one, 'un'); - }); - t.end(); -}); - - -/* - * By accident (starting with trentm/node-bunyan#85 in bunyan@0.23.0), - * log.info(null, ...) - * was interpreted as `null` being the object of fields. It is gracefully - * handled, which is good. However, had I to do it again, I would have made - * that interpret `null` as the *message*, and no fields having been passed. - * I think it is baked now. It would take a major bunyan rev to change it, - * but I don't think it is worth it: passing `null` as the first arg isn't - * really an intended way to call these Bunyan methods for either case. - */ - -test('log.info(null)', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, null); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, '', format('log.%s msg: got %j', lvl, rec.msg)); - }); - t.end(); -}); - -test('log.info(null, )', function (t) { - names.forEach(function (lvl) { - log3[lvl].call(log3, null, 'my message'); - var rec = catcher.records[catcher.records.length - 1]; - t.equal(rec.msg, 'my message', - format('log.%s msg: got %j', lvl, rec.msg)); - }); - t.end(); -}); diff --git a/test/log.test.mjs b/test/log.test.mjs new file mode 100644 index 0000000..e441191 --- /dev/null +++ b/test/log.test.mjs @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2012 Trent Mick. All rights reserved. + * + * Test the `log.trace(...)`, `log.debug(...)`, ..., `log.fatal(...)` API. + */ + +import { Eltro as t, assert} from 'eltro' +import { format } from 'util' +import bunyan from '../lib/bunyan.mjs' + +// ---- test boolean `log.()` calls + +var log1 = bunyan.createLogger({ + name: 'log1', + streams: [ + { + stream: process.stdout, + level: 'info' + } + ] +}); + +var log2 = bunyan.createLogger({ + name: 'log2', + streams: [ + { + stream: process.stdout, + level: 'error' + }, + { + stream: process.stdout, + level: 'debug' + } + ] +}) + +t.test('log.LEVEL() -> boolean', function () { + assert.strictEqual(log1.trace(), false, 'log1.trace() is false') + assert.strictEqual(log1.debug(), false) + assert.strictEqual(log1.info(), true) + assert.strictEqual(log1.warn(), true) + assert.strictEqual(log1.error(), true) + assert.strictEqual(log1.fatal(), true) + + // Level is the *lowest* level of all streams. + assert.strictEqual(log2.trace(), false) + assert.strictEqual(log2.debug(), true) + assert.strictEqual(log2.info(), true) + assert.strictEqual(log2.warn(), true) + assert.strictEqual(log2.error(), true) + assert.strictEqual(log2.fatal(), true) +}); + + +// ---- test `log.(...)` calls which various input types + +function Catcher() { + this.records = []; +} +Catcher.prototype.write = function (record) { + this.records.push(record); +} +var catcher = new Catcher(); +var log3 = new bunyan.createLogger({ + name: 'log3', + streams: [ + { + type: 'raw', + stream: catcher, + level: 'trace' + } + ] +}); + +var names = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; +var fields = {one: 'un'}; + +t.test('log.info(undefined, )', function () { + names.forEach(function (lvl) { + log3[lvl](undefined, 'some message'); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, 'undefined some message'); + }); +}); + +t.test('log.info(, undefined)', function () { + names.forEach(function (lvl) { + log3[lvl](fields, undefined); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, 'undefined'); + assert.strictEqual(rec.one, 'un'); + }); +}); + +t.test('log.info(null, )', function () { + names.forEach(function (lvl) { + log3[lvl](null, 'some message'); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, 'some message'); + }); +}); + +t.test('log.info(, null)', function () { + names.forEach(function (lvl) { + log3[lvl](fields, null); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, 'null'); + assert.strictEqual(rec.one, 'un'); + }); +}); + +t.test('log.info()', function () { + names.forEach(function (lvl) { + log3[lvl]('some message'); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, 'some message'); + }); +}); + +t.test('log.info(, )', function () { + names.forEach(function (lvl) { + log3[lvl](fields, 'some message'); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, 'some message'); + assert.strictEqual(rec.one, 'un'); + }); +}); + +t.test('log.info()', function () { + names.forEach(function (lvl) { + log3[lvl](true); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, 'true'); + }); +}); + +t.test('log.info(, )', function () { + names.forEach(function (lvl) { + log3[lvl](fields, true); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, 'true'); + assert.strictEqual(rec.one, 'un'); + }); +}); + +t.test('log.info()', function () { + names.forEach(function (lvl) { + log3[lvl](3.14); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, '3.14'); + }); +}); + +t.test('log.info(, )', function () { + names.forEach(function (lvl) { + log3[lvl](fields, 3.14); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, '3.14'); + assert.strictEqual(rec.one, 'un'); + }); +}); + +t.test('log.info()', function () { + var func = function func1() {}; + names.forEach(function (lvl) { + log3[lvl](func); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, '[Function: func1]'); + }); +}); + +t.test('log.info(, )', function () { + var func = function func2() {}; + names.forEach(function (lvl) { + log3[lvl](fields, func); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, '[Function: func2]'); + assert.strictEqual(rec.one, 'un'); + }); +}); + +t.test('log.info()', function () { + var arr = ['a', 1, {two: 'deux'}]; + names.forEach(function (lvl) { + log3[lvl](arr); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, format(arr)); + }); +}); + +t.test('log.info(, )', function () { + var arr = ['a', 1, {two: 'deux'}]; + names.forEach(function (lvl) { + log3[lvl](fields, arr); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, format(arr)); + assert.strictEqual(rec.one, 'un'); + }); +}); + + +/* + * By accident (starting with trentm/node-bunyan#85 in bunyan@0.23.0), + * log.info(null, ...) + * was interpreted as `null` being the object of fields. It is gracefully + * handled, which is good. However, had I to do it again, I would have made + * that interpret `null` as the *message*, and no fields having been passed. + * I think it is baked now. It would take a major bunyan rev to change it, + * but I don't think it is worth it: passing `null` as the first arg isn't + * really an intended way to call these Bunyan methods for either case. + */ + +t.test('log.info(null)', function () { + names.forEach(function (lvl) { + log3[lvl](null); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, '', format('log.%s msg: got %j', lvl, rec.msg)); + }); +}); + +t.test('log.info(null, )', function () { + names.forEach(function (lvl) { + log3[lvl](null, 'my message'); + var rec = catcher.records[catcher.records.length - 1]; + assert.strictEqual(rec.msg, 'my message'); + }); +}); diff --git a/test/other-api.test.js b/test/other-api.test.js deleted file mode 100644 index 0d9304b..0000000 --- a/test/other-api.test.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2012 Trent Mick. All rights reserved. - * - * Test other parts of the exported API. - */ - -var bunyan = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -test('bunyan.s', function (t) { - t.ok(bunyan.TRACE, 'TRACE'); - t.ok(bunyan.DEBUG, 'DEBUG'); - t.ok(bunyan.INFO, 'INFO'); - t.ok(bunyan.WARN, 'WARN'); - t.ok(bunyan.ERROR, 'ERROR'); - t.ok(bunyan.FATAL, 'FATAL'); - t.end(); -}); - -test('bunyan.resolveLevel()', function (t) { - t.equal(bunyan.resolveLevel('trace'), bunyan.TRACE, 'TRACE'); - t.equal(bunyan.resolveLevel('TRACE'), bunyan.TRACE, 'TRACE'); - t.equal(bunyan.resolveLevel('debug'), bunyan.DEBUG, 'DEBUG'); - t.equal(bunyan.resolveLevel('DEBUG'), bunyan.DEBUG, 'DEBUG'); - t.equal(bunyan.resolveLevel('info'), bunyan.INFO, 'INFO'); - t.equal(bunyan.resolveLevel('INFO'), bunyan.INFO, 'INFO'); - t.equal(bunyan.resolveLevel('warn'), bunyan.WARN, 'WARN'); - t.equal(bunyan.resolveLevel('WARN'), bunyan.WARN, 'WARN'); - t.equal(bunyan.resolveLevel('error'), bunyan.ERROR, 'ERROR'); - t.equal(bunyan.resolveLevel('ERROR'), bunyan.ERROR, 'ERROR'); - t.equal(bunyan.resolveLevel('fatal'), bunyan.FATAL, 'FATAL'); - t.equal(bunyan.resolveLevel('FATAL'), bunyan.FATAL, 'FATAL'); - t.end(); -}); diff --git a/test/other-api.test.mjs b/test/other-api.test.mjs new file mode 100644 index 0000000..ae3aebe --- /dev/null +++ b/test/other-api.test.mjs @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012 Trent Mick. All rights reserved. + * + * Test other parts of the exported API. + */ + +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' + + +t.test('bunyan.s', function () { + assert.ok(bunyan.TRACE, 'TRACE'); + assert.ok(bunyan.DEBUG, 'DEBUG'); + assert.ok(bunyan.INFO, 'INFO'); + assert.ok(bunyan.WARN, 'WARN'); + assert.ok(bunyan.ERROR, 'ERROR'); + assert.ok(bunyan.FATAL, 'FATAL'); +}); + +t.test('bunyan.resolveLevel()', function () { + assert.strictEqual(bunyan.resolveLevel('trace'), bunyan.TRACE, 'TRACE'); + assert.strictEqual(bunyan.resolveLevel('TRACE'), bunyan.TRACE, 'TRACE'); + assert.strictEqual(bunyan.resolveLevel('debug'), bunyan.DEBUG, 'DEBUG'); + assert.strictEqual(bunyan.resolveLevel('DEBUG'), bunyan.DEBUG, 'DEBUG'); + assert.strictEqual(bunyan.resolveLevel('info'), bunyan.INFO, 'INFO'); + assert.strictEqual(bunyan.resolveLevel('INFO'), bunyan.INFO, 'INFO'); + assert.strictEqual(bunyan.resolveLevel('warn'), bunyan.WARN, 'WARN'); + assert.strictEqual(bunyan.resolveLevel('WARN'), bunyan.WARN, 'WARN'); + assert.strictEqual(bunyan.resolveLevel('error'), bunyan.ERROR, 'ERROR'); + assert.strictEqual(bunyan.resolveLevel('ERROR'), bunyan.ERROR, 'ERROR'); + assert.strictEqual(bunyan.resolveLevel('fatal'), bunyan.FATAL, 'FATAL'); + assert.strictEqual(bunyan.resolveLevel('FATAL'), bunyan.FATAL, 'FATAL'); +}); diff --git a/test/process-exit.js b/test/process-exit.js deleted file mode 100644 index d273812..0000000 --- a/test/process-exit.js +++ /dev/null @@ -1,11 +0,0 @@ -var bunyan = require('../lib/bunyan'); -var log = bunyan.createLogger({ - name: 'default', - streams: [ { - type: 'rotating-file', - path: __dirname + '/log.test.rot.log', - period: '1d', - count: 7 - } ] -}); -console.log('done'); diff --git a/test/process-exit.test.js b/test/process-exit.test.js deleted file mode 100644 index 2b7a621..0000000 --- a/test/process-exit.test.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; -/* - * Test that bunyan process will terminate. - * - * Note: Currently (bunyan 0.23.1) this fails on node 0.8, because there is - * no `unref` in node 0.8 and bunyan doesn't yet have `Logger.prototype.close()` - * support. - */ - -var exec = require('child_process').exec; - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var test = tap4nodeunit.test; - -var nodeVer = process.versions.node.split('.').map(Number); - -if (nodeVer[0] <= 0 && nodeVer[1] <= 8) { - console.warn('skip test (node <= 0.8)'); -} else { - test('log with rotating file stream will terminate', function (t) { - exec('node ' +__dirname + '/process-exit.js', {timeout: 1000}, - function (err, stdout, stderr) { - t.ifError(err); - t.equal(stdout, 'done\n'); - t.equal(stderr, ''); - t.end(); - }); - }); -} diff --git a/test/raw-stream.test.js b/test/raw-stream.test.js deleted file mode 100644 index 88432f3..0000000 --- a/test/raw-stream.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2012 Trent Mick. All rights reserved. - * - * Test `type: 'raw'` Logger streams. - */ - -var format = require('util').format; -var Logger = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -function CapturingStream(recs) { - this.recs = recs; -} -CapturingStream.prototype.write = function (rec) { - this.recs.push(rec); -} - - -test('raw stream', function (t) { - var recs = []; - - var log = new Logger({ - name: 'raw-stream-test', - streams: [ - { - stream: new CapturingStream(recs), - type: 'raw' - } - ] - }); - log.info('first'); - log.info({two: 'deux'}, 'second'); - - t.equal(recs.length, 2); - t.equal(typeof (recs[0]), 'object', 'first rec is an object'); - t.equal(recs[1].two, 'deux', '"two" field made it through'); - t.end(); -}); - - -test('raw streams and regular streams can mix', function (t) { - var rawRecs = []; - var nonRawRecs = []; - - var log = new Logger({ - name: 'raw-stream-test', - streams: [ - { - stream: new CapturingStream(rawRecs), - type: 'raw' - }, - { - stream: new CapturingStream(nonRawRecs) - } - ] - }); - log.info('first'); - log.info({two: 'deux'}, 'second'); - - t.equal(rawRecs.length, 2); - t.equal(typeof (rawRecs[0]), 'object', 'first rawRec is an object'); - t.equal(rawRecs[1].two, 'deux', '"two" field made it through'); - - t.equal(nonRawRecs.length, 2); - t.equal(typeof (nonRawRecs[0]), 'string', 'first nonRawRec is a string'); - - t.end(); -}); - - -test('child adding a non-raw stream works', function (t) { - var parentRawRecs = []; - var rawRecs = []; - var nonRawRecs = []; - - var logParent = new Logger({ - name: 'raw-stream-test', - streams: [ - { - stream: new CapturingStream(parentRawRecs), - type: 'raw' - } - ] - }); - var logChild = logParent.child({ - child: true, - streams: [ - { - stream: new CapturingStream(rawRecs), - type: 'raw' - }, - { - stream: new CapturingStream(nonRawRecs) - } - ] - }); - logParent.info('first'); - logChild.info({two: 'deux'}, 'second'); - - t.equal(rawRecs.length, 1, - format('rawRecs length should be 1 (is %d)', rawRecs.length)); - t.equal(typeof (rawRecs[0]), 'object', 'rawRec entry is an object'); - t.equal(rawRecs[0].two, 'deux', '"two" field made it through'); - - t.equal(nonRawRecs.length, 1); - t.equal(typeof (nonRawRecs[0]), 'string', 'first nonRawRec is a string'); - - t.end(); -}); diff --git a/test/raw-stream.test.mjs b/test/raw-stream.test.mjs new file mode 100644 index 0000000..5fa45b7 --- /dev/null +++ b/test/raw-stream.test.mjs @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2012 Trent Mick. All rights reserved. + * + * Test `type: 'raw'` Logger streams. + */ + +import { Eltro as t, assert} from 'eltro' +import { format } from 'util' +import bunyan from '../lib/bunyan.mjs' + + +function CapturingStream(recs) { + this.recs = recs; +} +CapturingStream.prototype.write = function (rec) { + this.recs.push(rec); +} + + +t.test('raw stream', function () { + var recs = []; + + var log = new bunyan({ + name: 'raw-stream-test', + streams: [ + { + stream: new CapturingStream(recs), + type: 'raw' + } + ] + }); + log.info('first'); + log.info({two: 'deux'}, 'second'); + + assert.strictEqual(recs.length, 2); + assert.strictEqual(typeof (recs[0]), 'object', 'first rec is an object'); + assert.strictEqual(recs[1].two, 'deux', '"two" field made it through'); +}); + + +t.test('raw streams and regular streams can mix', function () { + var rawRecs = []; + var nonRawRecs = []; + + var log = new bunyan({ + name: 'raw-stream-test', + streams: [ + { + stream: new CapturingStream(rawRecs), + type: 'raw' + }, + { + stream: new CapturingStream(nonRawRecs) + } + ] + }); + log.info('first'); + log.info({two: 'deux'}, 'second'); + + assert.strictEqual(rawRecs.length, 2); + assert.strictEqual(typeof (rawRecs[0]), 'object', 'first rawRec is an object'); + assert.strictEqual(rawRecs[1].two, 'deux', '"two" field made it through'); + + assert.strictEqual(nonRawRecs.length, 2); + assert.strictEqual(typeof (nonRawRecs[0]), 'string', 'first nonRawRec is a string'); + +}); + + +t.test('child adding a non-raw stream works', function () { + var parentRawRecs = []; + var rawRecs = []; + var nonRawRecs = []; + + var logParent = new bunyan({ + name: 'raw-stream-test', + streams: [ + { + stream: new CapturingStream(parentRawRecs), + type: 'raw' + } + ] + }); + var logChild = logParent.child({ + child: true, + streams: [ + { + stream: new CapturingStream(rawRecs), + type: 'raw' + }, + { + stream: new CapturingStream(nonRawRecs) + } + ] + }); + logParent.info('first'); + logChild.info({two: 'deux'}, 'second'); + + assert.strictEqual(rawRecs.length, 1, + format('rawRecs length should be 1 (is %d)', rawRecs.length)); + assert.strictEqual(typeof (rawRecs[0]), 'object', 'rawRec entry is an object'); + assert.strictEqual(rawRecs[0].two, 'deux', '"two" field made it through'); + + assert.strictEqual(nonRawRecs.length, 1); + assert.strictEqual(typeof (nonRawRecs[0]), 'string', 'first nonRawRec is a string'); + +}); diff --git a/test/ringbuffer.test.js b/test/ringbuffer.test.js deleted file mode 100644 index 79d193a..0000000 --- a/test/ringbuffer.test.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Test the RingBuffer output stream. - */ - -var Logger = require('../lib/bunyan'); -var ringbuffer = new Logger.RingBuffer({ 'limit': 5 }); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -var log1 = new Logger({ - name: 'log1', - streams: [ - { - stream: ringbuffer, - type: 'raw', - level: 'info' - } - ] -}); - -test('ringbuffer', function (t) { - log1.info('hello'); - log1.trace('there'); - log1.error('android'); - t.equal(ringbuffer.records.length, 2); - t.equal(ringbuffer.records[0]['msg'], 'hello'); - t.equal(ringbuffer.records[1]['msg'], 'android'); - log1.error('one'); - log1.error('two'); - log1.error('three'); - t.equal(ringbuffer.records.length, 5); - log1.error('four'); - t.equal(ringbuffer.records.length, 5); - t.equal(ringbuffer.records[0]['msg'], 'android'); - t.equal(ringbuffer.records[1]['msg'], 'one'); - t.equal(ringbuffer.records[2]['msg'], 'two'); - t.equal(ringbuffer.records[3]['msg'], 'three'); - t.equal(ringbuffer.records[4]['msg'], 'four'); - t.end(); -}); diff --git a/test/ringbuffer.test.mjs b/test/ringbuffer.test.mjs new file mode 100644 index 0000000..88bd24c --- /dev/null +++ b/test/ringbuffer.test.mjs @@ -0,0 +1,38 @@ +/* + * Test the RingBuffer output stream. + */ + +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' +var ringbuffer = new bunyan.RingBuffer({ 'limit': 5 }); + +var log1 = new bunyan({ + name: 'log1', + streams: [ + { + stream: ringbuffer, + type: 'raw', + level: 'info' + } + ] +}); + +t.test('ringbuffer', function () { + log1.info('hello'); + log1.trace('there'); + log1.error('android'); + assert.strictEqual(ringbuffer.records.length, 2); + assert.strictEqual(ringbuffer.records[0]['msg'], 'hello'); + assert.strictEqual(ringbuffer.records[1]['msg'], 'android'); + log1.error('one'); + log1.error('two'); + log1.error('three'); + assert.strictEqual(ringbuffer.records.length, 5); + log1.error('four'); + assert.strictEqual(ringbuffer.records.length, 5); + assert.strictEqual(ringbuffer.records[0]['msg'], 'android'); + assert.strictEqual(ringbuffer.records[1]['msg'], 'one'); + assert.strictEqual(ringbuffer.records[2]['msg'], 'two'); + assert.strictEqual(ringbuffer.records[3]['msg'], 'three'); + assert.strictEqual(ringbuffer.records[4]['msg'], 'four'); +}); diff --git a/test/safe-json-stringify-1.js b/test/safe-json-stringify-1.mjs similarity index 85% rename from test/safe-json-stringify-1.js rename to test/safe-json-stringify-1.mjs index ba47a2e..584de6f 100644 --- a/test/safe-json-stringify-1.js +++ b/test/safe-json-stringify-1.mjs @@ -1,4 +1,4 @@ -var bunyan = require('../lib/bunyan'); +import bunyan from '../lib/bunyan.mjs' var log = bunyan.createLogger({ name: 'safe-json-stringify-1' diff --git a/test/safe-json-stringify-2.js b/test/safe-json-stringify-2.mjs similarity index 87% rename from test/safe-json-stringify-2.js rename to test/safe-json-stringify-2.mjs index 8910418..a1e309e 100644 --- a/test/safe-json-stringify-2.js +++ b/test/safe-json-stringify-2.mjs @@ -1,5 +1,5 @@ +import bunyan from '../lib/bunyan.mjs' process.env.BUNYAN_TEST_NO_SAFE_JSON_STRINGIFY = '1'; -var bunyan = require('../lib/bunyan'); var log = bunyan.createLogger({ name: 'safe-json-stringify-2' diff --git a/test/safe-json-stringify-3.js b/test/safe-json-stringify-3.mjs similarity index 91% rename from test/safe-json-stringify-3.js rename to test/safe-json-stringify-3.mjs index d2429f8..48f50fb 100644 --- a/test/safe-json-stringify-3.js +++ b/test/safe-json-stringify-3.mjs @@ -1,4 +1,4 @@ -var bunyan = require('../lib/bunyan'); +import bunyan from '../lib/bunyan.mjs' var log = bunyan.createLogger({ name: 'safe-json-stringify-3' diff --git a/test/safe-json-stringify-4.js b/test/safe-json-stringify-4.mjs similarity index 92% rename from test/safe-json-stringify-4.js rename to test/safe-json-stringify-4.mjs index 9e71139..a957ee4 100644 --- a/test/safe-json-stringify-4.js +++ b/test/safe-json-stringify-4.mjs @@ -1,5 +1,5 @@ +import bunyan from '../lib/bunyan.mjs' process.env.BUNYAN_TEST_NO_SAFE_JSON_STRINGIFY = '1'; -var bunyan = require('../lib/bunyan'); var log = bunyan.createLogger({ name: 'safe-json-stringify-4' diff --git a/test/safe-json-stringify.test.js b/test/safe-json-stringify.test.js deleted file mode 100644 index 6b8dc40..0000000 --- a/test/safe-json-stringify.test.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2015 Trent Mick. All rights reserved. - * - * If available, use `safe-json-stringfy` as a fallback stringifier. - * This covers the case where an enumerable property throws an error - * in its getter. - * - * See - */ - -var p = console.warn; -var exec = require('child_process').exec; - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -test('__defineGetter__ boom', function (t) { - var cmd = process.execPath + ' ' + __dirname + '/safe-json-stringify-1.js'; - exec(cmd, function (err, stdout, stderr) { - t.ifError(err, err); - var rec = JSON.parse(stdout.trim()); - t.equal(rec.obj.boom, '[Throws: __defineGetter__ ouch!]'); - t.end(); - }); -}); - -test('__defineGetter__ boom, without safe-json-stringify', function (t) { - var cmd = process.execPath + ' ' + __dirname + '/safe-json-stringify-2.js'; - exec(cmd, function (err, stdout, stderr) { - t.ifError(err, err); - t.ok(stdout.indexOf('Exception in JSON.stringify') !== -1); - t.ok(stderr.indexOf( - 'You can install the "safe-json-stringify" module') !== -1); - t.end(); - }); -}); - -test('defineProperty boom', function (t) { - var cmd = process.execPath + ' ' + __dirname + '/safe-json-stringify-3.js'; - exec(cmd, function (err, stdout, stderr) { - t.ifError(err, err); - var recs = stdout.trim().split(/\n/g); - t.equal(recs.length, 2); - var rec = JSON.parse(recs[0]); - t.equal(rec.obj.boom, '[Throws: defineProperty ouch!]'); - t.end(); - }); -}); - -test('defineProperty boom, without safe-json-stringify', function (t) { - var cmd = process.execPath + ' ' + __dirname + '/safe-json-stringify-4.js'; - exec(cmd, function (err, stdout, stderr) { - t.ifError(err, err); - t.ok(stdout.indexOf('Exception in JSON.stringify') !== -1); - t.equal(stdout.match(/Exception in JSON.stringify/g).length, 2); - t.ok(stderr.indexOf( - 'You can install the "safe-json-stringify" module') !== -1); - t.equal(stderr.match( - /* JSSTYLED */ - /You can install the "safe-json-stringify" module/g).length, 1); - t.end(); - }); -}); diff --git a/test/safe-json-stringify.test.mjs b/test/safe-json-stringify.test.mjs new file mode 100644 index 0000000..0a5774e --- /dev/null +++ b/test/safe-json-stringify.test.mjs @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2015 Trent Mick. All rights reserved. + * + * If available, use `safe-json-stringfy` as a fallback stringifier. + * This covers the case where an enumerable property throws an error + * in its getter. + * + * See + */ + +import { Eltro as t, assert} from 'eltro' +import { exec, dirname } from './helper.mjs' + + +t.test('__defineGetter__ boom', async function () { + let res = await exec('', 'node', dirname('/safe-json-stringify-1.mjs')) + + var rec = JSON.parse(res.stdout.trim()); + assert.strictEqual(rec.obj.boom, '[Throws: __defineGetter__ ouch!]'); +}); + +t.test('__defineGetter__ boom, without safe-json-stringify', async function () { + let res = await exec('', 'node', dirname('/safe-json-stringify-2.mjs')) + assert.ok(res.stdout.indexOf('Exception in JSON.stringify') !== -1); + assert.ok(res.stderr.indexOf( + 'You can install the "safe-json-stringify" module') !== -1); +}); + +t.test('defineProperty boom', async function () { + let res = await exec('', 'node', dirname('/safe-json-stringify-3.mjs')) + var recs = res.stdout.trim().split(/\n/g); + assert.strictEqual(recs.length, 2); + var rec = JSON.parse(recs[0]); + assert.strictEqual(rec.obj.boom, '[Throws: defineProperty ouch!]'); +}); + +t.test('defineProperty boom, without safe-json-stringify', async function () { + let res = await exec('', 'node', dirname('/safe-json-stringify-4.mjs')) + + assert.ok(res.stdout.indexOf('Exception in JSON.stringify') !== -1); + assert.strictEqual(res.stdout.match(/Exception in JSON.stringify/g).length, 2); + assert.ok(res.stderr.indexOf( + 'You can install the "safe-json-stringify" module') !== -1); + assert.strictEqual(res.stderr.match( + /You can install the "safe-json-stringify" module/g).length, 1); +}); diff --git a/test/safe-json.test.mjs b/test/safe-json.test.mjs new file mode 100644 index 0000000..1d12b35 --- /dev/null +++ b/test/safe-json.test.mjs @@ -0,0 +1,175 @@ +import { Eltro as t, assert} from 'eltro' +import safeJson from '../lib/safe-json.mjs' + +t.test('basic stringify', function() { + assert.strictEqual('"foo"', safeJson('foo')); + assert.strictEqual('{"foo":"bar"}', safeJson({foo: 'bar'})); +}); + +t.test('object identity', function() { + var a = { foo: 'bar' }; + var b = { one: a, two: a }; + assert.strictEqual('{"one":{"foo":"bar"},"two":{"foo":"bar"}}',safeJson(b)); +}); + +t.test('circular references', function() { + var a = {}; + a.a = a; + a.b = 'c'; + + assert.doesNotThrow( + function() { safeJson(a); }, + 'should not exceed stack size' + ); + + assert.strictEqual( + '{"a":"[Circular]","b":"c"}', + safeJson(a) + ); +}); + +t.test('null', function() { + assert.strictEqual( + '{"x":null}', + safeJson({x: null}) + ) +}); + +t.test('arrays', function() { + var arr = [ 2 ]; + assert.strictEqual( + '[2]', + safeJson(arr) + ); + + arr.push(arr); + + assert.strictEqual( + '[2,"[Circular]"]', + safeJson(arr) + ); + + assert.strictEqual( + '{"x":[2,"[Circular]"]}', + safeJson({x: arr}) + ); +}); + +t.test('throwing toJSON', function() { + var obj = { + toJSON: function() { + throw new Error('Failing'); + } + }; + + assert.strictEqual( + '"[Throws: Failing]"', + safeJson(obj) + ); + + assert.strictEqual( + '{"x":"[Throws: Failing]"}', + safeJson({ x: obj }) + ); +}); + +t.test('properties on Object.create(null)', function() { + var obj = Object.create(null, { + foo: { + get: function() { return 'bar'; }, + enumerable: true + } + }); + assert.strictEqual( + '{"foo":"bar"}', + safeJson(obj) + ); + + var obj = Object.create(null, { + foo: { + get: function() { return 'bar'; }, + enumerable: true + }, + broken: { + get: function() { throw new Error('Broken'); }, + enumerable: true + } + }); + assert.strictEqual( + '{"foo":"bar","broken":"[Throws: Broken]"}', + safeJson(obj) + ); +}); + +t.test('defined getter properties using __defineGetter__', function() { + // non throwing + var obj = {}; + obj.__defineGetter__('foo', function() { return 'bar'; }); + assert.strictEqual( + '{"foo":"bar"}', + safeJson(obj) + ); + + // throwing + obj = {}; + obj.__defineGetter__('foo', function() { return undefined['oh my']; }); + + assert.doesNotThrow( + function(){ safeJson(obj)} + ); + + assert.strictEqual( + '{"foo":"[Throws: Cannot read property \'oh my\' of undefined]"}', + safeJson(obj) + ); +}); + +t.test('enumerable defined getter properties using Object.defineProperty', function() { + // non throwing + var obj = {}; + Object.defineProperty(obj, 'foo', {get: function() { return 'bar'; }, enumerable: true}); + assert.strictEqual( + '{"foo":"bar"}', + safeJson(obj) + ); + + // throwing + obj = {}; + Object.defineProperty(obj, 'foo', {get: function() { return undefined['oh my']; }, enumerable: true}); + + assert.doesNotThrow( + function(){ safeJson(obj)} + ); + + assert.strictEqual( + '{"foo":"[Throws: Cannot read property \'oh my\' of undefined]"}', + safeJson(obj) + ); +}); + +t.test('formatting', function() { + var obj = {a:{b:1, c:[{d: 1}]}}; // some nested object + var formatters = [3, "\t", " "]; + formatters.forEach((formatter) => { + assert.strictEqual( + JSON.stringify(obj, null, formatter), + safeJson(obj, null, formatter) + ); + }); +}); + +t.test('replacing', function() { + var obj = {a:{b:1, c:[{d: 1}]}}; // some nested object + var replacers = [ + ["a", "c"], + (k, v) => typeof v == 'number' ? "***" : v, + () => undefined, + [] + ]; + replacers.forEach((replacer) => { + assert.strictEqual( + JSON.stringify(obj, replacer), + safeJson(obj, replacer) + ); + }); +}); diff --git a/test/serializers.test.js b/test/serializers.test.js deleted file mode 100644 index 1cddec0..0000000 --- a/test/serializers.test.js +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright (c) 2012 Trent Mick. All rights reserved. - * - * Test the standard serializers in Bunyan. - */ - -var http = require('http'); - -var bunyan = require('../lib/bunyan'); -var verror = require('verror'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -function CapturingStream(recs) { - this.recs = recs; -} -CapturingStream.prototype.write = function (rec) { - this.recs.push(rec); -} - - -test('req serializer', function (t) { - var records = []; - var log = bunyan.createLogger({ - name: 'serializer-test', - streams: [ - { - stream: new CapturingStream(records), - type: 'raw' - } - ], - serializers: { - req: bunyan.stdSerializers.req - } - }); - - // None of these should blow up. - var bogusReqs = [ - undefined, - null, - {}, - 1, - 'string', - [1, 2, 3], - {'foo':'bar'} - ]; - for (var i = 0; i < bogusReqs.length; i++) { - log.info({req: bogusReqs[i]}, 'hi'); - t.equal(records[i].req, bogusReqs[i]); - } - - // Get http request and response objects to play with and test. - var theReq, theRes; - var server = http.createServer(function (req, res) { - theReq = req; - theRes = res; - res.writeHead(200, {'Content-Type': 'text/plain'}); - res.end('Hello World\n'); - }) - server.listen(8765, function () { - http.get({host: '127.0.0.1', port: 8765, path: '/'}, function (res) { - res.resume(); - log.info({req: theReq}, 'the request'); - var lastRecord = records[records.length-1]; - t.equal(lastRecord.req.method, 'GET'); - t.equal(lastRecord.req.url, theReq.url); - t.equal(lastRecord.req.remoteAddress, - theReq.connection.remoteAddress); - t.equal(lastRecord.req.remotePort, theReq.connection.remotePort); - server.close(); - t.end(); - }).on('error', function (err) { - t.ok(false, 'error requesting to our test server: ' + err); - server.close(); - t.end(); - }); - }); -}); - - -test('res serializer', function (t) { - var records = []; - var log = bunyan.createLogger({ - name: 'serializer-test', - streams: [ - { - stream: new CapturingStream(records), - type: 'raw' - } - ], - serializers: { - res: bunyan.stdSerializers.res - } - }); - - // None of these should blow up. - var bogusRess = [ - undefined, - null, - {}, - 1, - 'string', - [1, 2, 3], - {'foo':'bar'} - ]; - for (var i = 0; i < bogusRess.length; i++) { - log.info({res: bogusRess[i]}, 'hi'); - t.equal(records[i].res, bogusRess[i]); - } - - // Get http request and response objects to play with and test. - var theReq, theRes; - var server = http.createServer(function (req, res) { - theReq = req; - theRes = res; - res.writeHead(200, {'Content-Type': 'text/plain'}); - res.end('Hello World\n'); - }) - server.listen(8765, function () { - http.get({host: '127.0.0.1', port: 8765, path: '/'}, function (res) { - res.resume(); - log.info({res: theRes}, 'the response'); - var lastRecord = records[records.length-1]; - t.equal(lastRecord.res.statusCode, theRes.statusCode); - t.equal(lastRecord.res.header, theRes._header); - server.close(); - t.end(); - }).on('error', function (err) { - t.ok(false, 'error requesting to our test server: ' + err); - server.close(); - t.end(); - }); - }); -}); - - -test('err serializer', function (t) { - var records = []; - var log = bunyan.createLogger({ - name: 'serializer-test', - streams: [ - { - stream: new CapturingStream(records), - type: 'raw' - } - ], - serializers: { - err: bunyan.stdSerializers.err - } - }); - - // None of these should blow up. - var bogusErrs = [ - undefined, - null, - {}, - 1, - 'string', - [1, 2, 3], - {'foo':'bar'} - ]; - for (var i = 0; i < bogusErrs.length; i++) { - log.info({err: bogusErrs[i]}, 'hi'); - t.equal(records[i].err, bogusErrs[i]); - } - - var theErr = new TypeError('blah'); - - log.info(theErr, 'the error'); - var lastRecord = records[records.length-1]; - t.equal(lastRecord.err.message, theErr.message); - t.equal(lastRecord.err.name, theErr.name); - t.equal(lastRecord.err.stack, theErr.stack); - t.end(); -}); - -test('err serializer: custom serializer', function (t) { - var records = []; - - function customSerializer(err) { - return { - message: err.message, - name: err.name, - stack: err.stack, - beep: err.beep - }; - } - - var log = bunyan.createLogger({ - name: 'serializer-test', - streams: [ - { - stream: new CapturingStream(records), - type: 'raw' - } - ], - serializers: { - err: customSerializer - } - }); - - var e1 = new Error('message1'); - e1.beep = 'bop'; - var e2 = new Error('message2'); - var errs = [e1, e2]; - - for (var i = 0; i < errs.length; i++) { - log.info(errs[i]); - t.equal(records[i].err.message, errs[i].message); - t.equal(records[i].err.beep, errs[i].beep); - } - t.end(); -}); - -test('err serializer: long stack', function (t) { - var records = []; - var log = bunyan.createLogger({ - name: 'serializer-test', - streams: [ { - stream: new CapturingStream(records), - type: 'raw' - } ], - serializers: { - err: bunyan.stdSerializers.err - } - }); - - var topErr, midErr, bottomErr; - - // Just a VError. - topErr = new verror.VError('top err'); - log.info(topErr, 'the error'); - var lastRecord = records[records.length-1]; - t.equal(lastRecord.err.message, topErr.message, 'Just a VError'); - t.equal(lastRecord.err.name, topErr.name, 'Just a VError'); - t.equal(lastRecord.err.stack, topErr.stack, 'Just a VError'); - - // Just a WError. - topErr = new verror.WError('top err'); - log.info(topErr, 'the error'); - var lastRecord = records[records.length-1]; - t.equal(lastRecord.err.message, topErr.message, 'Just a WError'); - t.equal(lastRecord.err.name, topErr.name, 'Just a WError'); - t.equal(lastRecord.err.stack, topErr.stack, 'Just a WError'); - - // WError <- TypeError - bottomErr = new TypeError('bottom err'); - topErr = new verror.WError(bottomErr, 'top err'); - log.info(topErr, 'the error'); - var lastRecord = records[records.length-1]; - t.equal(lastRecord.err.message, topErr.message, 'WError <- TypeError'); - t.equal(lastRecord.err.name, topErr.name, 'WError <- TypeError'); - var expectedStack = topErr.stack + '\nCaused by: ' + bottomErr.stack; - t.equal(lastRecord.err.stack, expectedStack, 'WError <- TypeError'); - - // WError <- WError - bottomErr = new verror.WError('bottom err'); - topErr = new verror.WError(bottomErr, 'top err'); - log.info(topErr, 'the error'); - var lastRecord = records[records.length-1]; - t.equal(lastRecord.err.message, topErr.message, 'WError <- WError'); - t.equal(lastRecord.err.name, topErr.name, 'WError <- WError'); - var expectedStack = topErr.stack + '\nCaused by: ' + bottomErr.stack; - t.equal(lastRecord.err.stack, expectedStack, 'WError <- WError'); - - // WError <- WError <- TypeError - bottomErr = new TypeError('bottom err'); - midErr = new verror.WError(bottomErr, 'mid err'); - topErr = new verror.WError(midErr, 'top err'); - log.info(topErr, 'the error'); - var lastRecord = records[records.length-1]; - t.equal(lastRecord.err.message, topErr.message, - 'WError <- WError <- TypeError'); - t.equal(lastRecord.err.name, topErr.name, 'WError <- WError <- TypeError'); - var expectedStack = (topErr.stack - + '\nCaused by: ' + midErr.stack - + '\nCaused by: ' + bottomErr.stack); - t.equal(lastRecord.err.stack, expectedStack, - 'WError <- WError <- TypeError'); - - // WError <- WError <- WError - bottomErr = new verror.WError('bottom err'); - midErr = new verror.WError(bottomErr, 'mid err'); - topErr = new verror.WError(midErr, 'top err'); - log.info(topErr, 'the error'); - var lastRecord = records[records.length-1]; - t.equal(lastRecord.err.message, topErr.message, - 'WError <- WError <- WError'); - t.equal(lastRecord.err.name, topErr.name, 'WError <- WError <- WError'); - var expectedStack = (topErr.stack - + '\nCaused by: ' + midErr.stack - + '\nCaused by: ' + bottomErr.stack); - t.equal(lastRecord.err.stack, expectedStack, 'WError <- WError <- WError'); - - t.end(); -}); - - -// Bunyan 0.18.3 introduced a bug where *all* serializers are applied -// even if the log record doesn't have the associated key. That means -// serializers that don't handle an `undefined` value will blow up. -test('do not apply serializers if no record key', function (t) { - var records = []; - var log = bunyan.createLogger({ - name: 'serializer-test', - streams: [ { - stream: new CapturingStream(records), - type: 'raw' - } ], - serializers: { - err: bunyan.stdSerializers.err, - boom: function (value) { - throw new Error('boom'); - } - } - }); - - log.info({foo: 'bar'}, 'record one'); - log.info({err: new Error('record two err')}, 'record two'); - - t.equal(records[0].boom, undefined); - t.equal(records[0].foo, 'bar'); - t.equal(records[1].boom, undefined); - t.equal(records[1].err.message, 'record two err'); - - t.end(); -}); diff --git a/test/serializers.test.mjs b/test/serializers.test.mjs new file mode 100644 index 0000000..4ac6127 --- /dev/null +++ b/test/serializers.test.mjs @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2012 Trent Mick. All rights reserved. + * + * Test the standard serializers in Bunyan. + */ + +import http from 'http' +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' + +function CapturingStream(recs) { + this.recs = recs; +} +CapturingStream.prototype.write = function (rec) { + this.recs.push(rec); +} + +t.test('req serializer', function (cb) { + var records = []; + var log = bunyan.createLogger({ + name: 'serializer-test', + streams: [ + { + stream: new CapturingStream(records), + type: 'raw' + } + ], + serializers: { + req: bunyan.stdSerializers.req + } + }); + + // None of these should blow up. + var bogusReqs = [ + undefined, + null, + {}, + 1, + 'string', + [1, 2, 3], + {'foo':'bar'} + ]; + for (var i = 0; i < bogusReqs.length; i++) { + log.info({req: bogusReqs[i]}, 'hi'); + assert.strictEqual(records[i].req, bogusReqs[i]); + } + + // Get http request and response objects to play with and test. + var theReq, theRes; + var server = http.createServer(function (req, res) { + theReq = req; + theRes = res; + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('Hello World\n'); + }) + server.listen(8765, function () { + http.get({host: '127.0.0.1', port: 8765, path: '/'}, function (res) { + res.resume(); + log.info({req: theReq}, 'the request'); + var lastRecord = records[records.length-1]; + + try { + assert.strictEqual(lastRecord.req.method, 'GET'); + assert.strictEqual(lastRecord.req.url, theReq.url); + assert.strictEqual(lastRecord.req.remoteAddress, + theReq.connection.remoteAddress); + assert.strictEqual(lastRecord.req.remotePort, theReq.connection.remotePort); + } catch (err) { + cb(err) + } + server.close(); + cb() + }).on('error', function (err) { + server.close(); + cb(err) + }); + }); +}); + + +t.test('res serializer', function (cb) { + var records = []; + var log = bunyan.createLogger({ + name: 'serializer-test', + streams: [ + { + stream: new CapturingStream(records), + type: 'raw' + } + ], + serializers: { + res: bunyan.stdSerializers.res + } + }); + + // None of these should blow up. + var bogusRess = [ + undefined, + null, + {}, + 1, + 'string', + [1, 2, 3], + {'foo':'bar'} + ]; + for (var i = 0; i < bogusRess.length; i++) { + log.info({res: bogusRess[i]}, 'hi'); + assert.strictEqual(records[i].res, bogusRess[i]); + } + + // Get http request and response objects to play with and test. + var theReq, theRes; + var server = http.createServer(function (req, res) { + theReq = req; + theRes = res; + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('Hello World\n'); + }) + server.listen(8765, function () { + http.get({host: '127.0.0.1', port: 8765, path: '/'}, function (res) { + res.resume(); + log.info({res: theRes}, 'the response'); + var lastRecord = records[records.length-1]; + try { + assert.strictEqual(lastRecord.res.statusCode, theRes.statusCode); + assert.strictEqual(lastRecord.res.header, theRes._header); + } catch (err) { + cb(err) + } + server.close(); + cb() + }).on('error', function (err) { + server.close(); + cb(err) + }); + }); +}); + + +t.test('err serializer', function () { + var records = []; + var log = bunyan.createLogger({ + name: 'serializer-test', + streams: [ + { + stream: new CapturingStream(records), + type: 'raw' + } + ], + serializers: { + err: bunyan.stdSerializers.err + } + }); + + // None of these should blow up. + var bogusErrs = [ + undefined, + null, + {}, + 1, + 'string', + [1, 2, 3], + {'foo':'bar'} + ]; + for (var i = 0; i < bogusErrs.length; i++) { + log.info({err: bogusErrs[i]}, 'hi'); + assert.strictEqual(records[i].err, bogusErrs[i]); + } + + var theErr = new TypeError('blah'); + + log.info(theErr, 'the error'); + var lastRecord = records[records.length-1]; + assert.strictEqual(lastRecord.err.message, theErr.message); + assert.strictEqual(lastRecord.err.name, theErr.name); + assert.strictEqual(lastRecord.err.stack, theErr.stack); +}); + +t.test('err serializer: custom serializer', function () { + var records = []; + + function customSerializer(err) { + return { + message: err.message, + name: err.name, + stack: err.stack, + beep: err.beep + }; + } + + var log = bunyan.createLogger({ + name: 'serializer-test', + streams: [ + { + stream: new CapturingStream(records), + type: 'raw' + } + ], + serializers: { + err: customSerializer + } + }); + + var e1 = new Error('message1'); + e1.beep = 'bop'; + var e2 = new Error('message2'); + var errs = [e1, e2]; + + for (var i = 0; i < errs.length; i++) { + log.info(errs[i]); + assert.strictEqual(records[i].err.message, errs[i].message); + assert.strictEqual(records[i].err.beep, errs[i].beep); + } +}); + +// Bunyan 0.18.3 introduced a bug where *all* serializers are applied +// even if the log record doesn't have the associated key. That means +// serializers that don't handle an `undefined` value will blow up. +t.test('do not apply serializers if no record key', function () { + var records = []; + var log = bunyan.createLogger({ + name: 'serializer-test', + streams: [ { + stream: new CapturingStream(records), + type: 'raw' + } ], + serializers: { + err: bunyan.stdSerializers.err, + boom: function (value) { + throw new Error('boom'); + } + } + }); + + log.info({foo: 'bar'}, 'record one'); + log.info({err: new Error('record two err')}, 'record two'); + + assert.strictEqual(records[0].boom, undefined); + assert.strictEqual(records[0].foo, 'bar'); + assert.strictEqual(records[1].boom, undefined); + assert.strictEqual(records[1].err.message, 'record two err'); +}); diff --git a/test/src.test.js b/test/src.test.js deleted file mode 100644 index 43553d1..0000000 --- a/test/src.test.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2015 Trent Mick. - * - * Test `src: true` usage. - */ - -// Intentionally on line 8 for tests below: -function logSomething(log) { log.info('something'); } - - -var format = require('util').format; -var Logger = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var after = tap4nodeunit.after; -var before = tap4nodeunit.before; -var test = tap4nodeunit.test; - - -function CapturingStream(recs) { - this.recs = recs; -} -CapturingStream.prototype.write = function (rec) { - this.recs.push(rec); -} - - -test('src', function (t) { - var recs = []; - - var log = new Logger({ - name: 'src-test', - src: true, - streams: [ - { - stream: new CapturingStream(recs), - type: 'raw' - } - ] - }); - - log.info('top-level'); - logSomething(log); - - t.equal(recs.length, 2); - recs.forEach(function (rec) { - t.ok(rec.src); - t.equal(typeof (rec.src), 'object'); - t.equal(rec.src.file, __filename); - t.ok(rec.src.line); - t.equal(typeof (rec.src.line), 'number'); - }); - var rec = recs[1]; - t.ok(rec.src.func); - t.equal(rec.src.func, 'logSomething'); - t.equal(rec.src.line, 8); - - t.end(); -}); diff --git a/test/src.test.mjs b/test/src.test.mjs new file mode 100644 index 0000000..7db4d77 --- /dev/null +++ b/test/src.test.mjs @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2015 Trent Mick. + * + * Test `src: true` usage. + */ + +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' + +// Intentionally on line 11 for tests below: +function logSomething(log) { log.info('something'); } + +function CapturingStream(recs) { + this.recs = recs; +} +CapturingStream.prototype.write = function (rec) { + this.recs.push(rec); +} + +// commented out due to broken implementation of +// getting caller line in strict mode (getCaller3Info()) + +/* t.only().test('src', function () { + var recs = []; + + var log = new bunyan({ + name: 'src-test', + src: true, + streams: [ + { + stream: new CapturingStream(recs), + type: 'raw' + } + ] + }); + + log.info('top-level'); + logSomething(log); + + assert.strictEqual(recs.length, 2); + recs.forEach(function (rec) { + assert.ok(rec.src); + assert.strictEqual(typeof (rec.src), 'object'); + assert.strictEqual(rec.src.file, __filename); + assert.ok(rec.src.line); + assert.strictEqual(typeof (rec.src.line), 'number'); + }); + var rec = recs[1]; + assert.ok(rec.src.func); + assert.strictEqual(rec.src.func, 'logSomething'); + assert.strictEqual(rec.src.line, 11); +});*/ diff --git a/test/stream-levels.test.js b/test/stream-levels.test.js deleted file mode 100644 index 6e12e09..0000000 --- a/test/stream-levels.test.js +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) 2015 Trent Mick. All rights reserved. - * - * Test that streams (the various way they can be added to - * a Logger instance) get the appropriate level. - */ - -var util = require('util'), - format = util.format, - inspect = util.inspect; -var p = console.log; - -var bunyan = require('../lib/bunyan'); - -// node-tap API -if (require.cache[__dirname + '/tap4nodeunit.js']) - delete require.cache[__dirname + '/tap4nodeunit.js']; -var tap4nodeunit = require('./tap4nodeunit.js'); -var test = tap4nodeunit.test; - - -// ---- Tests - -var log1 = bunyan.createLogger({ - name: 'log1', - streams: [ - { - path: __dirname + '/level.test.log1.log', - level: 'info' - } - ] -}); - - -test('default stream log level', function (t) { - var log = bunyan.createLogger({ - name: 'foo' - }); - t.equal(log.level(), bunyan.INFO); - t.equal(log.streams[0].level, bunyan.INFO); - t.end(); -}); - -test('default stream, level=DEBUG specified', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - level: bunyan.DEBUG - }); - t.equal(log.level(), bunyan.DEBUG); - t.equal(log.streams[0].level, bunyan.DEBUG); - t.end(); -}); - -test('default stream, level="trace" specified', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - level: 'trace' - }); - t.equal(log.level(), bunyan.TRACE); - t.equal(log.streams[0].level, bunyan.TRACE); - t.end(); -}); - -test('stream & level="trace" specified', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - stream: process.stderr, - level: 'trace' - }); - t.equal(log.level(), bunyan.TRACE); - t.equal(log.streams[0].level, bunyan.TRACE); - t.end(); -}); - -test('one stream, default level', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - streams: [ - { - stream: process.stderr - } - ] - }); - t.equal(log.level(), bunyan.INFO); - t.equal(log.streams[0].level, bunyan.INFO); - t.end(); -}); - -test('one stream, top-"level" specified', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - level: 'error', - streams: [ - { - stream: process.stderr - } - ] - }); - t.equal(log.level(), bunyan.ERROR); - t.equal(log.streams[0].level, bunyan.ERROR); - t.end(); -}); - -test('one stream, stream-"level" specified', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - streams: [ - { - stream: process.stderr, - level: 'error' - } - ] - }); - t.equal(log.level(), bunyan.ERROR); - t.equal(log.streams[0].level, bunyan.ERROR); - t.end(); -}); - -test('one stream, both-"level" specified', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - level: 'debug', - streams: [ - { - stream: process.stderr, - level: 'error' - } - ] - }); - t.equal(log.level(), bunyan.ERROR); - t.equal(log.streams[0].level, bunyan.ERROR); - t.end(); -}); - -test('two streams, both-"level" specified', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - level: 'debug', - streams: [ - { - stream: process.stdout, - level: 'trace' - }, - { - stream: process.stderr, - level: 'fatal' - } - ] - }); - t.equal(log.level(), bunyan.TRACE, 'log.level()'); - t.equal(log.streams[0].level, bunyan.TRACE); - t.equal(log.streams[1].level, bunyan.FATAL); - t.end(); -}); - -test('two streams, one with "level" specified', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - streams: [ - { - stream: process.stdout, - }, - { - stream: process.stderr, - level: 'fatal' - } - ] - }); - t.equal(log.level(), bunyan.INFO); - t.equal(log.streams[0].level, bunyan.INFO); - t.equal(log.streams[1].level, bunyan.FATAL); - t.end(); -}); - -// Issue #335 -test('log level 0 to turn on all logging', function (t) { - var log = bunyan.createLogger({ - name: 'foo', - level: 0 - }); - t.equal(log.level(), 0); - t.equal(log.streams[0].level, 0); - t.end(); -}); diff --git a/test/stream-levels.test.mjs b/test/stream-levels.test.mjs new file mode 100644 index 0000000..a1e9f8f --- /dev/null +++ b/test/stream-levels.test.mjs @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2015 Trent Mick. All rights reserved. + * + * Test that streams (the various way they can be added to + * a Logger instance) get the appropriate level. + */ +import { Eltro as t, assert} from 'eltro' +import bunyan from '../lib/bunyan.mjs' + + +// ---- Tests + + +t.test('default stream log level', function () { + var log = bunyan.createLogger({ + name: 'foo' + }); + assert.strictEqual(log.level(), bunyan.INFO); + assert.strictEqual(log.streams[0].level, bunyan.INFO); +}); + +t.test('default stream, level=DEBUG specified', function () { + var log = bunyan.createLogger({ + name: 'foo', + level: bunyan.DEBUG + }); + assert.strictEqual(log.level(), bunyan.DEBUG); + assert.strictEqual(log.streams[0].level, bunyan.DEBUG); +}); + +t.test('default stream, level="trace" specified', function () { + var log = bunyan.createLogger({ + name: 'foo', + level: 'trace' + }); + assert.strictEqual(log.level(), bunyan.TRACE); + assert.strictEqual(log.streams[0].level, bunyan.TRACE); +}); + +t.test('stream & level="trace" specified', function () { + var log = bunyan.createLogger({ + name: 'foo', + stream: process.stderr, + level: 'trace' + }); + assert.strictEqual(log.level(), bunyan.TRACE); + assert.strictEqual(log.streams[0].level, bunyan.TRACE); +}); + +t.test('one stream, default level', function () { + var log = bunyan.createLogger({ + name: 'foo', + streams: [ + { + stream: process.stderr + } + ] + }); + assert.strictEqual(log.level(), bunyan.INFO); + assert.strictEqual(log.streams[0].level, bunyan.INFO); +}); + +t.test('one stream, top-"level" specified', function () { + var log = bunyan.createLogger({ + name: 'foo', + level: 'error', + streams: [ + { + stream: process.stderr + } + ] + }); + assert.strictEqual(log.level(), bunyan.ERROR); + assert.strictEqual(log.streams[0].level, bunyan.ERROR); +}); + +t.test('one stream, stream-"level" specified', function () { + var log = bunyan.createLogger({ + name: 'foo', + streams: [ + { + stream: process.stderr, + level: 'error' + } + ] + }); + assert.strictEqual(log.level(), bunyan.ERROR); + assert.strictEqual(log.streams[0].level, bunyan.ERROR); +}); + +t.test('one stream, both-"level" specified', function () { + var log = bunyan.createLogger({ + name: 'foo', + level: 'debug', + streams: [ + { + stream: process.stderr, + level: 'error' + } + ] + }); + assert.strictEqual(log.level(), bunyan.ERROR); + assert.strictEqual(log.streams[0].level, bunyan.ERROR); +}); + +t.test('two streams, both-"level" specified', function () { + var log = bunyan.createLogger({ + name: 'foo', + level: 'debug', + streams: [ + { + stream: process.stdout, + level: 'trace' + }, + { + stream: process.stderr, + level: 'fatal' + } + ] + }); + assert.strictEqual(log.level(), bunyan.TRACE, 'log.level()'); + assert.strictEqual(log.streams[0].level, bunyan.TRACE); + assert.strictEqual(log.streams[1].level, bunyan.FATAL); +}); + +t.test('two streams, one with "level" specified', function () { + var log = bunyan.createLogger({ + name: 'foo', + streams: [ + { + stream: process.stdout, + }, + { + stream: process.stderr, + level: 'fatal' + } + ] + }); + assert.strictEqual(log.level(), bunyan.INFO); + assert.strictEqual(log.streams[0].level, bunyan.INFO); + assert.strictEqual(log.streams[1].level, bunyan.FATAL); +}); + +// Issue #335 +t.test('log level 0 to turn on all logging', function () { + var log = bunyan.createLogger({ + name: 'foo', + level: 0 + }); + assert.strictEqual(log.level(), 0); + assert.strictEqual(log.streams[0].level, 0); +}); diff --git a/test/tap4nodeunit.js b/test/tap4nodeunit.js deleted file mode 100644 index 5c46e4d..0000000 --- a/test/tap4nodeunit.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2012 Mark Cavage. All rights reserved. - * - * Help nodeunit API feel like node-tap's. - * - * Usage: - * if (require.cache[__dirname + '/tap4nodeunit.js']) - * delete require.cache[__dirname + '/tap4nodeunit.js']; - * var tap4nodeunit = require('./tap4nodeunit.js'); - * var after = tap4nodeunit.after; - * var before = tap4nodeunit.before; - * var test = tap4nodeunit.test; - */ - - - -//---- Exports - -module.exports = { - after: function after(teardown) { - module.parent.exports.tearDown = function _teardown(callback) { - try { - teardown.call(this, callback); - } catch (e) { - console.error('after:\n' + e.stack); - process.exit(1); - } - }; - }, - - before: function before(setup) { - module.parent.exports.setUp = function _setup(callback) { - try { - setup.call(this, callback); - } catch (e) { - console.error('before:\n' + e.stack); - process.exit(1); - } - }; - }, - - test: function test(name, tester) { - module.parent.exports[name] = function _(t) { - var _done = false; - t.end = function end() { - if (!_done) { - _done = true; - t.done(); - } - }; - t.notOk = function notOk(ok, message) { - return (t.ok(!ok, message)); - }; - t.error = t.ifError; - - tester.call(this, t); - }; - } - - -}; diff --git a/tools/ben.js b/tools/ben.js new file mode 100644 index 0000000..a15f534 --- /dev/null +++ b/tools/ben.js @@ -0,0 +1,43 @@ +// Bring ben to local and remove one more dependancy. +// The package ben has either way not been updated in over 10 years. +// https://github.com/substack/node-ben + +var ben = module.exports = function (times, cb) { + if (typeof times === 'function') { + cb = times; + times = 10000; + } + + var t0 = Date.now(); + for (var i = 0; i < times; i++) { + cb(); + } + var elapsed = Date.now() - t0; + + return elapsed / times; +}; +ben.sync = ben; + +ben.async = function (times, cb, resultCb) { + if (typeof times === 'function') { + resultCb = cb; + cb = times; + times = 100; + } + + var pending = times; + var t = Date.now(); + var elapsed = 0; + + cb(function fn () { + elapsed += Date.now() - t; + + if (--pending === 0) { + resultCb(elapsed / times); + } + else { + t = Date.now(); + cb(fn); + } + }); +}; diff --git a/tools/timechild.js b/tools/timechild.js index 168f759..1dcf1b6 100755 --- a/tools/timechild.js +++ b/tools/timechild.js @@ -13,7 +13,7 @@ * Result: Another order of magnitude. */ -var ben = require('ben'); // npm install ben +var ben = require('./ben'); // npm install ben var Logger = require('../lib/bunyan'); var log = new Logger({ diff --git a/tools/timeguard.js b/tools/timeguard.js index dc1ca06..c7813a0 100755 --- a/tools/timeguard.js +++ b/tools/timeguard.js @@ -6,7 +6,7 @@ console.log('Time JSON.stringify and alternatives in Logger._emit:'); -var ben = require('ben'); // npm install ben +var ben = require('./ben'); // npm install ben var bunyan = require('../lib/bunyan'); function Collector() {} diff --git a/tools/timenop.js b/tools/timenop.js index 11cde8d..d0db55b 100755 --- a/tools/timenop.js +++ b/tools/timenop.js @@ -5,7 +5,7 @@ console.log('Time log.trace() when log level is "info":'); -var ben = require('ben'); // npm install ben +var ben = require('./ben'); // npm install ben var bunyan = require('../lib/bunyan'); function Collector() {} diff --git a/tools/timesrc.js b/tools/timesrc.js index b94bf9d..55b17e4 100755 --- a/tools/timesrc.js +++ b/tools/timesrc.js @@ -5,7 +5,7 @@ console.log('Time adding "src" field with call source info:'); -var ben = require('ben'); // npm install ben +var ben = require('./ben'); // npm install ben var Logger = require('../lib/bunyan'); var records = [];