var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } __.prototype = b.prototype; d.prototype = new __(); }; function JL(loggerName) { // If name is empty, return the root logger if (!loggerName) { return JL.__; } // Implements Array.reduce. JSNLog supports IE8+ and reduce is not supported in that browser. // Same interface as the standard reduce, except that if (!Array.prototype.reduce) { Array.prototype.reduce = function (callback, initialValue) { var previousValue = initialValue; for (var i = 0; i < this.length; i++) { previousValue = callback(previousValue, this[i], i, this); } return previousValue; }; } var accumulatedLoggerName = ''; var logger = ('.' + loggerName).split('.').reduce(function (prev, curr, idx, arr) { // if loggername is a.b.c, than currentLogger will be set to the loggers // root (prev: JL, curr: '') // a (prev: JL.__, curr: 'a') // a.b (prev: JL.__.__a, curr: 'b') // a.b.c (prev: JL.__.__a.__a.b, curr: 'c') // Note that when a new logger name is encountered (such as 'a.b.c'), // a new logger object is created and added as a property to the parent ('a.b'). // The root logger is added as a property of the JL object itself. // It is essential that the name of the property containing the child logger // contains the full 'path' name of the child logger ('a.b.c') instead of // just the bit after the last period ('c'). // This is because the parent inherits properties from its ancestors. // So if the root has a child logger 'c' (stored in a property 'c' of the root logger), // then logger 'a.b' has that same property 'c' through inheritance. // The names of the logger properties start with __, so the root logger // (which has name ''), has a nice property name '__'. // accumulatedLoggerName evaluates false ('' is falsy) in first iteration when prev is the root logger. // accumulatedLoggerName will be the logger name corresponding with the logger in currentLogger. // Keep in mind that the currentLogger may not be defined yet, so can't get the name from // the currentLogger object itself. if (accumulatedLoggerName) { accumulatedLoggerName += '.' + curr; } else { accumulatedLoggerName = curr; } var currentLogger = prev['__' + accumulatedLoggerName]; // If the currentLogger (or the actual logger being sought) does not yet exist, // create it now. if (currentLogger === undefined) { // Set the prototype of the Logger constructor function to the parent of the logger // to be created. This way, __proto of the new logger object will point at the parent. // When logger.level is evaluated and is not present, the JavaScript runtime will // walk down the prototype chain to find the first ancestor with a level property. // // Note that prev at this point refers to the parent logger. JL.Logger.prototype = prev; currentLogger = new JL.Logger(accumulatedLoggerName); prev['__' + accumulatedLoggerName] = currentLogger; } return currentLogger; }, JL.__); return logger; } var JL; (function (JL) { JL.enabled; JL.maxMessages; JL.defaultAjaxUrl; JL.clientIP; JL.defaultBeforeSend; // Initialise requestId to empty string. If you don't do this and the user // does not set it via setOptions, then the JSNLog-RequestId header will // have value "undefined", which doesn't look good in a log. // // Note that you always want to send a requestId as part of log requests, // otherwise the server side component doesn't know this is a log request // and may create a new request id for the log request, causing confusion // in the log. JL.requestId = ''; /** Copies the value of a property from one object to the other. This is used to copy property values as part of setOption for loggers and appenders. Because loggers inherit property values from their parents, it is important never to create a property on a logger if the intent is to inherit from the parent. Copying rules: 1) if the from property is undefined (for example, not mentioned in a JSON object), the to property is not affected at all. 2) if the from property is null, the to property is deleted (so the logger will inherit from its parent). 3) Otherwise, the from property is copied to the to property. */ function copyProperty(propertyName, from, to) { if (from[propertyName] === undefined) { return; } if (from[propertyName] === null) { delete to[propertyName]; return; } to[propertyName] = from[propertyName]; } /** Returns true if a log should go ahead. Does not check level. @param filters Filters that determine whether a log can go ahead. */ function allow(filters) { // If enabled is not null or undefined, then if it is false, then return false // Note that undefined==null (!) if (!(JL.enabled == null)) { if (!JL.enabled) { return false; } } // If maxMessages is not null or undefined, then if it is 0, then return false. // Note that maxMessages contains number of messages that are still allowed to send. // It is decremented each time messages are sent. It can be negative when batch size > 1. // Note that undefined==null (!) if (!(JL.maxMessages == null)) { if (JL.maxMessages < 1) { return false; } } try { if (filters.userAgentRegex) { if (!new RegExp(filters.userAgentRegex).test(navigator.userAgent)) { return false; } } } catch (e) { } try { if (filters.ipRegex && JL.clientIP) { if (!new RegExp(filters.ipRegex).test(JL.clientIP)) { return false; } } } catch (e) { } return true; } /** Returns true if a log should go ahead, based on the message. @param filters Filters that determine whether a log can go ahead. @param message Message to be logged. */ function allowMessage(filters, message) { // If the regex contains a bug, that will throw an exception. // Ignore this, and pass the log item (better too much than too little). try { if (filters.disallow) { if (new RegExp(filters.disallow).test(message)) { return false; } } } catch (e) { } return true; } // If logObject is a function, the function is evaluated (without parameters) // and the result returned. // Otherwise, logObject itself is returned. function stringifyLogObjectFunction(logObject) { if (typeof logObject == "function") { if (logObject instanceof RegExp) { return logObject.toString(); } else { return logObject(); } } return logObject; } var StringifiedLogObject = (function () { // * msg - // if the logObject is a scalar (after possible function evaluation), this is set to // string representing the scalar. Otherwise it is left undefined. // * meta - // if the logObject is an object (after possible function evaluation), this is set to // that object. Otherwise it is left undefined. // * finalString - // This is set to the string representation of logObject (after possible function evaluation), // regardless of whether it is an scalar or an object. An object is stringified to a JSON string. // Note that you can't call this field "final", because as some point this was a reserved // JavaScript keyword and using final trips up some minifiers. function StringifiedLogObject(msg, meta, finalString) { this.msg = msg; this.meta = meta; this.finalString = finalString; } return StringifiedLogObject; })(); // Takes a logObject, which can be // * a scalar // * an object // * a parameterless function, which returns the scalar or object to log. // Returns a stringifiedLogObject function stringifyLogObject(logObject) { // Note that this works if logObject is null. // typeof null is object. // JSON.stringify(null) returns "null". var actualLogObject = stringifyLogObjectFunction(logObject); var finalString; switch (typeof actualLogObject) { case "string": return new StringifiedLogObject(actualLogObject, null, actualLogObject); case "number": finalString = actualLogObject.toString(); return new StringifiedLogObject(finalString, null, finalString); case "boolean": finalString = actualLogObject.toString(); return new StringifiedLogObject(finalString, null, finalString); case "undefined": return new StringifiedLogObject("undefined", null, "undefined"); case "object": if ((actualLogObject instanceof RegExp) || (actualLogObject instanceof String) || (actualLogObject instanceof Number) || (actualLogObject instanceof Boolean)) { finalString = actualLogObject.toString(); return new StringifiedLogObject(finalString, null, finalString); } else { return new StringifiedLogObject(null, actualLogObject, JSON.stringify(actualLogObject)); } default: return new StringifiedLogObject("unknown", null, "unknown"); } } function setOptions(options) { copyProperty("enabled", options, this); copyProperty("maxMessages", options, this); copyProperty("defaultAjaxUrl", options, this); copyProperty("clientIP", options, this); copyProperty("requestId", options, this); copyProperty("defaultBeforeSend", options, this); return this; } JL.setOptions = setOptions; function getAllLevel() { return -2147483648; } JL.getAllLevel = getAllLevel; function getTraceLevel() { return 1000; } JL.getTraceLevel = getTraceLevel; function getDebugLevel() { return 2000; } JL.getDebugLevel = getDebugLevel; function getInfoLevel() { return 3000; } JL.getInfoLevel = getInfoLevel; function getWarnLevel() { return 4000; } JL.getWarnLevel = getWarnLevel; function getErrorLevel() { return 5000; } JL.getErrorLevel = getErrorLevel; function getFatalLevel() { return 6000; } JL.getFatalLevel = getFatalLevel; function getOffLevel() { return 2147483647; } JL.getOffLevel = getOffLevel; function levelToString(level) { if (level <= 1000) { return "trace"; } if (level <= 2000) { return "debug"; } if (level <= 3000) { return "info"; } if (level <= 4000) { return "warn"; } if (level <= 5000) { return "error"; } return "fatal"; } // --------------------- var Exception = (function () { // data replaces message. It takes not just strings, but also objects and functions, just like the log function. // internally, the string representation is stored in the message property (inherited from Error) // // inner: inner exception. Can be null or undefined. function Exception(data, inner) { this.inner = inner; this.name = "JL.Exception"; this.message = stringifyLogObject(data).finalString; } return Exception; })(); JL.Exception = Exception; // Derive Exception from Error (a Host object), so browsers // are more likely to produce a stack trace for it in their console. // // Note that instanceof against an object created with this constructor // will return true in these cases: // instanceof JL.Exception); // instanceof Error); Exception.prototype = new Error(); // --------------------- var LogItem = (function () { // l: level // m: message // n: logger name // t (timeStamp) is number of milliseconds since 1 January 1970 00:00:00 UTC // // Keeping the property names really short, because they will be sent in the // JSON payload to the server. function LogItem(l, m, n, t) { this.l = l; this.m = m; this.n = n; this.t = t; } return LogItem; })(); JL.LogItem = LogItem; // --------------------- var Appender = (function () { // sendLogItems takes an array of log items. It will be called when // the appender has items to process (such as, send to the server). // Note that after sendLogItems returns, the appender may truncate // the LogItem array, so the function has to copy the content of the array // in some fashion (eg. serialize) before returning. function Appender(appenderName, sendLogItems) { this.appenderName = appenderName; this.sendLogItems = sendLogItems; this.level = JL.getTraceLevel(); // set to super high level, so if user increases level, level is unlikely to get // above sendWithBufferLevel this.sendWithBufferLevel = 2147483647; this.storeInBufferLevel = -2147483648; this.bufferSize = 0; // buffering switch off by default this.batchSize = 1; // Holds all log items with levels higher than storeInBufferLevel // but lower than level. These items may never be sent. this.buffer = []; // Holds all items that we do want to send, until we have a full // batch (as determined by batchSize). this.batchBuffer = []; } Appender.prototype.setOptions = function (options) { copyProperty("level", options, this); copyProperty("ipRegex", options, this); copyProperty("userAgentRegex", options, this); copyProperty("disallow", options, this); copyProperty("sendWithBufferLevel", options, this); copyProperty("storeInBufferLevel", options, this); copyProperty("bufferSize", options, this); copyProperty("batchSize", options, this); if (this.bufferSize < this.buffer.length) { this.buffer.length = this.bufferSize; } return this; }; /** Called by a logger to log a log item. If in response to this call one or more log items need to be processed (eg., sent to the server), this method calls this.sendLogItems with an array with all items to be processed. Note that the name and parameters of this function must match those of the log function of a Winston transport object, so that users can use these transports as appenders. That is why there are many parameters that are not actually used by this function. level - string with the level ("trace", "debug", etc.) Only used by Winston transports. msg - human readable message. Undefined if the log item is an object. Only used by Winston transports. meta - log object. Always defined, because at least it contains the logger name. Only used by Winston transports. callback - function that is called when the log item has been logged. Only used by Winston transports. levelNbr - level as a number. Not used by Winston transports. message - log item. If the user logged an object, this is the JSON string. Not used by Winston transports. loggerName: name of the logger. Not used by Winston transports. */ Appender.prototype.log = function (level, msg, meta, callback, levelNbr, message, loggerName) { var logItem; if (!allow(this)) { return; } if (!allowMessage(this, message)) { return; } if (levelNbr < this.storeInBufferLevel) { // Ignore the log item completely return; } logItem = new LogItem(levelNbr, message, loggerName, (new Date).getTime()); if (levelNbr < this.level) { // Store in the hold buffer. Do not send. if (this.bufferSize > 0) { this.buffer.push(logItem); // If we exceeded max buffer size, remove oldest item if (this.buffer.length > this.bufferSize) { this.buffer.shift(); } } return; } if (levelNbr < this.sendWithBufferLevel) { // Want to send the item, but not the contents of the buffer this.batchBuffer.push(logItem); } else { // Want to send both the item and the contents of the buffer. // Send contents of buffer first, because logically they happened first. if (this.buffer.length) { this.batchBuffer = this.batchBuffer.concat(this.buffer); this.buffer.length = 0; } this.batchBuffer.push(logItem); } if (this.batchBuffer.length >= this.batchSize) { this.sendBatch(); return; } }; // Processes the batch buffer Appender.prototype.sendBatch = function () { if (this.batchBuffer.length == 0) { return; } if (!(JL.maxMessages == null)) { if (JL.maxMessages < 1) { return; } } // If maxMessages is not null or undefined, then decrease it by the batch size. // This can result in a negative maxMessages. // Note that undefined==null (!) if (!(JL.maxMessages == null)) { JL.maxMessages -= this.batchBuffer.length; } this.sendLogItems(this.batchBuffer); this.batchBuffer.length = 0; }; return Appender; })(); JL.Appender = Appender; // --------------------- var AjaxAppender = (function (_super) { __extends(AjaxAppender, _super); function AjaxAppender(appenderName) { _super.call(this, appenderName, AjaxAppender.prototype.sendLogItemsAjax); } AjaxAppender.prototype.setOptions = function (options) { copyProperty("url", options, this); copyProperty("beforeSend", options, this); _super.prototype.setOptions.call(this, options); return this; }; AjaxAppender.prototype.sendLogItemsAjax = function (logItems) { try { // Only determine the url right before you send a log request. // Do not set the url when constructing the appender. // // This is because the server side component sets defaultAjaxUrl // in a call to setOptions, AFTER the JL object and the default appender // have been created. var ajaxUrl = "/jsnlog.logger"; // This evaluates to true if defaultAjaxUrl is null or undefined if (!(JL.defaultAjaxUrl == null)) { ajaxUrl = JL.defaultAjaxUrl; } if (this.url) { ajaxUrl = this.url; } var json = JSON.stringify({ r: JL.requestId, lg: logItems }); // Send the json to the server. // Note that there is no event handling here. If the send is not // successful, nothing can be done about it. var xhr = this.getXhr(ajaxUrl); // call beforeSend callback // first try the callback on the appender // then the global defaultBeforeSend callback if (typeof this.beforeSend === 'function') { this.beforeSend(xhr); } else if (typeof JL.defaultBeforeSend === 'function') { JL.defaultBeforeSend(xhr); } xhr.send(json); } catch (e) { } }; // Creates the Xhr object to use to send the log request. // Sets out to create an Xhr object that can be used for CORS. // However, if there seems to be no CORS support on the browser, // returns a non-CORS capable Xhr. AjaxAppender.prototype.getXhr = function (ajaxUrl) { var xhr = new XMLHttpRequest(); // Check whether this xhr is CORS capable by checking whether it has // withCredentials. // "withCredentials" only exists on XMLHTTPRequest2 objects. if (!("withCredentials" in xhr)) { // Just found that no XMLHttpRequest2 available. // Check if XDomainRequest is available. // This only exists in IE, and is IE's way of making CORS requests. if (typeof XDomainRequest != "undefined") { // Note that here we're not setting request headers on the XDomainRequest // object. This is because this object doesn't let you do that: // http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx // This means that for IE8 and IE9, CORS logging requests do not carry request ids. var xdr = new XDomainRequest(); xdr.open('POST', ajaxUrl); return xdr; } } // At this point, we're going with XMLHttpRequest, whether it is CORS capable or not. // If it is not CORS capable, at least will handle the non-CORS requests. xhr.open('POST', ajaxUrl); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('JSNLog-RequestId', JL.requestId); return xhr; }; return AjaxAppender; })(Appender); JL.AjaxAppender = AjaxAppender; // --------------------- var ConsoleAppender = (function (_super) { __extends(ConsoleAppender, _super); function ConsoleAppender(appenderName) { _super.call(this, appenderName, ConsoleAppender.prototype.sendLogItemsConsole); } ConsoleAppender.prototype.clog = function (logEntry) { console.log(logEntry); }; ConsoleAppender.prototype.cerror = function (logEntry) { if (console.error) { console.error(logEntry); } else { this.clog(logEntry); } }; ConsoleAppender.prototype.cwarn = function (logEntry) { if (console.warn) { console.warn(logEntry); } else { this.clog(logEntry); } }; ConsoleAppender.prototype.cinfo = function (logEntry) { if (console.info) { console.info(logEntry); } else { this.clog(logEntry); } }; // IE11 has a console.debug function. But its console doesn't have // the option to show/hide debug messages (the same way Chrome and FF do), // even though it does have such buttons for Error, Warn, Info. // // For now, this means that debug messages can not be hidden on IE. // Live with this, seeing that it works fine on FF and Chrome, which // will be much more popular with developers. ConsoleAppender.prototype.cdebug = function (logEntry) { if (console.debug) { console.debug(logEntry); } else { this.cinfo(logEntry); } }; ConsoleAppender.prototype.sendLogItemsConsole = function (logItems) { try { if (!console) { return; } var i; for (i = 0; i < logItems.length; ++i) { var li = logItems[i]; var msg = li.n + ": " + li.m; // Only log the timestamp if we're on the server // (window is undefined). On the browser, the user // sees the log entry probably immediately, so in that case // the timestamp is clutter. if (typeof window === 'undefined') { msg = new Date(li.t) + " | " + msg; } if (li.l <= JL.getDebugLevel()) { this.cdebug(msg); } else if (li.l <= JL.getInfoLevel()) { this.cinfo(msg); } else if (li.l <= JL.getWarnLevel()) { this.cwarn(msg); } else { this.cerror(msg); } } } catch (e) { } }; return ConsoleAppender; })(Appender); JL.ConsoleAppender = ConsoleAppender; // -------------------- var Logger = (function () { function Logger(loggerName) { this.loggerName = loggerName; // Create seenRexes, otherwise this logger will use the seenRexes // of its parent via the prototype chain. this.seenRegexes = []; } Logger.prototype.setOptions = function (options) { copyProperty("level", options, this); copyProperty("userAgentRegex", options, this); copyProperty("disallow", options, this); copyProperty("ipRegex", options, this); copyProperty("appenders", options, this); copyProperty("onceOnly", options, this); // Reset seenRegexes, in case onceOnly has been changed. this.seenRegexes = []; return this; }; // Turns an exception into an object that can be sent to the server. Logger.prototype.buildExceptionObject = function (e) { var excObject = {}; if (e.stack) { excObject.stack = e.stack; } else { excObject.e = e; } if (e.message) { excObject.message = e.message; } if (e.name) { excObject.name = e.name; } if (e.data) { excObject.data = e.data; } if (e.inner) { excObject.inner = this.buildExceptionObject(e.inner); } return excObject; }; // Logs a log item. // Parameter e contains an exception (or null or undefined). // // Reason that processing exceptions is done at this low level is that // 1) no need to spend the cpu cycles if the logger is switched off // 2) fatalException takes both a logObject and an exception, and the logObject // may be a function that should only be executed if the logger is switched on. // // If an exception is passed in, the contents of logObject is attached to the exception // object in a new property logData. // The resulting exception object is than worked into a message to the server. // // If there is no exception, logObject itself is worked into the message to the server. Logger.prototype.log = function (level, logObject, e) { var i = 0; var compositeMessage; var excObject; // If we can't find any appenders, do nothing if (!this.appenders) { return this; } if (((level >= this.level)) && allow(this)) { if (e) { excObject = this.buildExceptionObject(e); excObject.logData = stringifyLogObjectFunction(logObject); } else { excObject = logObject; } compositeMessage = stringifyLogObject(excObject); if (allowMessage(this, compositeMessage.finalString)) { // See whether message is a duplicate if (this.onceOnly) { i = this.onceOnly.length - 1; while (i >= 0) { if (new RegExp(this.onceOnly[i]).test(compositeMessage.finalString)) { if (this.seenRegexes[i]) { return this; } this.seenRegexes[i] = true; } i--; } } // Pass message to all appenders // Note that these appenders could be Winston transports // https://github.com/flatiron/winston // // These transports do not take the logger name as a parameter. // So add it to the meta information, so even Winston transports will // store this info. compositeMessage.meta = compositeMessage.meta || {}; compositeMessage.meta.loggerName = this.loggerName; i = this.appenders.length - 1; while (i >= 0) { this.appenders[i].log(levelToString(level), compositeMessage.msg, compositeMessage.meta, function () { }, level, compositeMessage.finalString, this.loggerName); i--; } } } return this; }; Logger.prototype.trace = function (logObject) { return this.log(getTraceLevel(), logObject); }; Logger.prototype.debug = function (logObject) { return this.log(getDebugLevel(), logObject); }; Logger.prototype.info = function (logObject) { return this.log(getInfoLevel(), logObject); }; Logger.prototype.warn = function (logObject) { return this.log(getWarnLevel(), logObject); }; Logger.prototype.error = function (logObject) { return this.log(getErrorLevel(), logObject); }; Logger.prototype.fatal = function (logObject) { return this.log(getFatalLevel(), logObject); }; Logger.prototype.fatalException = function (logObject, e) { return this.log(getFatalLevel(), logObject, e); }; return Logger; })(); JL.Logger = Logger; function createAjaxAppender(appenderName) { return new AjaxAppender(appenderName); } JL.createAjaxAppender = createAjaxAppender; function createConsoleAppender(appenderName) { return new ConsoleAppender(appenderName); } JL.createConsoleAppender = createConsoleAppender; // ----------------------- // In the browser, the default appender is the AjaxAppender. // Under nodejs (where there is no "window"), use the ConsoleAppender instead. var defaultAppender = new AjaxAppender(""); if (typeof window === 'undefined') { defaultAppender = new ConsoleAppender(""); } // Create root logger // // Note that this is the parent of all other loggers. // Logger "x" will be stored at // JL.__.x // Logger "x.y" at // JL.__.x.y JL.__ = new JL.Logger(""); JL.__.setOptions({ level: JL.getDebugLevel(), appenders: [defaultAppender] }); })(JL || (JL = {})); // Support CommonJS module format var exports; if (typeof exports !== 'undefined') { exports.JL = JL; } // Support AMD module format var define; if (typeof define == 'function' && define.amd) { define('jsnlog', [], function () { return JL; }); } // If the __jsnlog_configure global function has been // created, call it now. This allows you to create a global function // setting logger options etc. inline in the page before jsnlog.js // has been loaded. if (typeof __jsnlog_configure == 'function') { __jsnlog_configure(JL); }