Skip to content

Commit

Permalink
Merge pull request #189 from Mr0grog/187-make-loggers-rebuildable
Browse files Browse the repository at this point in the history
Add `Logger.rebuild()` Method
  • Loading branch information
pimterry committed Jan 25, 2024
2 parents f7becd9 + 67e968b commit bd9eca0
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 50 deletions.
48 changes: 38 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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).)*
Expand Down
9 changes: 9 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
128 changes: 93 additions & 35 deletions lib/loglevel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -83,42 +86,70 @@

// 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);
}
};
}

// 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") {
Expand Down Expand Up @@ -182,7 +213,6 @@
// Use localStorage if available
try {
window.localStorage.removeItem(storageKey);
return;
} catch (ignore) {}

// Use session cookie as fallback
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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);
}

/*
Expand All @@ -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;
};
Expand Down
10 changes: 5 additions & 5 deletions test/level-setting-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});

Expand Down

0 comments on commit bd9eca0

Please sign in to comment.