Mini Kabibi Habibi
/*
* Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
(function () {
"use strict";
var util = require("util"),
stream = require("stream"),
EventEmitter = require("events").EventEmitter;
Object.defineProperties(exports, {
"LOG_LEVEL_NONE" : {
value : 1,
writable : false,
enumerable : true
},
"LOG_LEVEL_ERROR" : {
value : 2,
writable : false,
enumerable : true
},
"LOG_LEVEL_WARNING" : {
value : 3,
writable : false,
enumerable : true
},
"LOG_LEVEL_INFO" : {
value : 4,
writable : false,
enumerable : true
},
"LOG_LEVEL_DEBUG" : {
value : 5,
writable : false,
enumerable : true
}
});
function getCallLocationFromStackString(stackString, entry) {
// A stack string looks like this:
//
// Error
// at repl:1:15
// at REPLServer.self.eval (repl.js:110:21)
// at Interface.<anonymous> (repl.js:239:12)
// at Interface.EventEmitter.emit (events.js:95:17)
// at Interface._onLine (readline.js:202:10)
var longLocation = "",
shortLocation = "",
line = stackString.split("\n")[entry + 1];
if (line) {
var i = line.indexOf("at ");
if (i >= 0) { // have an actual location
longLocation = line.substr(i + 3); // "at " is 3 chars long
// longLocation will look like one of the following:
//
// "repl:1:6"
// or
// "EventEmitter.error (/some/place/on/disk/this.js:153:13)"
//
// If there is something in parens, we want the stuff after the last / (or if there isn't
// a slash, then just everything in the parens). If there are no parens, we just want
// the whole thing
shortLocation = longLocation;
var parenLocation = shortLocation.indexOf("(");
if (parenLocation >= 0) {
shortLocation = shortLocation.substr(parenLocation + 1, shortLocation.length - parenLocation - 2);
}
var slashLocation = shortLocation.lastIndexOf("/");
if (slashLocation >= 0) {
shortLocation = shortLocation.substr(slashLocation + 1);
}
}
}
return {long: longLocation, short: shortLocation};
}
function makeLogMethod(level, stackEntry) {
return function () { // (message, arg1, ...)
var message = arguments[0] || "",
args = Array.prototype.slice.call(arguments, 1),
callLocation = null,
time = null,
entry = stackEntry === undefined ? 1 : stackEntry;
if (!this.manager.ended &&
this.manager.level >= level &&
EventEmitter.listenerCount(this.manager, "message") > 0) {
callLocation = getCallLocationFromStackString((new Error()).stack, entry);
time = new Date();
this.manager.emit("message", {
source : this.source,
level : level,
time : time,
callLocation : callLocation,
message : message,
args : args
});
}
};
}
function makeTimerStartMethod() {
return function (timerName) {
if (!timerName) {
return;
}
var startDate = new Date();
this._timerData[timerName] = startDate;
this._timeLog(timerName + ": timer started");
};
}
function makeTimerEndMethod() {
return function (timerName) {
if (!timerName || !this._timerData[timerName]) {
return;
}
var startDate = this._timerData[timerName],
endDate = new Date(),
duration = endDate.getTime() - startDate.getTime();
this._timeLog(timerName + ": " + duration + "ms");
this._timerData[timerName] = null;
};
}
/**
* Logger objects are used to create log events. The constructor for Logger objects is
* private (not accessible outside this module). Instead, they are created through the
* LoggerManager.prototype.createLogger method.
*/
function Logger(manager, source) {
Object.defineProperties(this, {
"manager" : {
value : manager,
writable : false,
enumerable : true
},
"source" : {
value : source,
writable : false,
enumerable : true
},
"_timerData" : {
value : {},
writable : false,
enumerable : false
}
});
}
/**
* Variadic logging methods. The first argument is the log message, and any additional
* arguments are passed along in the "args" array in the log event.
*/
Logger.prototype.debug = makeLogMethod(exports.LOG_LEVEL_DEBUG);
Logger.prototype.info = makeLogMethod(exports.LOG_LEVEL_INFO);
Logger.prototype.warn = makeLogMethod(exports.LOG_LEVEL_WARNING);
Logger.prototype.error = makeLogMethod(exports.LOG_LEVEL_ERROR);
Logger.prototype.warning = Logger.prototype.warn;
Logger.prototype.log = Logger.prototype.info;
Logger.prototype.time = makeTimerStartMethod();
Logger.prototype.timeEnd = makeTimerEndMethod();
Logger.prototype._timeLog = makeLogMethod(exports.LOG_LEVEL_INFO, 2);
/**
* LoggerManager objects are EventEmitters that emit two events: "message" and "end"
*
* "message" events have a single object as their data. That object has the following properties:
* source -- the ID of the plugin (or "core") from which the log event originated.
* level -- an integer corresponding to the log level constants defined on this module,
* time -- a Date object representing the time of the log event,
* callLocation -- an object containing "long" and "short" properties that contains long and short string
* representations of the location in the source where the log event was generated
* message -- the actual log message, possibly containing formatting caracters (e.g. "%d") for a
* string formatting function to use (he Logger itself doesn't do any string formatting).
* args -- an array of optional args, possibly used by a string format function
*
* "end" events do not have any data. An "end" event will be emitted at most once, in response
* to a call to Logger.prototype.end. No "message" events will be emitted after an "end" event.
*
* Log level can be set by assigning an int (corresponding to the log level constants defined in
* this module) to the property "level".
*
* Log entries are initiated through Logger objects, which can be created by calling the
* "createLogger" method on a LoggerManager object. Then, users can call the
* Logger.prototype.error/warn/info/debug/warning/log functions, which are variadic. The first
* argument is the log message, and any additional arguments are passed along in the "args" array.
*/
function LoggerManager(level) {
if (!(this instanceof LoggerManager)) {
return new LoggerManager(level);
}
EventEmitter.call(this);
// set the internal level variable to default
var _logLevel = exports.LOG_LEVEL_WARNING;
// define getter/setter for log level
Object.defineProperty(this, "level", {
enumerable: true,
get: function () {
return _logLevel;
},
set: function (val) {
var newLevel = parseInt(val, 10); // coerce to int
if (newLevel >= exports.LOG_LEVEL_NONE && newLevel <= exports.LOG_LEVEL_DEBUG) {
_logLevel = newLevel;
}
}
});
// use setter to (possibly) set the log level to caller's value
this.level = level;
var _ended = false;
Object.defineProperty(this, "ended", {
enumerable: true,
get: function () {
return _ended;
}
});
this.end = function () {
if (!_ended) {
_ended = true;
this.emit("end");
}
};
}
util.inherits(LoggerManager, EventEmitter);
/**
* Creates a new Logger object. All events coming from the returned Logger object
* will have a source as specified in the "source" parameter. All log events
* created by the returned Logger will be emitted by this LoggerManager.
*/
LoggerManager.prototype.createLogger = function (source) {
return new Logger(this, source);
};
function levelToString(level) {
switch (level) {
case exports.LOG_LEVEL_ERROR:
return "error";
case exports.LOG_LEVEL_WARNING:
return "warning";
case exports.LOG_LEVEL_INFO:
return "info";
case exports.LOG_LEVEL_DEBUG:
return "debug";
default:
return "";
}
}
function dateToMilliTimeString(date) {
function padString(s, places) {
var i = places - s.length;
while (i > 0) {
s = "0" + s;
i--;
}
return s;
}
return util.format("%s:%s:%s.%s",
padString(String(date.getHours()), 2),
padString(String(date.getMinutes()), 2),
padString(String(date.getSeconds()), 2),
padString(String(date.getMilliseconds()), 3)
);
}
/**
* StreamFormatter objects are Readable streams. They output a string represntation
* of the log events generated by the "logger" variable.
*
* The "options" argument is passed directly to the stream.Readable constructor.
* No configuration of the log format is supported at this time.
*/
function StreamFormatter(loggerManager, options) {
if (!(this instanceof StreamFormatter)) {
return new StreamFormatter(loggerManager, options);
}
stream.Readable.call(this, options);
this._buffer = [];
this._pushable = false;
this._ended = false;
loggerManager.on("message", this._handleMessage.bind(this));
loggerManager.on("end", function () { this._handleMessage("END"); }.bind(this));
}
util.inherits(StreamFormatter, stream.Readable);
StreamFormatter.prototype._doRead = function () {
var entry = null,
entryString = null,
messageArgs = [];
while (this._pushable && this._buffer.length > 0) {
entry = this._buffer.shift();
if (entry === "END") {
this.push(null); // EOF
this._pushable = false;
this._ended = true;
} else {
entryString = util.format("[%s:%s %s %s] ",
levelToString(entry.level),
entry.source,
dateToMilliTimeString(entry.time),
entry.callLocation.short
);
messageArgs = entry.args.concat(); // copy args array
messageArgs.unshift(entry.message); // add format string to beginning
entryString += util.format.apply(null, messageArgs);
entryString += "\n";
this._pushable = this.push(entryString, "utf8");
}
}
};
StreamFormatter.prototype._handleMessage = function (entry) {
if (!this._ended) {
this._buffer.push(entry);
this._doRead();
}
};
StreamFormatter.prototype._read = function () { // (size)
this._pushable = true;
this._doRead();
};
exports.LoggerManager = LoggerManager;
exports.createLoggerManager = function (level) { return new LoggerManager(level); };
exports.levelToString = levelToString;
exports.dateToMilliTimeString = dateToMilliTimeString;
exports.StreamFormatter = StreamFormatter;
exports.createStreamFormatter = function (loggerManager, options) {
return new StreamFormatter(loggerManager, options);
};
}());