src/misc/bunyan.js
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 (!moment && opts.timeFormat === TIME_UTC) {
            // Don't require momentjs install, as long as not using TIME_LOCAL.
            time = rec.time.substr(11);
        } else {
            var tzFormat;
            var moTime = moment(rec.time);
            switch (opts.timeFormat) {
            case TIME_UTC:
                tzFormat = TIMEZONE_UTC_FORMATS[short ? 'short' : 'long'];
                moTime.utc();
                break;
            case TIME_LOCAL:
                tzFormat = TIMEZONE_LOCAL_FORMATS[short ? 'short' : 'long'];
                break;
            default:
                throw new Error('unexpected timeFormat: ' + opts.timeFormat);
            };
            time = moTime.format(tzFormat);
        }
        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];
            })
        }

        /*
         * `client_req` is the typical field name for an *HTTP client request
         * object* serialized by the restify-clients library. Render the
         * client request somewhat like `curl` debug output shows it.
         */
        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;

            /*
             * `client_req.address`, and `client_req.port`, provide values for
             * a *likely* "Host" header that wasn't included in the serialized
             * headers. Node.js will often add this "Host" header in its
             * `http.ClientRequest`, e.g. for node v6.10.3:
             *      // JSSTYLED
             *      https://github.com/nodejs/node/blob/v6.10.3/lib/_http_client.js#L88-L105
             *
             * If `client_req.port` exists and is 80 or 443, we *assume* that
             * is the default protocol port, and elide it per the `defaultPort`
             * handling in the node.js link above.
             *
             * Note: This added Host header is a *guess*. Bunyan shouldn't be
             * doing this "favour" for users because it can be wrong and
             * misleading. Bunyan 2.x will drop adding this. See issue #504
             * for details.
             */
            var hostHeaderLine = '';
            if (!headers || !(
                    Object.hasOwnProperty.call(headers, 'host') ||
                    Object.hasOwnProperty.call(headers, 'Host') ||
                    Object.hasOwnProperty.call(headers, 'HOST')
                )
            ) {
                if (Object.hasOwnProperty.call(client_req, 'address')) {
                    hostHeaderLine = '\nHost: ' + client_req.address;
                    if (Object.hasOwnProperty.call(client_req, 'port')) {
                        // XXX
                        var port = +client_req.port;
                        if (port !== 80 && port !== 443) {
                            hostHeaderLine += ':' + client_req.port;
                        }
                        delete client_req.port;
                    }
                    delete client_req.address;
                }
            }

            var s = format('%s %s HTTP/%s%s%s', client_req.method,
                client_req.url,
                client_req.httpVersion || '1.1',
                hostHeaderLine,
                (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 || '<no-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 */
        // <http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/SimpleLayout.html>
        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 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 (!moment && opts.timeFormat === TIME_UTC) {
            // Don't require momentjs install, as long as not using TIME_LOCAL.
            time = rec.time.substr(11);
        } else {
            var tzFormat;
            var moTime = moment(rec.time);
            switch (opts.timeFormat) {
            case TIME_UTC:
                tzFormat = TIMEZONE_UTC_FORMATS[short ? 'short' : 'long'];
                moTime.utc();
                break;
            case TIME_LOCAL:
                tzFormat = TIMEZONE_LOCAL_FORMATS[short ? 'short' : 'long'];
                break;
            default:
                throw new Error('unexpected timeFormat: ' + opts.timeFormat);
            };
            time = moTime.format(tzFormat);
        }
        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];
            })
        }

        /*
         * `client_req` is the typical field name for an *HTTP client request
         * object* serialized by the restify-clients library. Render the
         * client request somewhat like `curl` debug output shows it.
         */
        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;

            /*
             * `client_req.address`, and `client_req.port`, provide values for
             * a *likely* "Host" header that wasn't included in the serialized
             * headers. Node.js will often add this "Host" header in its
             * `http.ClientRequest`, e.g. for node v6.10.3:
             *      // JSSTYLED
             *      https://github.com/nodejs/node/blob/v6.10.3/lib/_http_client.js#L88-L105
             *
             * If `client_req.port` exists and is 80 or 443, we *assume* that
             * is the default protocol port, and elide it per the `defaultPort`
             * handling in the node.js link above.
             *
             * Note: This added Host header is a *guess*. Bunyan shouldn't be
             * doing this "favour" for users because it can be wrong and
             * misleading. Bunyan 2.x will drop adding this. See issue #504
             * for details.
             */
            var hostHeaderLine = '';
            if (!headers || !(
                    Object.hasOwnProperty.call(headers, 'host') ||
                    Object.hasOwnProperty.call(headers, 'Host') ||
                    Object.hasOwnProperty.call(headers, 'HOST')
                )
            ) {
                if (Object.hasOwnProperty.call(client_req, 'address')) {
                    hostHeaderLine = '\nHost: ' + client_req.address;
                    if (Object.hasOwnProperty.call(client_req, 'port')) {
                        // XXX
                        var port = +client_req.port;
                        if (port !== 80 && port !== 443) {
                            hostHeaderLine += ':' + client_req.port;
                        }
                        delete client_req.port;
                    }
                    delete client_req.address;
                }
            }

            var s = format('%s %s HTTP/%s%s%s', client_req.method,
                client_req.url,
                client_req.httpVersion || '1.1',
                hostHeaderLine,
                (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 || '<no-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 */
        // <http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/SimpleLayout.html>
        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);
    }
}
Print out a single result, considering input options.