Ctrl K
ComunidadGuías…

logger.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use strict'
// Log class

const util = require('node:util')
const tty = require('node:tty')

const config = require('haraka-config')
const constants = require('haraka-constants')

let plugins

const escapes = { '"': '\\"', '\\': '\\\\', '\r': '\\r', '\n': '\\n' }
const escape_char = (c) => escapes[c]

const needs_quoting = /(^$|[ ="\\\r\n])/
const needs_escaping = /["\\\r\n]/g
const line_breaks = /[\r\n]/g

// A free-form message is not a quoted value, so there is no escape character to
// make `\n` unambiguous. Collapse instead of escaping.
function collapse_line_breaks(str) {
    return str.replace(line_breaks, ' ')
}

function stringify(obj) {
    let str = ''
    let key
    for (key in obj) {
        let v = obj[key]
        if (v == null) {
            str += `${key}="" `
            continue
        }
        v = v.toString()
        if (needs_quoting.test(v)) {
            str += `${key}="${v.replace(needs_escaping, escape_char)}" `
        } else {
            str += `${key}=${v} `
        }
    }
    return str.trim()
}

const logger = exports

logger.levels = {
    DATA: 9,
    PROTOCOL: 8,
    DEBUG: 7,
    INFO: 6,
    NOTICE: 5,
    WARN: 4,
    ERROR: 3,
    CRIT: 2,
    ALERT: 1,
    EMERG: 0,
}
const level_names = Object.keys(logger.levels)

for (const le in logger.levels) {
    logger.levels[`LOG${le}`] = logger.levels[le]
    logger[`LOG${le}`] = logger.levels[le]
}

logger.formats = {
    DEFAULT: 'DEFAULT',
    LOGFMT: 'LOGFMT',
    JSON: 'JSON',
}

logger.loglevel = logger.levels.WARN
logger.format = logger.formats.DEFAULT
logger.timestamps = false
logger.deferred_logs = []
logger.name = 'logger'

logger.colors = {
    DATA: 'green',
    PROTOCOL: 'green',
    DEBUG: 'grey',
    INFO: 'cyan',
    NOTICE: 'blue',
    WARN: 'red',
    ERROR: 'red',
    CRIT: 'red',
    ALERT: 'red',
    EMERG: 'red',
}

const stdout_is_tty = tty.isatty(process.stdout.fd)

logger._init = function () {
    this.load_log_ini()
    this._init_loglevel()
    this._init_timestamps()
}

logger.load_log_ini = function () {
    this.cfg = config.get(
        'log.ini',
        {
            booleans: ['+main.timestamps'],
        },
        () => {
            this.load_log_ini()
        },
    )

    this.set_loglevel(this.cfg.main.level)
    this.set_timestamps(this.cfg.main.timestamps)
    this.set_format(this.cfg.main.format)
}

logger.colorize = (color, str) => {
    if (!util.inspect.colors[color]) {
        return str
    } // unknown color
    return `\u001b[${util.inspect.colors[color][0]}m${str}\u001b[${util.inspect.colors[color][1]}m`
}

logger.dump_logs = (cb) => {
    while (logger.deferred_logs.length > 0) {
        const log_item = logger.deferred_logs.shift()
        plugins.run_hooks('log', logger, log_item)
    }
    // Run callback after flush
    if (cb) process.stdout.write('', cb)
    return true
}

logger.dump_and_exit = function (code) {
    this.dump_logs(() => {
        if (typeof code === 'function') return code()
        process.exit(code)
    })
}

logger.log = (level, data, logobj) => {
    if (level === 'PROTOCOL') {
        data = data.replace(/\n/g, '\\n')
    }
    data = data.replace(/\r/g, '\\r').replace(/\n$/, '')

    const item = { level, data, obj: logobj }

    // buffer until plugins are loaded
    const emptyPluginList = !plugins || (Array.isArray(plugins.plugin_list) && !plugins.plugin_list.length)
    if (emptyPluginList) {
        logger.deferred_logs.push(item)
        return true
    }

    // process buffered logs
    while (logger.deferred_logs.length > 0) {
        const log_item = logger.deferred_logs.shift()
        plugins.run_hooks('log', logger, log_item)
    }

    plugins.run_hooks('log', logger, item)
    return true
}

logger.log_respond = (retval, msg, data) => {
    // any other return code is irrelevant
    if (retval !== constants.cont) return false

    let timestamp_string = ''
    if (logger.timestamps) timestamp_string = `${new Date().toISOString()} `

    const color = logger.colors[data.level]
    if (color && stdout_is_tty) {
        process.stdout.write(`${timestamp_string}${logger.colorize(color, data.data)}\n`)
    } else {
        process.stdout.write(`${timestamp_string}${data.data}\n`)
    }

    return true
}

logger.set_loglevel = function (level) {
    if (level === undefined || level === null) return

    const loglevel_num = parseInt(level)
    if (typeof level === 'string') {
        this.log('INFO', `loglevel: ${level.toUpperCase()}`)
        logger.loglevel = logger.levels[level.toUpperCase()]
    } else {
        logger.loglevel = loglevel_num
    }

    if (!Number.isInteger(logger.loglevel)) {
        this.log('WARN', `invalid loglevel: ${level} defaulting to LOGWARN`)
        logger.loglevel = logger.levels.WARN
    }
}

logger.set_format = function (format) {
    if (format) {
        logger.format = logger.formats[format.toUpperCase()]
        this.log('INFO', `log format: ${format.toUpperCase()}`)
    } else {
        logger.format = null
    }
    if (!logger.format) {
        this.log('WARN', `invalid log format: ${format} defaulting to DEFAULT`)
        logger.format = logger.formats.DEFAULT
    }
}

logger._init_loglevel = function () {
    const _loglevel = config.get('loglevel', 'value', () => {
        this._init_loglevel()
    })

    this.set_loglevel(_loglevel)
}

logger.would_log = (level) => {
    if (logger.loglevel < level) return false
    return true
}

logger.set_timestamps = (value) => {
    logger.timestamps = !!value
}

logger._init_timestamps = function () {
    const _timestamps = config.get('log_timestamps', 'value', () => {
        this._init_timestamps()
    })

    // If we've already been toggled to true by the cfg, we should respect this.
    this.set_timestamps(logger.timestamps || _timestamps)
}

logger._init()

function emit(level, logobj) {
    switch (logger.format) {
        case logger.formats.LOGFMT:
            return logger.log(level, stringify(logobj))
        case logger.formats.JSON:
            return logger.log(level, JSON.stringify(logobj))
        case logger.formats.DEFAULT:
        default:
            return logger.log(
                level,
                `[${logobj.level}] [${logobj.uuid}] [${logobj.origin}] ${collapse_line_breaks(logobj.message)}`,
            )
    }
}

logger.log_if_level = (level, key, origin) =>
    function () {
        if (logger.loglevel < logger[key]) return

        let logobj = {
            level,
            uuid: '-',
            origin: origin || 'core',
            message: '',
        }
        // JSON escapes line breaks, so it keeps a stack inside one record. The
        // line-oriented formats get a log line per frame instead.
        const split_stacks = logger.format !== logger.formats.JSON
        const stack_frames = []

        for (const data of arguments) {
            if (typeof data !== 'object') {
                logobj.message += data
                continue
            }
            if (!data) continue

            if (data instanceof Error) {
                const [summary, ...frames] = util.inspect(data).split('\n')
                // an Error raised only to capture a stack has nothing to say
                if (data.message) {
                    if (logobj.message && !logobj.message.endsWith(' ')) logobj.message += ' '
                    logobj.message += summary
                }
                if (split_stacks) {
                    stack_frames.push(...frames)
                } else {
                    logobj.message += frames.map((f) => `\n${f}`).join('')
                }
                continue
            }

            // if the object is a connection, add the connection id
            if (data.constructor?.name === 'Connection') {
                logobj.uuid = data.uuid
                if (data.tran_count > 0) logobj.uuid += `.${data.tran_count}`
            } else if (data instanceof plugins.Plugin) {
                logobj.origin = data.name
            } else if (Object.hasOwn(data, 'name')) {
                // outbound
                logobj.origin = data.name
                if (Object.hasOwn(data, 'uuid')) logobj.uuid = data.uuid
                if (data.todo?.uuid) logobj.uuid = data.todo.uuid // outbound/hmail
            } else if (logger.format === logger.formats.LOGFMT && data.constructor === Object) {
                logobj = { ...logobj, ...data }
            } else if (logger.format === logger.formats.JSON && data.constructor === Object) {
                logobj = { ...logobj, ...data }
            } else if (Object.hasOwn(data, 'uuid')) {
                // outbound/client_pool
                logobj.uuid = data.uuid
            } else if (data.constructor === Object) {
                if (!logobj.message.endsWith(' ')) logobj.message += ' '
                logobj.message += stringify(data)
            } else {
                logobj.message += util.inspect(data)
            }
        }

        emit(level, logobj)
        for (const frame of stack_frames) emit(level, { ...logobj, message: frame })
        return true
    }

logger.add_log_methods = (object, logName) => {
    if (!object) return

    if (typeof object === 'function') {
        // add logging methods to class prototypes (Connection, Plugin, etc.)

        for (const level of level_names.map((l) => l.toLowerCase())) {
            object.prototype[`log${level}`] = (function (level) {
                return function () {
                    logger[level].apply(logger, [this, ...arguments])
                }
            })(`log${level}`)
        }
    } else if (typeof object === 'object') {
        // add logging methods to objects

        for (const level of level_names) {
            // objects gets log function names: loginfo, logwarn, logdebug, ...
            const fnNames = [`log${level.toLowerCase()}`]

            // logger also gets short names
            if (Object.hasOwn(object, 'name') && object.name === 'logger') {
                fnNames.push(level.toLowerCase())
            }

            for (const fnName of fnNames) {
                if (Object.hasOwn(object, fnName)) continue // already added
                object[fnName] = logger.log_if_level(level, `LOG${level}`, logName)
            }
        }
    }
}

logger.add_log_methods(logger)

// load these down here so it sees all the logger methods compiled above
plugins = require('./plugins')