Skip to content

Commit

Permalink
Add stderrLevels option to Console Transport and update docs
Browse files Browse the repository at this point in the history
- Let users choose which levels to log to stderr instead of stdout
- Maintain backwards compatibility with previous options/behavior
  • Loading branch information
paulhroth committed Jul 8, 2015
1 parent 4dd10f5 commit 83188c6
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 13 deletions.
3 changes: 2 additions & 1 deletion docs/transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ The Console transport takes a few simple options:
* __humanReadableUnhandledException__ Boolean flag indicating if uncaught exception should be output as human readable, instead of a single line
* __showLevel:__ Boolean flag indicating if we should prepend output with level (default true).
* __formatter:__ If function is specified, its return value will be used instead of default output. (default undefined)
* __debugStdout:__ Boolean flag indicating if 'debug'-level output should be redirected to stdout instead of to stderr. (default false)
* __stderrLevels__ Array of strings containing the levels to log to stderr instead of stdout, for example `['error', 'debug', 'info']`. (default `['error', 'debug']`)
* (Deprecated: Use __stderrLevels__ instead) __debugStdout:__ Boolean flag indicating if 'debug'-level output should be redirected to stdout instead of to stderr. Cannot be used with __stderrLevels__. (default false)

*Metadata:* Logged via util.inspect(meta);

Expand Down
19 changes: 19 additions & 0 deletions lib/winston/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,22 @@ exports.tailFile = function(options, callback) {

return stream.destroy;
};

//
// ### function stringArrayToSet (array)
// #### @strArray {Array} Array of Set-elements as strings.
// #### @errMsg {string} **Optional** Custom error message thrown on invalid input.
// Returns a Set-like object with strArray's elements as keys (each with the value true).
//
exports.stringArrayToSet = function (strArray, errMsg) {
if (typeof errMsg === 'undefined') {
errMsg = 'Cannot make set from Array with non-string elements';
}
return strArray.reduce(function (set, el) {
if (!(typeof el === 'string' || el instanceof String)) {
throw new Error(errMsg);
}
set[el] = true;
return set;
}, Object.create(null));
};
47 changes: 37 additions & 10 deletions lib/winston/transports/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,48 @@ var Console = exports.Console = function (options) {
Transport.call(this, options);
options = options || {};

this.json = options.json || false;
this.colorize = options.colorize || false;
this.prettyPrint = options.prettyPrint || false;
this.timestamp = typeof options.timestamp !== 'undefined' ? options.timestamp : false;
this.showLevel = options.showLevel === undefined ? true : options.showLevel;
this.label = options.label || null;
this.logstash = options.logstash || false;
this.debugStdout = options.debugStdout || false;
this.depth = options.depth || null;
this.json = options.json || false;
this.colorize = options.colorize || false;
this.prettyPrint = options.prettyPrint || false;
this.timestamp = typeof options.timestamp !== 'undefined' ? options.timestamp : false;
this.showLevel = options.showLevel === undefined ? true : options.showLevel;
this.label = options.label || null;
this.logstash = options.logstash || false;
this.depth = options.depth || null;
this.stderrLevels = setStderrLevels(options.stderrLevels, options.debugStdout);

if (this.json) {
this.stringify = options.stringify || function (obj) {
return JSON.stringify(obj, null, 2);
};
}

//
// Convert stderrLevels into an Object for faster key-lookup times than an Array.
//
// For backwards compatibility, stderrLevels defaults to ['error', 'debug']
// or ['error'] depending on whether options.debugStdout is true.
//
function setStderrLevels (levels, debugStdout) {
var toSetErrMsg = 'Cannot have non-string elements in stderrLevels Array';
if (debugStdout) {
if (levels) {
//
// Don't allow setting both debugStdout and stderrLevels together,
// since this could cause behaviour a programmer might not expect.
//
throw new Error('Cannot set debugStdout and stderrLevels together');
}
return common.stringArrayToSet(['error'], toSetErrMsg);
}
if (!levels) {
return common.stringArrayToSet(['error', 'debug'], toSetErrMsg);
}
if (!(Array.isArray(levels))) {
throw new Error('Cannot set stderrLevels to type other than Array');
}
return common.stringArrayToSet(levels, toSetErrMsg);
};
};

//
Expand Down Expand Up @@ -82,7 +109,7 @@ Console.prototype.log = function (level, msg, meta, callback) {
humanReadableUnhandledException: this.humanReadableUnhandledException
});

