UNPKG

bajo

Version:

The ultimate framework for whipping up massive apps in no time

267 lines (249 loc) 8.41 kB
import os from 'os' import logLevels from '../../lib/log-levels.js' import chalk from 'chalk' import { stripVTControlCharacters } from 'node:util' /** * Log output in stringified JSON format. Returned when app run in ```prod``` environment * * @typedef TLogJson * @property {string} prefix - Message prefix * @property {string} message - The message itself * @property {string} level - Log level * @property {number} time - Time in millisecond * @property {number} pid - Process ID * @property {string} hostname - Hostname * @property {Object} [data] - Payload data, if any * @see Log#formatMsg */ /** * A thin & fast logger system. * * An instance is created by the {@link App|app} and available to use anywhere like this: * * ```javascript * ... anywhere inside your code * this.app.log.debug(...) * ``` * * Shortcuts to log's methods are also available on every Bajo {@link Plugin|plugin}. Call on * these shortcuts will be prefixed with it's plugin name automatically: * * ```javascript * ... anywhere inside your code * if (!isValid) this.log.error('Invalid value!') * ``` * * @class */ class Log { /** * @param {App} app - App instance */ constructor (app) { this.lastDelta = 0 /** * The app instance * @type {App} */ this.app = app const { fs } = this.app.lib this.logDir = `${this.app.bajo.dir.data}/log` if (this.app.bajo.config.log.save) fs.ensureDirSync(this.logDir) } /** * Display & format message according to one of these rules: * 1. ```level``` ```prefix``` ```text``` ```var 1``` ```var 2``` ```...var n``` - Translate ```text``` and interpolate with ```vars``` for level ```level``` * 2. ```level``` ```prefix``` ```data``` ```text``` ```var 1``` ```var 2``` ```...var n``` - As above, and append stringified ```data``` * 3. ```level``` ```prefix``` ```error``` - Format as {@link Err} object. If current log level is _trace_, dump it on screen * * In ```prod``` environment, log will be delivered as JSON stringified object. See {@link TLogJson} for more info * * @method * @param {string} level - Log level to use * @param {string} prefix - Prefix to the message * @param {...any} params - See format above * @see Err * @see TLogJson */ formatMsg = (level, prefix, ...params) => { const { dayjs } = this.app.lib const { isEmpty, merge, without } = this.app.lib._ if (this.app.bajo.config.log.level === 'silent') return if (!this.app.bajo.isLogInRange(level)) return const { useUtc, timeTaken, dateFormat, pretty } = this.app.bajo.config.log let [data, msg, ...args] = params if (data instanceof Error) { msg = 'error%s' args = [this.getErrorMessage(data)] console.error(data) } if (typeof data === 'string') { args.unshift(msg) msg = data data = null } args = without(args, undefined) msg = this.app.t(prefix, msg, ...args) let text const dt = dayjs() let diff = null if (timeTaken) { const delta = dt.diff(this.app.runAt, 'ms') diff = delta - this.lastDelta this.lastDelta = delta } if (this.app.bajo.config.env === 'prod') { const json = { prefix, msg, level: logLevels[level].number, time: dt.valueOf(), pid: process.pid, hostname: os.hostname() } if (!isEmpty(data)) merge(json, { data }) if (timeTaken) merge(json, { timeTaken: diff }) text = JSON.stringify(json) } else { let date = dt.clone() if (useUtc) date = dayjs.utc(dt) date = date.format(dateFormat) let tdate = pretty ? chalk.cyan(date) : `[${date}]` if (timeTaken) { const tdiff = pretty ? chalk.cyan(`+${diff}ms`) : `[+${diff}ms]` tdate += ` ${tdiff}` } const tlevel = pretty ? `${chalk[logLevels[level].color](level.toUpperCase())}:` : `[${level.toUpperCase()}]` const tprefix = pretty ? chalk.bgBlue(`${prefix}`) : `[${prefix}]` text = `${tdate} ${tlevel} ${tprefix} ${msg}` if (!isEmpty(data) && !(data instanceof Error)) text += '\n' + JSON.stringify(data) } console.log(text) if (this.app.bajo.config.log.save) this.save(text, prefix) } getErrorMessage = error => { const { isEmpty } = this.app.lib._ return isEmpty(error.message) ? (error.code ?? error.statusCode) : error.message } /** * Calculate pattern used for log rotation * * @method * @param {boolean} isPrev - If true, calculate previous rotation pattern * @returns {string} Calculated pattern */ getRotationPattern = (isPrev) => { const { dayjs } = this.app.lib const { cycle } = this.app.bajo.config.log.rotation if (cycle === 'none') return let pattern const now = dayjs() switch (cycle) { case 'monthly': { const dt = isPrev ? now.subtract(1, 'month') : now pattern = dt.format('YYYY-MM') break } case 'weekly': { const dt = isPrev ? now.subtract(1, 'week') : now pattern = dt.format(`YYYY-W${dt.week()}`) break } case 'daily': { const dt = isPrev ? now.subtract(1, 'day') : now pattern = dt.format('YYYY-MM-DD') break } } return pattern } /** * Save log to file in {dataDir}/log * * @method * @param {string} text - Log message to save * @param {string} prefix - Use prefix as basename. Defaults to 'bajo' */ save = (text, prefix = 'bajo') => { const { fs } = this.app.lib const fname = this.app.bajo.config.log.rotation.byPlugin ? prefix : 'bajo' let file = `${this.logDir}/${fname}.log` const content = stripVTControlCharacters(text) const pattern = this.getRotationPattern() if (pattern) { file = `${this.logDir}/${fname}.${pattern}.log` } fs.appendFileSync(file, `${content}\n`, 'utf8') // TODO: symlink bajo.log to target } /** * Display & format message in ```trace``` level. See {@link Log#formatMsg|formatMsg} for details * * @method * @param {string} prefix - Message prefix * @param {...any} params - Parameters */ trace = (prefix, ...params) => { this.formatMsg('trace', prefix, ...params) } /** * Display & format message in ```debug``` level. See {@link Log#formatMsg|formatMsg} for details * * @method * @param {string} prefix - Message prefix * @param {...any} params - Parameters */ debug = (prefix, ...params) => { this.formatMsg('debug', prefix, ...params) } /** * Display & format message in ```info``` level. See {@link Log#formatMsg|formatMsg} for details * * @method * @param {string} prefix - Message prefix * @param {...any} params - Parameters */ info = (prefix, ...params) => { this.formatMsg('info', prefix, ...params) } /** * Display & format message in ```warn``` level. See {@link Log#formatMsg|formatMsg} for details * * @method * @param {string} prefix - Message prefix * @param {...any} params - Parameters */ warn = (prefix, ...params) => { this.formatMsg('warn', prefix, ...params) } /** * Display & format message in ```error``` level. See {@link Log#formatMsg|formatMsg} for details * * @method * @param {string} prefix - Message prefix * @param {...any} params - Parameters */ error = (prefix, ...params) => { this.formatMsg('error', prefix, ...params) } /** * Display & format message in ```fatal``` level. See {@link Log#formatMsg|formatMsg} for details * * @method * @param {string} prefix - Message prefix * @param {...any} params - Parameters */ fatal = (prefix, ...params) => { this.formatMsg('fatal', prefix, ...params) } /** * Display & format message in ```silent``` level. See {@link Log#formatMsg|formatMsg} for details * * @method * @param {string} prefix - Message prefix * @param {...any} params - Parameters */ silent = (prefix, ...params) => { this.formatMsg('silent', prefix, ...params) } /** * Dispose internal references */ dispose = async () => { this.app = null } } export default Log