New: add SUCCESS log level, scope logging & color coding (fixes #818)#832
New: add SUCCESS log level, scope logging & color coding (fixes #818)#832joe-replin wants to merge 2 commits intomasterfrom
Conversation
There was a problem hiding this comment.
Using:
const circularObj = {
name: 'Example Object',
description: 'This is a large circular JSON object used for testing log truncation.',
data: Array.from({ length: 100 }, (_, i) => `Item ${i + 1}`)
};
circularObj.self = circularObj; // Create a circular reference for testing
const largeJSON = {
name: 'Example Object',
description: 'This is a large JSON object used for testing log truncation.',
data: Array.from({ length: 100 }, (_, i) => `Item ${i + 1}`)
};
const shortJSON = { name: 'Short Object', value: 42 };
const scopeA = logging.scope('KeyA', 'Display Name');
const scopeB = logging.scope('KeyB');
scopeA.debug('This is a debug message from Scope A', circularObj, largeJSON, shortJSON);
scopeA.info('This is an info message from Scope A', circularObj, largeJSON, shortJSON);
scopeA.success('This is a success message from Scope A', circularObj, largeJSON, shortJSON);
scopeA.error('This is an error message from Scope A', circularObj, largeJSON, shortJSON);
scopeA.warn('This is a warning from Scope A', circularObj, largeJSON, shortJSON);
scopeA.fatal('This is a fatal error from Scope A', circularObj, largeJSON, shortJSON);
scopeB.debug('This is a debug message from Scope B', circularObj, largeJSON, shortJSON);
scopeB.info('This is an info message from Scope B', circularObj, largeJSON, shortJSON);
scopeB.success('This is a success message from Scope B', circularObj, largeJSON, shortJSON);
scopeB.error('This is an error message from Scope B', circularObj, largeJSON, shortJSON);
scopeB.warn('This is a warning from Scope B', circularObj, largeJSON, shortJSON);
scopeB.fatal('This is a fatal error from Scope B', circularObj, largeJSON, shortJSON);
logging.debug('This is a debug message', circularObj, largeJSON, shortJSON);
logging.info('This is an info message', circularObj, largeJSON, shortJSON);
logging.success('This is a success message', circularObj, largeJSON, shortJSON);
logging.error('This is an error message', circularObj, largeJSON, shortJSON);
logging.warn('This is a warning', circularObj, largeJSON, shortJSON);
logging.fatal('This is a fatal error', circularObj, largeJSON, shortJSON);
scopeA.debug('This is a debug message from Scope A');
scopeA.info('This is an info message from Scope A');
scopeA.success('This is a success message from Scope A');
scopeA.error('This is an error message from Scope A');
scopeA.warn('This is a warning from Scope A');
scopeA.fatal('This is a fatal error from Scope A');
scopeB.debug('This is a debug message from Scope B');
scopeB.info('This is an info message from Scope B');
scopeB.success('This is a success message from Scope B');
scopeB.error('This is an error message from Scope B');
scopeB.warn('This is a warning from Scope B');
scopeB.fatal('This is a fatal error from Scope B');
logging.debug('This is a debug message');
logging.info('This is an info message');
logging.success('This is a success message');
logging.error('This is an error message');
logging.warn('This is a warning');
logging.fatal('This is a fatal error');tldr: Remove the color, displayName and JSON serialisation stuff. Keep scope and success.
| */ | ||
| _getColorForLevel(level) { | ||
| const colors = { | ||
| debug: 'RoyalBlue', |
| * @returns {string} String representation of the value | ||
| * @private | ||
| */ | ||
| _serializeArg(item) { |
There was a problem hiding this comment.
The console in most browsers handles JSON objects by using a separate console JSON view. The view allows you to expand and collapse the object in the console. The JSON view in the console deals with circular references just fine.
Converting the object to a string and/or truncating it will prevent the console JSON view from appearing. Circular object now don't appear in the console. The JSON as text view is much longer than the JSON view output.
| this._config = Adapt.config.get('_logging'); | ||
| const courseConfig = Adapt.config.get('_logging'); | ||
| // Merge course config with defaults instead of replacing | ||
| this._config = Object.assign({}, this._config, courseConfig); |
There was a problem hiding this comment.
As the override colors config is only applied at configModel:dataLoaded, it's possible to have a mixed colourization; default might be true but the config false. Any logs triggered before this event might have a different colors config.
| * const logger = logging.scope('MyPlugin', 'Feature-X'); | ||
| * logger.warn('Retrying…'); | ||
| */ | ||
| scope(source, name) { |
There was a problem hiding this comment.
I don't understand what the displayName name parameter is for. It seems an unnecessary feature. It is passed through the rest of the code as source. Being that the third arguments of _log and _logToConsole are source, which are derived from displayName here.
|
|
||
| const log = [level.asUpperCase + ':']; | ||
| data && log.push(...data); | ||
| const useColors = this._config._colors && source; |
There was a problem hiding this comment.
Colours and serialisation are only applied to scopped logs, is this intended?
| * @classdesc Singleton logging service. Wraps `console` output with log-level | ||
| * filtering, coloured scoped output for plugins, and once-only deduplication | ||
| * for deprecation and removal warnings. | ||
| * @fires module:core/js/logging~log |
There was a problem hiding this comment.
Remove the self-referential module references.
At some point this will be adapt-contrib-core in an npm folder.
The events are fired from this singleton, so can be singleton relative.




I've been using logging methods like this for the better part of a year now after being shown this by @chris-steele. I find it handy in all my work now and wanted to contribute it back.
Fixes #818
Fix
_loggingwas replacing the entire logging config object, which could silently disable logging if keys like_isEnabledweren't present in the course JSON — it now merges with the defaults instead.?loglevel=query string with an empty value was incorrectly passing the override guard; it is now ignored as expected.console.logregardless of severity; it now routes to the correct console method for each level (e.g.console.errorfor ERROR/FATAL).logging.scope()a second time with the same source but a different display name silently discarded the new name; it now logs a warning so the mismatch is visible._levelconfig value would throw at runtime; it now falls back toinfosafely.Update
trigger()now include the source name, so plugins listening tologevents can tell which module sent the message.removed()anddeprecated()no longer mutate their arguments array before passing towarnOnce().checkQueryStringOverride()renamed to_checkQueryStringOverride()to follow the existing private method convention.logging.jsandlogLevelEnum.js:@file/@moduleblocks,@classdescwith@firesfor all public events,@param/@returnson public methods,@exampleonscope(),removed()anddeprecated(),@throwsonscope(),@privateon all internal methods, and aScopedLoggertypedef.New
SUCCESSlog level, sitting betweenINFOandWARN, for confirming things worked correctly without it looking like a warning.logging.success()as a convenience method, consistent with the existingdebug(),info(),warn()etc.logging.scope('PluginName')to create a named logger for a plugin — output is prefixed[PluginName]in the console and optionally colour-coded by level._colorsconfig option (defaulttrue) that enables coloured console output for scoped loggers.Testing
{ "_logging": { "_level": "debug" } }toconfig.jsonand confirm logging still works as normal (defaults are not wiped).?loglevel=successin the URL — only SUCCESS, WARN, ERROR and FATAL messages should appear in the console.logging.scope('MyPlugin').success('hello')— confirm[MyPlugin] helloappears with a coloured background in the console.logging.scope('MyPlugin').error('oops')-- confirm the message appears asconsole.erroroutput (red in DevTools), notconsole.log.logging.scope('MyPlugin', 'Name1')thenlogging.scope('MyPlugin', 'Name2')-- confirm a warning is logged about the ignored display name.?loglevel=(empty value) and confirm no override is applied and no errors occur.