cozy-labs/cozy-desktop

View on GitHub
core/utils/logger.js

Summary

Maintainability
A
3 hrs
Test Coverage
/** A winston-based logger.
 *
 * @module core/utils/logger
 * @flow weak
 */

const fse = require('fs-extra')
const os = require('os')
const path = require('path')
const _ = require('lodash')
const winston = require('winston')
const DailyRotateFile = require('winston-daily-rotate-file')
const { combine, json, splat, timestamp, printf } = winston.format

/*::
export type Logger = winston.Logger
*/

const LOG_DIR = path.join(
  process.env.COZY_DESKTOP_DIR || os.homedir(),
  '.cozy-desktop'
)
const LOG_BASENAME = 'logs.txt'

fse.ensureDirSync(LOG_DIR)

// Remove the `timestamp` field as we use the `time` alias (for backwards
// compatibility with our jq filters.
//
// eslint-disable-next-line no-unused-vars
const dropTimestamp = winston.format(({ timestamp, ...meta }) => ({ ...meta }))

// Replace winston's string `level` with integers whose values come from
// bunyan for backwards compatibility with out jq filters.
// This has the added advantage of being sligtly lighter.
//
const FATAL_LVL = 60
const ERROR_LVL = 50
const WARN_LVL = 40
const INFO_LVL = 30
const DEBUG_LVL = 20
const TRACE_LVL = 10
const levelToInt = winston.format(({ level, ...meta }) => {
  const int =
    {
      fatal: FATAL_LVL,
      error: ERROR_LVL,
      warn: WARN_LVL,
      info: INFO_LVL,
      debug: DEBUG_LVL,
      trace: TRACE_LVL
    }[level] || 70
  return { level: int, ...meta }
})

// Replace `message` with `msg` for backwards compatibility with our jq filters.
const messageToMsg = winston.format(({ message, ...meta }) => ({
  msg: message,
  ...meta
}))

// Allow logging without message.
//
// e.g. log.info({ err, sentry: true })
//
const objectMsgToMeta = winston.format(({ message, ...meta }) => {
  if (typeof message === 'string') {
    return { message, ...meta }
  } else {
    return { message: '', ...message, ...meta }
  }
})

const hostname = winston.format(info => ({ ...info, hostname: os.hostname() }))

// Add the process pid to the logs so we can more easily detect when there are
// multiple instances of Desktop running at the same time or if it was
// restarted.
//
const pid = winston.format(info => ({ ...info, pid: process.pid }))

// Copied from bunyan for backwards compatibility.
//
const getFullErrorStack = err => {
  let ret = err.stack || err.toString()
  if (err.cause && typeof err.cause === 'function') {
    const cerr = err.cause()
    if (cerr) {
      ret += '\nCaused by: ' + getFullErrorStack(cerr)
    }
  }
  return ret
}
const errSerializer = err => {
  if (!err || !err.stack) return err
  const obj = {
    stack: getFullErrorStack(err),
    ..._.pick(err, [
      'message',
      'name',
      'code',
      'signal',
      'type',
      'reason',
      'address',
      'dest',
      'info',
      'path',
      'port',
      'syscall',
      'code',
      'status',
      'originalErr',
      'errors',
      'doc',
      'incompatibilities',
      'change',
      'data'
    ])
  }
  return obj
}
const error = winston.format(({ err, ...meta }) => ({
  err: errSerializer(err),
  ...meta
}))

const defaultFormatter = combine(
  objectMsgToMeta(),
  splat(),
  hostname(),
  pid(),
  timestamp({ alias: 'time' }),
  dropTimestamp(),
  error(),
  messageToMsg(),
  levelToInt()
)

const defaultTransport = new DailyRotateFile({
  level: process.env.DEBUG ? 'trace' : 'info',
  dirname: LOG_DIR,
  filename: LOG_BASENAME,
  datePattern: 'YYYY-MM-DD', // XXX: rotate every day
  maxFiles: 7,
  zippedArchive: true, // XXX: gzip archived log files
  format: combine(defaultFormatter, json())
})

const defaultLogger = winston.createLogger({
  levels: {
    fatal: 0,
    error: 1,
    warn: 2,
    info: 3,
    debug: 4,
    trace: 5
  },
  level: 'trace',
  transports: [defaultTransport]
})
defaultLogger.on('error', err => {
  // eslint-disable-next-line no-console
  console.log('failed to log', { err })
})

if (process.env.DEBUG) {
  const filename = 'debug.log'
  // XXX: Clear log file from previous logs to simplify analysis
  fse.outputFileSync(filename, '')

  defaultLogger.add(
    new winston.transports.File({
      filename,
      format: combine(defaultFormatter, json())
    })
  )
}

if (process.env.TESTDEBUG) {
  defaultLogger.add(
    new winston.transports.Console({
      handleExceptions: true,
      format: combine(
        splat(),
        error(),
        printf(({ component, message, ...meta }) => {
          let out = component

          if (meta.path) out += ` ${meta.path}`
          if (meta._id) out += ` ${meta._id}`

          out += ` ${message}`

          const extra = _.omit(meta, ['level'])
          if (Object.keys(extra).length > 0) out += ` ${JSON.stringify(extra)}`

          return out
        })
      )
    })
  )
}

function logger(options) {
  return defaultLogger.child(options)
}

module.exports = {
  FATAL_LVL,
  ERROR_LVL,
  WARN_LVL,
  INFO_LVL,
  DEBUG_LVL,
  TRACE_LVL,
  LOG_DIR,
  LOG_BASENAME,
  defaultFormatter,
  defaultLogger,
  defaultTransport,
  dropTimestamp,
  logger,
  messageToMsg
}