diff --git a/README.md b/README.md index 19955f5..8af297b 100644 --- a/README.md +++ b/README.md @@ -154,27 +154,27 @@ Be aware that all this means that these method won't necessarily always produce #### `log.setLevel(level, [persist])` -This disables all logging below the given level, so that after a log.setLevel("warn") call log.warn("something") or log.error("something") will output messages, but log.info("something") will not. +This disables all logging below the given level, so that after a `log.setLevel("warn")` call `log.warn("something")` or `log.error("something")` will output messages, but `log.info("something")` will not. -This can take either a log level name or 'silent' (which disables everything) in one of a few forms: +This can take either a log level name or `'silent'` (which disables everything) in one of a few forms: -* As a log level from the internal levels list, e.g. log.levels.SILENT ← _for type safety_ -* As a string, like 'error' (case-insensitive) ← _for a reasonable practical balance_ -* As a numeric index from 0 (trace) to 5 (silent) ← _deliciously terse, and more easily programmable (...although, why?)_ +* As a log level from the internal levels list, e.g. `log.levels.SILENT` ← _for type safety_ +* As a string, like `'error'` (case-insensitive) ← _for a reasonable practical balance_ +* As a numeric index from `0` (trace) to `5` (silent) ← _deliciously terse, and more easily programmable (...although, why?)_ -Where possible the log level will be persisted. LocalStorage will be used if available, falling back to cookies if not. If neither is available in the current environment (i.e. in Node), or if you pass `false` as the optional 'persist' second argument, persistence will be skipped. +Where possible, the log level will be persisted. LocalStorage will be used if available, falling back to cookies if not. If neither is available in the current environment (i.e. in Node), or if you pass `false` as the optional 'persist' second argument, persistence will be skipped. -If log.setLevel() is called when a console object is not available (in IE 8 or 9 before the developer tools have been opened, for example) logging will remain silent until the console becomes available, and then begin logging at the requested level. +If `log.setLevel()` is called when a console object is not available (in IE 8 or 9 before the developer tools have been opened, for example) logging will remain silent until the console becomes available, and then begin logging at the requested level. #### `log.setDefaultLevel(level)` -This sets the current log level only if one has not been persisted and can’t be loaded. This is useful when initializing scripts; if a developer or user has previously called `setLevel()`, this won’t alter their settings. For example, your application might set the log level to `error` in a production environment, but when debugging an issue, you might call `setLevel("trace")` on the console to see all the logs. If that `error` setting was set using `setDefaultLevel()`, it will still stay as `trace` on subsequent page loads and refreshes instead of resetting to `error`. +This sets the current log level only if one has not been persisted and can’t be loaded. This is useful when initializing modules or scripts; if a developer or user has previously called `setLevel()`, this won’t alter their settings. For example, your application might set the log level to `error` in a production environment, but when debugging an issue, you might call `setLevel("trace")` on the console to see all the logs. If that `error` setting was set using `setDefaultLevel()`, it will still stay as `trace` on subsequent page loads and refreshes instead of resetting to `error`. The `level` argument takes is the same values that you might pass to `setLevel()`. Levels set using `setDefaultLevel()` never persist to subsequent page loads. #### `log.resetLevel()` -This resets the current log level to the default level (or `warn` if no explicit default was set) and clears the persisted level if one was previously persisted. +This resets the current log level to the logger's default level (if no explicit default was set, then it resets it to the root logger's level, or to `WARN`) and clears the persisted level if one was previously persisted. #### `log.enableAll()` and `log.disableAll()` @@ -245,6 +245,34 @@ Likewise, loggers will inherit the root logger’s `methodFactory`. After creati This will return you the dictionary of all loggers created with `getLogger`, keyed off of their names. +#### `log.rebuild()` + +Ensure the various logging methods (`log.info()`, `log.warn()`, etc.) behave as expected given the currently set logging level and `methodFactory`. It will also rebuild all child loggers of the logger this was called on. + +This is mostly useful for plugin development. When you call `log.setLevel()` or `log.setDefaultLevel()`, the logger is rebuilt automatically. However, if you change the logger’s `methodFactory`, you should use this to rebuild all the logging methods with your new factory. + +It is also useful if you change the level of the root logger and want it to affect child loggers that you’ve already created (and have not called `someChildLogger.setLevel()` or `someChildLogger.setDefaultLevel()` on). For example: + +```js +var childLogger1 = log.getLogger("child1"); +childLogger1.getLevel(); // WARN (inherited from the root logger) + +var childLogger2 = log.getLogger("child2"); +childLogger2.setDefaultLevel("TRACE"); +childLogger2.getLevel(); // TRACE + +log.setLevel("ERROR"); + +// At this point, the child loggers have not changed: +childLogger1.getLevel(); // WARN +childLogger2.getLevel(); // TRACE + +// To update them: +log.rebuild(); +childLogger1.getLevel(); // ERROR (still inheriting from root logger) +childLogger2.getLevel(); // TRACE (no longer inheriting because `.setDefaultLevel() was called`) +``` + ## Plugins ### Existing plugins @@ -276,7 +304,7 @@ log.methodFactory = function (methodName, logLevel, loggerName) { rawMethod("Newsflash: " + message); }; }; -log.setLevel(log.getLevel()); // Be sure to call setLevel method in order to apply plugin +log.rebuild(); // Be sure to call the rebuild method in order to apply plugin. ``` *(The above supports only a single string `log.warn("...")` argument for clarity, but it's easy to extend to a [fuller variadic version](http://jsbin.com/xehoye/edit?html,console).)* diff --git a/index.d.ts b/index.d.ts index acb4d55..a0e7379 100644 --- a/index.d.ts +++ b/index.d.ts @@ -190,5 +190,14 @@ declare namespace log { * false as the optional 'persist' second argument, persistence will be skipped. */ disableAll(persist?: boolean): void; + + /** + * Rebuild the logging methods on this logger and its child loggers. + * + * This is mostly intended for plugin developers, but can be useful if you update a logger's `methodFactory` or + * if you want to apply the root logger’s level to any *pre-existing* child loggers (this updates the level on + * any child logger that hasn't used `setLevel()` or `setDefaultLevel()`). + */ + rebuild(): void; } } diff --git a/lib/loglevel.js b/lib/loglevel.js index eebcae1..469069e 100644 --- a/lib/loglevel.js +++ b/lib/loglevel.js @@ -31,6 +31,9 @@ "error" ]; + var _loggersByName = {}; + var defaultLogger = null; + // Cross-browser bind equivalent that works at least back to IE6 function bindMethod(obj, methodName) { var method = obj[methodName]; @@ -83,25 +86,33 @@ // These private functions always need `this` to be set properly - function replaceLoggingMethods(level, loggerName) { + function replaceLoggingMethods() { /*jshint validthis:true */ + var level = this.getLevel(); + + // Replace the actual methods. for (var i = 0; i < logMethods.length; i++) { var methodName = logMethods[i]; this[methodName] = (i < level) ? noop : - this.methodFactory(methodName, level, loggerName); + this.methodFactory(methodName, level, this.name); } // Define log.log as an alias for log.debug this.log = this.debug; + + // Return any important warnings. + if (typeof console === undefinedType && level < this.levels.SILENT) { + return "No console available for logging"; + } } // In old IE versions, the console isn't present until you first open it. // We build realMethod() replacements here that regenerate logging methods - function enableLoggingWhenConsoleArrives(methodName, level, loggerName) { + function enableLoggingWhenConsoleArrives(methodName) { return function () { if (typeof console !== undefinedType) { - replaceLoggingMethods.call(this, level, loggerName); + replaceLoggingMethods.call(this); this[methodName].apply(this, arguments); } }; @@ -109,16 +120,36 @@ // By default, we use closely bound real methods wherever possible, and // otherwise we wait for a console to appear, and then try again. - function defaultMethodFactory(methodName, level, loggerName) { + function defaultMethodFactory(methodName, _level, _loggerName) { /*jshint validthis:true */ return realMethod(methodName) || enableLoggingWhenConsoleArrives.apply(this, arguments); } - function Logger(name, defaultLevel, factory) { + function Logger(name, factory) { + // Private instance variables. var self = this; - var currentLevel; - defaultLevel = defaultLevel == null ? "WARN" : defaultLevel; + /** + * The level inherited from a parent logger (or a global default). We + * cache this here rather than delegating to the parent so that it stays + * in sync with the actual logging methods that we have installed (the + * parent could change levels but we might not have rebuilt the loggers + * in this child yet). + * @type {number} + */ + var inheritedLevel; + /** + * The default level for this logger, if any. If set, this overrides + * `inheritedLevel`. + * @type {number|null} + */ + var defaultLevel; + /** + * A user-specific level for this logger. If set, this overrides + * `defaultLevel`. + * @type {number|null} + */ + var userLevel; var storageKey = "loglevel"; if (typeof name === "string") { @@ -182,7 +213,6 @@ // Use localStorage if available try { window.localStorage.removeItem(storageKey); - return; } catch (ignore) {} // Use session cookie as fallback @@ -192,6 +222,18 @@ } catch (ignore) {} } + function normalizeLevel(input) { + var level = input; + if (typeof level === "string" && self.levels[level.toUpperCase()] !== undefined) { + level = self.levels[level.toUpperCase()]; + } + if (typeof level === "number" && level >= 0 && level <= self.levels.SILENT) { + return level; + } else { + throw new TypeError("log.setLevel() called with invalid level: " + input); + } + } + /* * * Public logger API - see https://github.com/pimterry/loglevel for details @@ -206,37 +248,36 @@ self.methodFactory = factory || defaultMethodFactory; self.getLevel = function () { - return currentLevel; + if (userLevel != null) { + return userLevel; + } else if (defaultLevel != null) { + return defaultLevel; + } else { + return inheritedLevel; + } }; self.setLevel = function (level, persist) { - if (typeof level === "string" && self.levels[level.toUpperCase()] !== undefined) { - level = self.levels[level.toUpperCase()]; - } - if (typeof level === "number" && level >= 0 && level <= self.levels.SILENT) { - currentLevel = level; - if (persist !== false) { // defaults to true - persistLevelIfPossible(level); - } - replaceLoggingMethods.call(self, level, name); - if (typeof console === undefinedType && level < self.levels.SILENT) { - return "No console available for logging"; - } - } else { - throw "log.setLevel() called with invalid level: " + level; + userLevel = normalizeLevel(level); + if (persist !== false) { // defaults to true + persistLevelIfPossible(userLevel); } + + // NOTE: in v2, this should call rebuild(), which updates children. + return replaceLoggingMethods.call(this); }; self.setDefaultLevel = function (level) { - defaultLevel = level; + defaultLevel = normalizeLevel(level); if (!getPersistedLevel()) { self.setLevel(level, false); } }; self.resetLevel = function () { - self.setLevel(defaultLevel, false); + userLevel = null; clearPersistedLevel(); + replaceLoggingMethods.call(this); }; self.enableAll = function(persist) { @@ -247,12 +288,28 @@ self.setLevel(self.levels.SILENT, persist); }; - // Initialize with the right level + self.rebuild = function () { + if (defaultLogger !== self) { + inheritedLevel = normalizeLevel(defaultLogger.getLevel()); + } + replaceLoggingMethods.call(this); + + if (defaultLogger === self) { + for (var childName in _loggersByName) { + _loggersByName[childName].rebuild(); + } + } + }; + + // Initialize all the internal levels. + inheritedLevel = normalizeLevel( + defaultLogger ? defaultLogger.getLevel() : "WARN" + ); var initialLevel = getPersistedLevel(); - if (initialLevel == null) { - initialLevel = defaultLevel; + if (initialLevel != null) { + userLevel = normalizeLevel(initialLevel); } - self.setLevel(initialLevel, false); + replaceLoggingMethods.call(this); } /* @@ -261,18 +318,19 @@ * */ - var defaultLogger = new Logger(); + defaultLogger = new Logger(); - var _loggersByName = {}; defaultLogger.getLogger = function getLogger(name) { if ((typeof name !== "symbol" && typeof name !== "string") || name === "") { - throw new TypeError("You must supply a name when creating a logger."); + throw new TypeError("You must supply a name when creating a logger."); } var logger = _loggersByName[name]; if (!logger) { - logger = _loggersByName[name] = new Logger( - name, defaultLogger.getLevel(), defaultLogger.methodFactory); + logger = _loggersByName[name] = new Logger( + name, + defaultLogger.methodFactory + ); } return logger; }; diff --git a/test/level-setting-test.js b/test/level-setting-test.js index f5d6d13..25f6c15 100644 --- a/test/level-setting-test.js +++ b/test/level-setting-test.js @@ -64,31 +64,31 @@ define(['../lib/loglevel'], function(log) { it("no level argument", function() { expect(function() { log.setLevel(); - }).toThrow("log.setLevel() called with invalid level: undefined"); + }).toThrowError(TypeError, "log.setLevel() called with invalid level: undefined"); }); it("a null level argument", function() { expect(function() { log.setLevel(null); - }).toThrow("log.setLevel() called with invalid level: null"); + }).toThrowError(TypeError, "log.setLevel() called with invalid level: null"); }); it("an undefined level argument", function() { expect(function() { log.setLevel(undefined); - }).toThrow("log.setLevel() called with invalid level: undefined"); + }).toThrowError(TypeError, "log.setLevel() called with invalid level: undefined"); }); it("an invalid log level index", function() { expect(function() { log.setLevel(-1); - }).toThrow("log.setLevel() called with invalid level: -1"); + }).toThrowError(TypeError, "log.setLevel() called with invalid level: -1"); }); it("an invalid log level name", function() { expect(function() { log.setLevel("InvalidLevelName"); - }).toThrow("log.setLevel() called with invalid level: InvalidLevelName"); + }).toThrowError(TypeError, "log.setLevel() called with invalid level: InvalidLevelName"); }); }); diff --git a/test/multiple-logger-test.js b/test/multiple-logger-test.js index 9849f7a..80905be 100644 --- a/test/multiple-logger-test.js +++ b/test/multiple-logger-test.js @@ -150,5 +150,126 @@ define(['test/test-helpers'], function(testHelpers) { expect(newLogger).toBeAtLevel("trace"); }); }); + + describe("logger.resetLevel()", function() { + beforeEach(function() { + window.console = {"log" : jasmine.createSpy("console.log")}; + jasmine.addMatchers({ + "toBeAtLevel" : testHelpers.toBeAtLevel + }); + testHelpers.clearStoredLevels(); + }); + + afterEach(function() { + window.console = originalConsole; + }); + + it("resets to the inherited level if no local level was set", function(log) { + testHelpers.setStoredLevel("ERROR", "newLogger"); + + log.setLevel("TRACE"); + var newLogger = log.getLogger("newLogger"); + expect(newLogger).toBeAtLevel("ERROR"); + + newLogger.resetLevel(); + expect(newLogger).toBeAtLevel("TRACE"); + + // resetLevel() should not have broken inheritance. + log.setLevel("DEBUG"); + log.rebuild(); + expect(newLogger).toBeAtLevel("DEBUG"); + }); + + it("resets to the inherited level if no default level was set", function(log) { + log.setLevel("TRACE"); + var newLogger = log.getLogger("newLogger"); + expect(newLogger).toBeAtLevel("TRACE"); + + newLogger.setLevel("ERROR"); + expect(newLogger).toBeAtLevel("ERROR"); + + newLogger.resetLevel(); + expect(newLogger).toBeAtLevel("TRACE"); + + // resetLevel() should not have broken inheritance. + log.setLevel("DEBUG"); + log.rebuild(); + expect(newLogger).toBeAtLevel("DEBUG"); + }); + + it("resets to the default level if one was set", function(log) { + testHelpers.setStoredLevel("ERROR", "newLogger"); + + log.setLevel("TRACE"); + var newLogger = log.getLogger("newLogger"); + newLogger.setDefaultLevel("INFO"); + expect(newLogger).toBeAtLevel("ERROR"); + + newLogger.resetLevel(); + expect(newLogger).toBeAtLevel("INFO"); + + // resetLevel() should not have caused inheritance to start. + log.setLevel("DEBUG"); + log.rebuild(); + expect(newLogger).toBeAtLevel("INFO"); + }); + }); + + describe("logger.rebuild()", function() { + beforeEach(function() { + window.console = {"log" : jasmine.createSpy("console.log")}; + jasmine.addMatchers({ + "toBeAtLevel" : testHelpers.toBeAtLevel + }); + testHelpers.clearStoredLevels(); + }); + + afterEach(function() { + window.console = originalConsole; + }); + + it("rebuilds existing child loggers", function(log) { + log.setLevel("TRACE"); + var newLogger = log.getLogger("newLogger"); + expect(newLogger).toBeAtLevel("TRACE"); + + log.setLevel("ERROR"); + expect(newLogger).toBeAtLevel("TRACE"); + + log.rebuild(); + expect(newLogger).toBeAtLevel("ERROR"); + }); + + it("should not change a child's persisted level", function(log) { + testHelpers.setStoredLevel("ERROR", "newLogger"); + + log.setLevel("TRACE"); + var newLogger = log.getLogger("newLogger"); + expect(newLogger).toBeAtLevel("ERROR"); + + log.rebuild(); + expect(newLogger).toBeAtLevel("ERROR"); + }); + + it("should not change a child's level set with `setLevel()`", function(log) { + log.setLevel("TRACE"); + var newLogger = log.getLogger("newLogger"); + expect(newLogger).toBeAtLevel("TRACE"); + + newLogger.setLevel("DEBUG", false); + log.rebuild(); + expect(newLogger).toBeAtLevel("DEBUG"); + }); + + it("should not change a child's level set with `setDefaultLevel()`", function(log) { + log.setLevel("TRACE"); + var newLogger = log.getLogger("newLogger"); + expect(newLogger).toBeAtLevel("TRACE"); + + newLogger.setDefaultLevel("DEBUG"); + log.rebuild(); + expect(newLogger).toBeAtLevel("DEBUG"); + }); + }); }); });