logging.js

// =============================================================================
/*! countinmultiples */
/*! author: Toni Price (https://toni.rbind.io) */
// =============================================================================

/**
 * A simplistic logging system.
 */

/**
 * Logging levels.
 */
export const Level = Object.freeze({
  None:  0,
  Error: 1,
  Warn:  2,
  Info:  3,
  Debug: 4,
  Vbose: 5,
});

/**
 * Logging formats which are suitable for general messages (usually strings) as
 * 'log', arrays as 'tab' or 'dir' and objects as 'dir'.
 */
export const LogFmt = Object.freeze({
  log: 'log',
  dir: 'dir',
  tab: 'table',
});

// The global log level
export let LOGLEV;

if (typeof (LOGLEV) === 'undefined') { LOGLEV = Level.None; }

/**
 * Gets name of calling function name for logging purposes.
 * @param {RegExp} replacePattern - Pattern to replace from start of function
 *   name as it is returned from the stack trace.
 *   Default: /http.*\/js\//
 * @param {string} replaceStr - Replacement string for pattern from start of
 *   function name as it is returned from the stack trace. Default: ''
 * @param {number} padLen - Length for left-padding function name. Default: 52
 * @returns {string} The caller function name, filtered as specified.
 * @example
 * // Example import:
 * import { Level, loggingAt, logging, ctxt, log } from './logging.js';
 *
 * // Example usage:
 * LOGLEV = Level.Debug;
 * function someFunc() {
 *   log(Level.Info, `The current log level is '${LOGLEV}'`, ctxt());
 *   log(Level.Debug, `This is a debugging message`, ctxt());
 * }
 *
 * // Specifying non-default args:
 * // Note: Set args to 'undefined' for default parameter values
 * const ctxt1 = ctxt(/.*[@].*\/js\//);
 * const ctxt2 = ctxt(undefined, ':');
 * const ctxt3 = ctxt(undefined, undefined, 60);
 */
export function ctxt(replacePattern, replaceStr, padLen) {
  replacePattern = replacePattern || /http.*\/js\//;

  replaceStr = replaceStr || '';

  padLen = padLen || 52;

  // Note: You cannot use 'ctxt.caller.name' in strict mode.
  // The following is a completely hacky option - See:
  // Get current function name in strict mode
  // asked Jul 18, 2016 at 11:22
  // by exebook
  // https://stackoverflow.com/questions/
  //   38435450/get-current-function-name-in-strict-mode
  // [16spe23]

  const stack = new Error().stack;
  let caller = stack.split('\n')[1].trim();

  if (replacePattern !== undefined) {
    caller = caller.replace(replacePattern, replaceStr);
  }

  return caller.padStart(padLen, ' ');
}

/**
 * Checks if the logging level is enabled, i.e. if it is above 'Level.None'.
 * @returns {boolean} true if logging is enabled, false if not.
 * @example
 * if (logging()) { log(Level.Info, 'Logging is enabled.'); }
 */
export function logging() {
  return ((LOGLEV || 0) > Level.None);
}

/**
 * Checks if the logging level is enabled at or above the specified level.
 * @param {object} level - The `Level` value to check.
 * @returns {boolean} true if logging is enabled at or above the specified
 *   level, false if not.
 * @example
 * if (loggingAt(Level.Info)) { log(Level.Info, 'Logging at info level.'); }
 * if (loggingAt(Level.Debug)) { log(Level.Info, 'Logging at debug level.'); }
 */
export function loggingAt(level) {
  return ((LOGLEV || 0) >= level);
}

/**
 * Logs the given message at the current global logging level.
 * @param {object} level - A valid `Level` value which is the logging level
 *   at which to log this message.
 * @param {object} obj - The object to log at the specified logging level. this
 *   would typically be a string (message) but could also be, e.g., an array
 *   or class object, particularly if used in conjunction with a non-defualt
 *   value for `logFmt`.
 * @param {string} logCtxt - The logging context. Can be any string but would
 *   usually contain information about the calling function.
 * @param {object} logFmt - A valid `LogFmt` value. Valid options: 'log', 'dir',
 *   'tab'. 'log' will log as one of console.log, console.warn or console.error;
 *   'dir' will log as console.dir; 'tab' will log as console.table.
 *   Default: 'log'
 * @example
 * LOGLEV = Level.Info;
 * log(Level.Info, `The current log level is '${LOGLEV}'`, ctxt());
 *
 * const value = { 1: 'one', 2: 'two', 3: 'three' };
 * log(Level.Debug, value, ctxt(), 'tab');
 * log(Level.Debug, value, ctxt(), 'dir');
 *
 * class Fruit {
 *   constructor(type, colour) {
 *     this.type = type;
 *     this.colour = colour;
 *   }
 *  }
 * log(Level.Vbose, new Fruit('avo', 'green'), ctxt(), 'dir');
 */
export function log(level, obj, logCtxt, logFmt) {

  if ((LOGLEV || 0) < level) { return; }

  logFmt = logFmt || 'log';

  let msg = null;

  switch (logFmt) {

  case 'log':
    logCtxt = (logCtxt || '').length === 0 ? '' : `[${logCtxt}]`;
    msg = `${logCtxt} ${level}: ${obj}`;

    switch (level) {
    case Level.Error:
      console.error(msg);
      break;
    case Level.Warn:
      console.warn(msg);
      break;
    default:
      console.log(msg);
      break;
    }
    break;

  case 'dir':
    console.dir(obj);
    break;

  case 'tab':
    console.table(obj);
    break;

  default:
    break;
  }
}

// =============================================================================