Logger.js

import jsLogger from 'js-logger'
import { version } from '../package.json'
import Diagnostics from './utils/Diagnostics'

/**
 * @module Logger
 * @description Manages all log messages from SDK modules, you can use this logger to add your custom
 * messages and set your custom log handlers to forward all messages to your own monitoring
 * system.
 *
 * By default all loggers are set in level OFF (Logger.OFF), and there are available
 * the following log levels.
 *
 * This module is based on [js-logger](https://github.com/jonnyreeves/js-logger) you can refer
 * to its documentation or following our examples.
 * @example
 * // Log a message
 * Logger.info('This is an info log', 445566)
 * // [Global] 2021-04-05T15:58:44.893Z - This is an info log 445566
 * @example
 * // Create a named logger
 * const myLogger = Logger.get('CustomLogger')
 * myLogger.setLevel(Logger.WARN)
 * myLogger.warn('This is a warning log')
 * // [CustomLogger] 2021-04-05T15:59:53.377Z - This is a warning log
 * @example
 * // Profiling
 * // Start timing something
 * Logger.time('Timer name')
 *
 * // ... some time passes ...
 *
 * // Stop timing something.
 * Logger.timeEnd('Timer name')
 * // Timer name: 35282.997802734375 ms
 */

jsLogger.useDefaults({ defaultLevel: jsLogger.TRACE })

const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR']

const formatter = (messages, context) => {
  messages.unshift(`[${context.name || 'Global'}] ${new Date().toISOString()} - ${context.level.name} -`)
}
const enabledFor = (level, loggerName) => {
  if (loggerName) {
    return level.value >= namedLoggerLevels[loggerName].value
  }
  return level.value >= loggerLevel.value
}

const historyHandler = (messages, context) => {
  messages = Array.prototype.slice.call(messages)
  messages = messages.map((m) => typeof m === 'object' ? JSON.stringify(m) : m)
  formatter(messages, context)

  if (maxLogHistorySize !== 0) {
    history.push(messages.join(' '))
    if (history.length >= maxLogHistorySize) {
      history = history.slice(-maxLogHistorySize)
    }
  } else {
    history = []
  }
}

const consoleHandler = jsLogger.createDefaultHandler({ formatter })
jsLogger.setHandler((messages, context) => {
  historyHandler(messages, context)
  if (enabledFor(context.level, context.name)) {
    consoleHandler(messages, context)
  }

  for (const { handler, level } of customHandlers) {
    if (context.level.value >= level.value) {
      handler(messages, context)
    }
  }
})

const DEFAULT_LOG_HISTORY_SIZE = 10000
let maxLogHistorySize = DEFAULT_LOG_HISTORY_SIZE
let history = []
let loggerLevel = jsLogger.OFF
const namedLoggerLevels = {}
const customHandlers = []

/**
 * @typedef {Object} LogLevel
 * @global
 * @property {Number} value - The numerical representation of the level.
 * @property {String} name - Human readable name of the log level.
 */

/** @constant {LogLevel} TRACE - Logger.TRACE */
/** @constant {LogLevel} DEBUG - Logger.DEBUG */
/** @constant {LogLevel} INFO  - Logger.INFO */
/** @constant {LogLevel} TIME  - Logger.TIME */
/** @constant {LogLevel} WARN  - Logger.WARN */
/** @constant {LogLevel} ERROR - Logger.ERROR */
/** @constant {LogLevel} OFF   - Logger.OFF */

