msimerson/Haraka

View on GitHub
logger.js

Summary

Maintainability
D
1 day
Test Coverage
'use strict';
// Log class

const util      = require('util');
const tty       = require('tty');

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

let plugins;
let connection;
let outbound;

const regex = /(^$|[ ="\\])/;
const escape_replace_regex = /["\\]/g;

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 (regex.test(v)) {
            str += `${key}="${v.replace(escape_replace_regex, '\\$&')}" `;
        }
        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,
}

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.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;
}

if (!util.isFunction) {
    util.isFunction = functionToCheck => {
        const getType = {};
        return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
    };
}

logger.dump_and_exit = function (code) {
    this.dump_logs(() => {
        if (util.isFunction(code)) 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();

logger.log_if_level = (level, key, plugin) => function () {
    if (logger.loglevel < logger[key]) { return; }
    let logobj = {
        level,
        uuid: '-',
        origin: (plugin || 'core'),
        message: ''
    };
    for (const data of arguments) {
        if (typeof data !== 'object') {
            logobj.message += (data);
            continue;
        }
        if (!data) continue;

        // if the object is a connection, add the connection id
        if (data instanceof connection.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 (data.name) {
            logobj.origin = data.name;
        }
        else if (data instanceof outbound.HMailItem) {
            logobj.origin = 'outbound';
            if (data.todo) {
                if (data.todo.uuid) logobj.uuid = data.todo.uuid;
                if (data.todo.client_uuid) {    // dirty hack
                    logobj.origin = `outbound] [${data.todo.client_uuid}`;
                }
            }
        }
        else if (
            logger.format === logger.formats.LOGFMT && data.constructor === Object) {
            logobj = Object.assign(logobj, data);
        }
        else if (
            logger.format === logger.formats.JSON && data.constructor === Object) {
            logobj = Object.assign(logobj, data);
        }
        else if (typeof data === 'object' && Object.prototype.hasOwnProperty.call(data, "uuid")) {
            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));
        }
    }

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

logger.add_log_methods = (object, plugin) => {
    if (!object) return;
    if (typeof(object) !== 'object') return;

    for (const level in logger.levels) {
        const fname = `log${level.toLowerCase()}`;
        if (object[fname]) continue;  // already added
        object[fname] = logger.log_if_level(level, `LOG${level}`, plugin);
    }
}

logger.add_log_methods(logger);

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