Skip to content

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

Open
joe-replin wants to merge 8 commits into
masterfrom
issue/818
Open

New: add SUCCESS log level, & scope logging (fixes #818)#832
joe-replin wants to merge 8 commits into
masterfrom
issue/818

Conversation

@joe-replin

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

Copy link
Copy Markdown
Contributor

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.

👀

@oliverfoster oliverfoster left a comment

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.

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 Outdated
*/
_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 Outdated
* @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 Outdated
* 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 Outdated

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 Outdated
@taylortom taylortom moved this from New to Assigned in adapt_framework: The TODO Board Mar 5, 2026
Add and refine JSDoc across logging files: add file/module header to logLevelEnum, include usage example in logging.js, mark Logging class with @Class and @extends, simplify @fires tags to short event names, and add docblocks for onLoadConfigData and loadConfig. Also document args for removed/deprecated/warnOnce methods and update _log event annotations. These changes improve documentation, readability and tooling support for the logging service.
Document that course `_logging` is applied at `configModel:dataLoaded` and that logs emitted before that use constructor defaults. Remove the optional `name` parameter and `_displayName` handling from `logging.scope`; scoped loggers are now cached by source and always use the source string as the console display name. This simplifies the API, avoids inconsistent display-name warnings for cached scopes, and clarifies why early log output may not reflect course-configured settings.
When useColors is enabled, log output now prints a CSS-styled prefix and spreads the original arguments into the console method instead of pre-serializing and joining them. This preserves native object inspection in devtools, avoids costly JSON serialization and truncation, and reduces console spam/overhead. Removes the _serializeArg helper and its truncation/serialization logic.
Remove support for colorized console styling and the internal _colors config. Simplifies Logging by always prefixing messages with a source or level and using the appropriate console method (falling back to console.log). Removes _getColorForLevel and related branching, and updates comments to reflect that console output is no longer colorized.
Add a CONSOLE_METHOD constant to centralize console method mapping and use it in _getConsoleMethod; add @fires JSDoc tags to each level method (debug, info, success, warn, error, fatal); simplify _log by inlining the isEnabled and level-comparison checks and using nullish coalescing for the default config level. Purely refactoring for clarity and maintainability; no intended behavior changes.
@joe-replin joe-replin requested a review from oliverfoster May 13, 2026 19:23
@joe-replin

Copy link
Copy Markdown
Contributor Author

Update

  • Removed %c CSS console styling — hardcoded colours clashed with dark mode DevTools; browser-native formatting is sufficient
  • Removed _serializeArg — objects were being stringified to a flat string, preventing the interactive DevTools JSON tree view and breaking circular-reference handling; raw args are now spread directly
  • Removed name parameter from scope() — display name was always identical to the source key; source now serves as both the cache key and the console label
  • Hoisted CONSOLE_METHOD lookup table to module scope — was recreated on every _logToConsole call despite depending only on constants available at module load
  • Simplified _logToConsole to a single code path with no branching
  • Added @fires log and @fires log:<level> to all six public level methods (debug, info, success, warn, error, fatal) — events were only documented on the private _log dispatch, leaving the public contract silent
  • Fixed misaligned JSDoc continuation line in scope()
  • Inlined single-use boolean intermediaries in _log
  • Fixed @enum placement in logLevelEnum.js — tag was on the @file block instead of directly above the LOG_LEVEL constant

Delete the deprecated `_colors` boolean setting from schema/config.model.schema and schema/config.schema.json. This removes the now-unused configuration for enabling colored console output.
@joe-replin joe-replin changed the title New: add SUCCESS log level, scope logging & color coding (fixes #818) New: add SUCCESS log level, & scope logging (fixes #818) May 13, 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