2d535a6395
Some Safari versions will error with a syntax error if a function has the same name as one of its parameter. Found the bug in safari included with iOS 9.3.5. Fixed for me with this patch.
1627 lines
48 KiB
JavaScript
1627 lines
48 KiB
JavaScript
/**
|
|
* Copyright (c) 2015 Trent Mick.
|
|
* Copyright (c) 2015 Joyent Inc.
|
|
*
|
|
* The bunyan logging library for node.js.
|
|
*
|
|
* -*- mode: js -*-
|
|
* vim: expandtab:ts=4:sw=4
|
|
*/
|
|
|
|
var VERSION = '1.8.8';
|
|
|
|
/*
|
|
* 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) + '\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.<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;
|