if (level === 'error' || (level === 'debug' && !this.debugStdout) ) {
if (this.stderrLevels[level]) {
process.stderr.write(output + '\n');
} else {
process.stdout.write(output + '\n');
Expand Down
58 changes: 58 additions & 0 deletions test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,61 @@ helpers.testLevels = function (levels, transport, assertMsg, assertFn) {

return tests;
};

helpers.assertOptionsThrow = function (options, errMsg) {
return function () {
assert.throws(
function () {
try {
new (winston.transports.Console)(options);
} catch (err) {
throw(err);
}
},
new RegExp('^' + errMsg.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$')
);
}
};

helpers.assertStderrLevels = function (transport, stderrLevels) {
return function () {
assert.equal(
JSON.stringify(Object.keys(transport.stderrLevels).sort()),
JSON.stringify(stderrLevels.sort())
);
}
};

helpers.testLoggingToStreams = function (levels, transport, stderrLevels, stdMocks) {
return {
topic: function () {
stdMocks.use();
transport.showLevel = true;
Object.keys(levels).forEach(function (level) {
transport.log(
level,
level + ' should go to ' + (stderrLevels.indexOf(level) > -1 ? 'stderr' : 'stdout'),
{},
function () {}
);
});
var output = stdMocks.flush();
stdMocks.restore();
this.callback(null, output, levels);
},
"output should go to the appropriate streams": function (ign, output, levels) {
var outCount = 0,
errCount = 0;
Object.keys(levels).forEach(function (level) {
var line;
if (stderrLevels.indexOf(level) > -1) {
line = output.stderr[errCount++];
assert.equal(line, level + ': ' + level + ' should go to stderr\n');
} else {
line = output.stdout[outCount++];
assert.equal(line, level + ': ' + level + ' should go to stdout\n');
}
});
}
}
};
73 changes: 71 additions & 2 deletions test/transports/console-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,22 @@ var path = require('path'),
stdMocks = require('std-mocks');

var npmTransport = new (winston.transports.Console)(),
syslogTransport = new (winston.transports.Console)({ levels: winston.config.syslog.levels });
syslogTransport = new (winston.transports.Console)({ levels: winston.config.syslog.levels }),
defaultTransport = new (winston.transports.Console)(),
debugStdoutTransport = new (winston.transports.Console)({ debugStdout: true }),
stderrLevelsTransport = new (winston.transports.Console)({ stderrLevels: ['info', 'warn'] }),
customLevels = {
alpha: 0,
beta: 1,
gamma: 2,
delta: 3,
epsilon: 4,
},
customLevelsAndStderrTransport = new (winston.transports.Console)({
levels: customLevels,
stderrLevels: ['delta', 'epsilon']
}),
noStderrTransport = new (winston.transports.Console)({ stderrLevels: [] });

vows.describe('winston/transports/console').addBatch({
"An instance of the Console Transport": {
Expand Down Expand Up @@ -65,4 +80,58 @@ vows.describe('winston/transports/console').addBatch({
})
}
}
}).export(module);
}).addBatch({
"An instance of the Console Transport with no options": {
"should set stderrLevels to 'error' and 'debug' by default": helpers.assertStderrLevels(
defaultTransport,
['error', 'debug']
),
"should log only 'error' and 'debug' to stderr": helpers.testLoggingToStreams(
winston.config.npm.levels, defaultTransport, ['debug', 'error'], stdMocks
)
}
}).addBatch({
"An instance of the Console Transport with debugStdout set": {
"should throw an Error if stderrLevels is set": helpers.assertOptionsThrow(
{ debugStdout: true, stderrLevels: ['debug'] },
"Error: Cannot set debugStdout and stderrLevels together"
),
"should set stderrLevels to 'error' by default": helpers.assertStderrLevels(
debugStdoutTransport,
['error']
),
"should log only the 'error' level to stderr": helpers.testLoggingToStreams(
winston.config.npm.levels, debugStdoutTransport, ['error'], stdMocks
)
}
}).addBatch({
"An instance of the Console Transport with stderrLevels set": {
"should throw an Error if stderrLevels is set but not an Array": helpers.assertOptionsThrow(
{ debugStdout: false, stderrLevels: new String('Not an Array') },
"Error: Cannot set stderrLevels to type other than Array"
),
"should throw an Error if stderrLevels contains non-string elements": helpers.assertOptionsThrow(
{ debugStdout: false, stderrLevels: ["good", /^invalid$/, "valid"] },
"Error: Cannot have non-string elements in stderrLevels Array"
),
"should correctly set stderrLevels": helpers.assertStderrLevels(
stderrLevelsTransport,
['info', 'warn']
),
"should log only the levels in stderrLevels to stderr": helpers.testLoggingToStreams(
winston.config.npm.levels, stderrLevelsTransport, ['info', 'warn'], stdMocks
)
}
}).addBatch({
"An instance of the Console Transport with stderrLevels set to an empty array": {
"should log only to stdout, and not to stderr": helpers.testLoggingToStreams(
winston.config.npm.levels, noStderrTransport, [], stdMocks
)
}
}).addBatch({
"An instance of the Console Transport with custom levels and stderrLevels set": {
"should log only the levels in stderrLevels to stderr": helpers.testLoggingToStreams(
customLevels, customLevelsAndStderrTransport, ['delta', 'epsilon'], stdMocks
)
}
}).export(module);

0 comments on commit 83188c6

Please sign in to comment.