diff --git a/lib/bunyan.js b/lib/bunyan.js index aa09214..7401ad8 100644 --- a/lib/bunyan.js +++ b/lib/bunyan.js @@ -69,7 +69,7 @@ if (!format) { switch (x) { case '%s': return String(args[i++]); case '%d': return Number(args[i++]); - case '%j': return JSON.stringify(args[i++]); + case '%j': return JSON.stringify(args[i++], safeCycles()); case '%%': return '%'; default: return x; @@ -672,24 +672,7 @@ Logger.prototype._emit = function (rec) { // Stringify the object. Attempt to warn/recover on error. var str; if (this.haveNonRawStreams) { - try { - str = JSON.stringify(obj) + '\n'; - } catch (e) { - var src = ((obj.src && obj.src.file) ? obj.src : getCaller3Info()); - var emsg = format('bunyan: ERROR: could not stringify log record from ' - + '%s:%d: %s', src.file, src.line, e); - var eobj = objCopy(rec[0]); - eobj.bunyanMsg = emsg; - eobj.msg = obj.msg; - eobj.time = obj.time; - eobj.v = LOG_VERSION; - _warn(emsg, src.file, src.line); - try { - str = JSON.stringify(eobj) + '\n'; - } catch (e2) { - str = JSON.stringify({bunyanMsg: emsg}); - } - } + str = JSON.stringify(obj, safeCycles()) + '\n'; } for (i = 0; i < this.streams.length; i++) { @@ -1022,6 +1005,21 @@ var errSerializer = Logger.stdSerializers.err = function err(err) { return obj; }; +// A JSON stringifier that handles cycles safely. +// Usage: JSON.stringify(obj, safeCycles()) +function safeCycles() { + 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; + }; +} /** * RingBuffer is a Writable Stream that just stores the last N records in diff --git a/test/cycles.test.js b/test/cycles.test.js new file mode 100644 index 0000000..c845aae --- /dev/null +++ b/test/cycles.test.js @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012 Trent Mick. All rights reserved. + * + * Make sure cycles are safe. + */ + +var test = require('tap').test; +var Logger = require('../lib/bunyan.js'); + +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) { + t.has(o, 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.pass('did not throw'); +});