/**
 * Copyright 2020 Trent Mick.
 * Copyright 2020 Joyent Inc.
 *
 * The bunyan logging library for node.js.
 *
 * -*- mode: js -*-
 * vim: expandtab:ts=4:sw=4
 */

var VERSION = '1.8.14';

/*
 * 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
 *   <https://developer.mozilla.org/en-US/docs/Web/API/Window/window>.
 *      - 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(<single-string>)` 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, dtrace;
if (runtimeEnv === 'browser') {
    os = {
        hostname: function () {
            return window.location.host;
        }
    };
    fs = {};
    dtrace = null;
} else {
    os = require('os');
    fs = require('fs');
    try {
        dtrace = require('dtrace-provider' + '');
    } catch (e) {
        dtrace = null;
    }
}
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' + '');
} 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`:
    // <https://github.com/joyent/node/blob/master/lib/util.js#L22>:
    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 <http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi>.
 */
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;
});

// Dtrace probes.
var dtp = undefined;
var probes = dtrace && {};

/**
 * 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(<parent logger>, <child options>[, <child opts are simple>]);
 *
 * @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 = {};
    }

    if (!dtp && dtrace) {
        dtp = dtrace.createDTraceProvider('bunyan');

        for (var level in levelFromName) {
            var probe;

            probes[levelFromName[level]] = probe =
                dtp.addProbe('log-' + level, 'char *');

            // Explicitly add a reference to dtp to prevent it from being GC'd
            probe.dtp = dtp;
        }

        dtp.enable();
    }

    // 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 `<logger>.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 <https://github.com/trentm/node-bunyan/issues/104>.
 */
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) + os.EOL;
    }

    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.<level>(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.<level>(msg, ...)`
        fields = null;
        msgArgs = args.slice();
    } else if (Buffer.isBuffer(args[0])) {  // `log.<level>(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.<level>(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 an array that dtrace-provider can use to fire a USDT probe. If we've
 * already built the appropriate string, we use it. Otherwise, build the
 * record object and stringify it.
 */
function mkProbeArgs(str, log, minLevel, msgArgs) {
    return [ str || log._emit(mkRecord(log, minLevel, msgArgs), true) ];
}


/**
 * 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 <https://github.com/trentm/node-bunyan/issues/100> 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.<level>()`
            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);
        }

        if (probes) {
            probes[minLevel].fire(mkProbeArgs, str, log, minLevel, msgArgs);
        }
    }
}


/**
 * The functions below log a record at a specific level.
 *
 * Usages:
 *    log.<level>()  -> boolean is-trace-enabled
 *    log.<level>(<Error> err, [<string> msg, ...])
 *    log.<level>(<string> msg, ...)
 *    log.<level>(<object> fields, <string> msg, ...)
 *
 * where <level> 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) {
        // <number><scope> 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: <path to which to rotate>
    //      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
    // <https://github.com/joyent/node/issues/8656>.
    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 <https://github.com/joyent/node/issues/8656>.
    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 = 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;
