Skip to content

New: add SUCCESS log level, scope logging & color coding (fixes #818)#832

Open
joe-replin wants to merge 2 commits intomasterfrom
issue/818
Open

New: add SUCCESS log level, scope logging & color coding (fixes #818)#832
joe-replin wants to merge 2 commits intomasterfrom
issue/818

Conversation

@joe-replin
Copy link
Copy Markdown
Contributor

@joe-replin joe-replin commented Feb 24, 2026

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

  • Course config set to _logging was replacing the entire logging config object, which could silently disable logging if keys like _isEnabled weren't present in the course JSON — it now merges with the defaults instead.
  • A ?loglevel= query string with an empty value was incorrectly passing the override guard; it is now ignored as expected.
  • Scoped logger output was always sent to console.log regardless of severity; it now routes to the correct console method for each level (e.g. console.error for ERROR/FATAL).
  • Calling 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.
  • A null or undefined _level config value would throw at runtime; it now falls back to info safely.

Update

  • Log events fired via trigger() now include the source name, so plugins listening to log events can tell which module sent the message.
  • removed() and deprecated() no longer mutate their arguments array before passing to warnOnce().
  • checkQueryStringOverride() renamed to _checkQueryStringOverride() to follow the existing private method convention.
  • Added JSDoc to both logging.js and logLevelEnum.js: @file/@module blocks, @classdesc with @fires for all public events, @param/@returns on public methods, @example on scope(), removed() and deprecated(), @throws on scope(), @private on all internal methods, and a ScopedLogger typedef.

New

  • Added a SUCCESS log level, sitting between INFO and WARN, for confirming things worked correctly without it looking like a warning.
  • Added logging.success() as a convenience method, consistent with the existing debug(), info(), warn() etc.
  • Added logging.scope('PluginName') to create a named logger for a plugin — output is prefixed [PluginName] in the console and optionally colour-coded by level.
  • Added a _colors config option (default true) that enables coloured console output for scoped loggers.

Testing

  1. Steps for testing1. Add { "_logging": { "_level": "debug" } } to config.json and confirm logging still works as normal (defaults are not wiped).
  2. Load the course with ?loglevel=success in the URL — only SUCCESS, WARN, ERROR and FATAL messages should appear in the console.
  3. In a plugin, call logging.scope('MyPlugin').success('hello') — confirm [MyPlugin] hello appears with a coloured background in the console.
  4. In a plugin, call logging.scope('MyPlugin').error('oops') -- confirm the message appears as console.error output (red in DevTools), not console.log.
  5. Call logging.scope('MyPlugin', 'Name1') then logging.scope('MyPlugin', 'Name2') -- confirm a warning is logged about the ignored display name.
  6. Load the course with ?loglevel= (empty value) and confirm no override is applied and no errors occur.

@joe-replin joe-replin self-assigned this Feb 24, 2026
@joe-replin joe-replin changed the title New: add SUCCESS log level, scope logging & colors (fixes #818) New: add SUCCESS log level, scope logging & color coding (fixes #818) Feb 24, 2026
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Copy link
Copy Markdown
Member

@oliverfoster oliverfoster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread js/logging.js
*/
_getColorForLevel(level) {
const colors = {
debug: 'RoyalBlue',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Browsers can be light/dark mode. The colours look like this in dark mode:

Image

The default colouration seems just fine:

Image

Comment thread js/logging.js
* @returns {string} String representation of the value
* @private
*/
_serializeArg(item) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Firefox:
Image

Chrome:
Image

Comment thread js/logging.js
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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread js/logging.js
* const logger = logging.scope('MyPlugin', 'Feature-X');
* logger.warn('Retrying…');
*/
scope(source, name) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread js/logging.js

const log = [level.asUpperCase + ':'];
data && log.push(...data);
const useColors = this._config._colors && source;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Colours and serialisation are only applied to scopped logs, is this intended?

Comment thread js/logging.js
* @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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@taylortom taylortom moved this from New to Assigned in adapt_framework: The TODO Board Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

Add scoped & color coded logging

4 participants