class_base_log.js

import os from 'os'
import lodash from 'lodash'
import dayjs from 'dayjs'
import logLevels from '../../lib/log-levels.js'
import chalk from 'chalk'

const { isEmpty, without, merge } = lodash

export function isIgnored (level) {
  const { filter, isArray } = this.lib._
  let ignore = this.app.bajo.config.log.ignore ?? []
  if (!isArray(ignore)) ignore = [ignore]
  const items = filter(ignore, i => {
    const [ns, lvl] = i.split(':')
    if (lvl) return ns === this.name && lvl === level
    return ns === this.name
  })
  return items.length > 0
}

/**
 * A thin logger system.
 *
 * @class
 */
class Log {
  /**
   * @param {Object} plugin - Plugin instance
   */
  constructor (plugin) {
    this.plugin = plugin
    this.app = plugin.app
    this.format = 'YYYY-MM-DDTHH:mm:ss.SSS[Z]'
  }

  /**
   * Initialize logger. Auto detect to use different logger via Bajo's config file
   *
   * @method
   */
  init = () => {
    this.bajoLog = this.plugin.app.bajo.config.log.logger ?? 'bajoLogger'
  }

  /**
   * Interpolate and translate text via plugin's print engine. Check Print class
   * for more information
   *
   * @method
   * @param {string} text - Text pattern to use
   * @param {...any} [args] - Variables to interpolate with text pattern above
   * @returns {string} Interpolated & translated text
   */
  write = (text, ...args) => {
    return this.plugin.print.write(text, ...args)
  }

  /**
   * Do we use external logger or Bajo's built-in one?
   *
   * @method
   * @returns {boolean}
   */
  isExtLogger = () => {
    return !!(this.plugin.app[this.bajoLog] && this.plugin.app[this.bajoLog].logger)
  }

  /**
   * Is provided level being ignored by config?
   *
   * @method
   * @param {string} level - Log level
   * @returns {boolean}
   */
  isIgnored = level => {
    return isIgnored.call(this.plugin, level)
  }

  /**
   * Create child logger
   *
   * @method
   * @returns {Object} Child logger instance
   */
  child = () => {
    if (this.isExtLogger()) return this.plugin.app[this.bajoLog].logger.child()
    return this.plugin.app
  }

  /**
   * Display & format message according to one of these rules:
   * 1. ```level``` ```text``` ```var 1``` ```var 2``` ```...var n``` - Translate ```text``` and interpolate with ```vars``` for level ```level```
   * 2. ```level``` ```data``` ```text``` ```var 1``` ```var 2``` ```...var n``` - As above, and append stringified ```data```
   * 3. ```level``` ```error``` - Format as **error**. If current log level is _trace_, dump the error object on screen
   *
   * @method
   * @param {string} level - Log level to use
   * @param {...any} params - See format above
   */
  formatMsg = (level, ...params) => {
    if (this.plugin.app.bajo.config.log.level === 'silent') return
    if (!this.plugin.app.bajo.isLogInRange(level)) return
    const plain = this.plugin.app.bajo.config.log.plain
    let [data, msg, ...args] = params
    if (typeof data === 'string') {
      args.unshift(msg)
      msg = data
      data = null
    }
    args = without(args, undefined)
    if (data instanceof Error) {
      msg = 'error%s'
      args = [data.message]
    }
    msg = this.write(msg, ...args)
    if (this.plugin.app[this.bajoLog] && this.plugin.app[this.bajoLog].logger) {
      this.plugin.app[this.bajoLog].logger[level](data, `[${this.plugin.name}] ${msg}`, ...args)
    } else {
      let text
      const dt = new Date()
      if (this.plugin.app.bajo.config.env === 'prod') {
        const json = { level: logLevels[level].number, time: dt.valueOf(), pid: process.pid, hostname: os.hostname() }
        if (!isEmpty(data)) merge(json, data)
        merge(json, { msg: `[${this.plugin.name}] ${msg}` })
        text = JSON.stringify(json)
      } else {
        const date = dayjs(dt).utc(true).format(this.format)
        const tdate = plain ? `[${date}]` : chalk.cyan(date)
        const tlevel = plain ? level.toUpperCase() : chalk[logLevels[level].color](level.toUpperCase())
        const tplugin = plain ? `[${this.plugin.name}]` : chalk.bgBlue(`${this.plugin.name}`)
        text = `${tdate} ${tlevel}: ${tplugin} ${msg}`
        if (!isEmpty(data)) text += '\n' + JSON.stringify(data)
      }
      if (!this.isIgnored(level)) {
        console.log(text)
        if (data instanceof Error && level === 'trace') console.error(data)
      }
    }
  }

  /**
   * Display & format message as ```trace``` level. See {@link Log#formatMsg|formatMsg} for details
   *
   * @method
   * @param  {...any} params
   */
  trace = (...params) => {
    this.formatMsg('trace', ...params)
  }

  /**
   * Display & format message as ```debug``` level. See {@link Log#formatMsg|formatMsg} for details
   *
   * @method
   * @param  {...any} params
   */
  debug = (...params) => {
    this.formatMsg('debug', ...params)
  }

  /**
   * Display & format message as ```info``` level. See {@link Log#formatMsg|formatMsg} for details
   *
   * @method
   * @param  {...any} params
   */
  info = (...params) => {
    this.formatMsg('info', ...params)
  }

  /**
   * Display & format message as ```warn``` level. See {@link Log#formatMsg|formatMsg} for details
   *
   * @method
   * @param  {...any} params
   */
  warn = (...params) => {
    this.formatMsg('warn', ...params)
  }

  /**
   * Display & format message as ```error``` level. See {@link Log#formatMsg|formatMsg} for details
   *
   * @method
   * @param  {...any} params
   */
  error = (...params) => {
    this.formatMsg('error', ...params)
  }

  /**
   * Display & format message as ```fatal``` level. See {@link Log#formatMsg|formatMsg} for details
   *
   * @method
   * @param  {...any} params
   */
  fatal = (...params) => {
    this.formatMsg('fatal', ...params)
  }

  silent = (...params) => {
    this.formatMsg('silent', ...params)
  }
}

export default Log