const Logger = {
  ...jsLogger,
  enabledFor,
  /**
   * @function
   * @name getHistory
   * @description Get all logs generated during a session.
   * All logs are recollected besides the log level selected by the user.
   * @returns {Array<String>} All logs recollected from level TRACE.
   * @example Logger.getHistory()
   * // Outupt
   * // [
   * //   "[Director] 2021-04-05T14:09:26.625Z - Getting publisher connection data for stream name:  1xxx2",
   * //   "[Director] 2021-04-05T14:09:27.064Z - Getting publisher response",
   * //   "[Publish]  2021-04-05T14:09:27.066Z - Broadcasting"
   * // ]
   */
  getHistory: () => history,
  /**
   * @function
   * @name getHistoryMaxSize
   * @description Get the maximum count of logs preserved during a session.
   * @example Logger.getHistoryMaxSize()
   */
  getHistoryMaxSize: () => maxLogHistorySize,

  /**
   * @function
   * @name setHistoryMaxSize
   * @description Set the maximum count of logs to preserve during a session.
   * By default it is set to 10000.
   * @param {Number} maxSize - Max size of log history. Set 0 to disable history or -1 to unlimited log history.
   * @example Logger.setHistoryMaxSize(100)
   */
  setHistoryMaxSize: maxSize => { maxLogHistorySize = maxSize },

  /**
   * @function
   * @name setLevel
   * @description Set log level to all loggers.
   * @param {LogLevel} level - New log level to be set.
   * @example
   * // Global Level
   * Logger.setLevel(Logger.DEBUG)
   *
   * // Module Level
   * Logger.get('Publish').setLevel(Logger.DEBUG)
   */
  setLevel: level => {
    loggerLevel = level
    for (const key in namedLoggerLevels) {
      namedLoggerLevels[key] = level
    }
  },

  /**
   * @function
   * @name getLevel
   * @description Get global current logger level.
   * Also you can get the level of any particular logger.
   * @returns {LogLevel}
   * @example
   * // Global Level
   * Logger.getLevel()
   * // Output
   * // {value: 2, name: 'DEBUG'}
   *
   * // Module Level
   * Logger.get('Publish').getLevel()
   * // Output
   * // {value: 5, name: 'WARN'}
   */
  getLevel: () => loggerLevel,

  /**
   * @function
   * @name get
   * @description Gets or creates a named logger. Named loggers are used to group log messages
   * that refers to a common context.
   * @param {String} name
   * @returns {Object} Logger object with same properties and functions as Logger except
   * history and handlers related functions.
   * @example
   * const myLogger = Logger.get('MyLogger')
   * // Set logger level
   * myLogger.setLevel(Logger.DEBUG)
   *
   * myLogger.debug('This is a debug log')
   * myLogger.info('This is a info log')
   * myLogger.warn('This is a warning log')
   *
   * // Get logger level
   * myLogger.getLevel()
   * // {value: 3, name: 'INFO'}
   */
  get: name => {
    if (!namedLoggerLevels[name]) {
      namedLoggerLevels[name] = loggerLevel
    }
    const logger = jsLogger.get(name)
    logger.setLevel = (level) => { namedLoggerLevels[name] = level }
    logger.getLevel = () => namedLoggerLevels[name]
    return logger
  },
  /**
   * Callback which handles log messages.
   *
   * @callback loggerHandler
   * @global
   * @param {any[]} messages         - Arguments object with the supplied log messages.
   * @param {Object} context
   * @param {LogLevel} context.level - The currrent log level.
   * @param {String?} context.name   - The optional current logger name.
   */
  /**
   * @function
   * @name setHandler
   * @description Add your custom log handler to Logger at the specified level.
   * @param {loggerHandler} handler  - Your custom log handler function.
   * @param {LogLevel} level         - Log level to filter messages.
   * @example
   * const myHandler = (messages, context) => {
   *  // You can filter by logger
   *  if (context.name === 'Publish') {
   *    sendToMyLogger(messages[0])
   *  }
   *
   *  // You can filter by logger level
   *  if (context.level.value >= Logger.INFO.value) {
   *    sendToMyLogger(messages[0])
   *  }
   * }
   *
   * Logger.setHandler(myHandler, Logger.INFO)
   */
  setHandler: (handler, level) => { customHandlers.push({ handler, level }) },
  /**
   * @function
   * @name diagnose
   * @description Returns diagnostics information about the connection and environment, formatted according to the specified parameters.
   * @param {Object | Number} config - Configuration object for the diagnostic parameters
   * @param {Number} [config.statsCount = 60] - Number of stats objects to be included in the diagnostics report.
   * @param {Number} [config.historySize = 1000]  - Amount of history messages to be returned.
   * @param {String} [config.minLogLevel] - Levels of history messages to be included.
        * examples of minLogLevel values in level order:
        * 1 - TRACE
        * 2 - DEBUG
        * 3 - INFO
        * 4 - WARN
        * 5 - ERROR
        * If 'INFO' (3) given, return INFO (3), WARN (4), and ERROR (5) level messages.
   * @param {String} [config.statsFormat='JSON'] - Format of the stats objects in the diagnostics report. Use Logger.JSON or Logger.CMCD.
   * @returns {Object} An object containing relevant diagnostics information such as userAgent, SDK version, and stats data.
   * @example
   * // Example using default parameters
   * const diagnosticsDefault = Logger.diagnose();
   *
   * // Example specifying statsCount and format
   * const diagnostics = Logger.diagnose({ statsCount: 30, minLogLevel: 'INFO', format: Logger.CMCD });
   *
   * // Output: Diagnostics object with specified configuration
   */
  diagnose: (config = {}) => {
    let finalConfig
    const defaultConfig = {
      statsCount: 60,
      historySize: 1000,
      minLogLevel: 'TRACE',
      statsFormat: 'JSON'
    }
    // Method originally only took statsCount:number, check for backwards compatibility
    if (typeof config === 'number') {
      defaultConfig.statsCount = config
      finalConfig = defaultConfig
    } else {
      finalConfig = { ...defaultConfig, ...config }
    }
    const { statsCount, historySize, minLogLevel, statsFormat } = finalConfig
    const result = Diagnostics.get(statsCount, statsFormat)
    const history = Logger.getHistory()

    if (!Number.isInteger(historySize) || historySize <= 0) {
      throw new Error('Invalid Argument Exception : historySize must be a positive integer.')
    }

    if (!LOG_LEVELS.includes(minLogLevel.toUpperCase())) {
      throw new Error('Invalid Argument Exception : the minLogLevel parameter only excepts "trace", "debug", "info", "warn", and "error" as arguments.')
    }
    if (LOG_LEVELS.includes(minLogLevel.toUpperCase())) {
      const filteredLogLevels = LOG_LEVELS.slice(LOG_LEVELS.indexOf(minLogLevel.toUpperCase()))
      const filteredLevels = history.filter((log) => filteredLogLevels.some(level => log.includes(level)))
      result.history = filteredLevels.slice(-historySize)
    }
    return result
  },
  JSON: 'JSON',
  CMCD: 'CMCD',
  /**
   * @var
   * @name VERSION
   * @description Returns the current SDK version.
   */
  VERSION: version
}

export default Logger