127.0.0.1:8000 watch-together / master server / utils / log.js
master

Tree @master (Download .tar.gz)

log.js @masterraw · history · blame

const fs = require("fs");
const os = require("os");
const chalk = require("chalk");
const time = require("./time");
const path = require("path");
const append_file = require('util').promisify(fs.appendFile);

let save_verbose = false;
let verbose = false;
let log_folder_exists = false;
let log_queue = [];
let log_queue_processing = [];

const MAX_DEPTH = 25;
const INDENT = '    ';
const LOGS_ROOT = path.join(__dirname, "../../logs");
const LOG_LEVEL = {
    ERROR: chalk.red,
    WARNING: chalk.yellow,
    INFO: chalk.greenBright,
    VERBOSE: x => x
};

push_log([], "general", "INFO", ["hostname:", os.hostname()]);

function formatData(data, type, depth) {
    if (depth >= MAX_DEPTH) {
        return '... (max depth reached)';
    }

    const format = {
        object: function(obj, depth) {
            let lastIndex = Object.keys(obj).length;
            if (!lastIndex) {
                return '{}';
            }
            let formatted = '{\n';
            let i = 1;
            for (let key in obj) {
                formatted += indent(depth + 1) + key + ': ' + formatData(
                    obj[key], getType(obj[key]), depth + 1
                );
                if (i < lastIndex) {
                    formatted += ',';
                }
                formatted += '\n';
                i++;
            }
            return formatted + indent(depth) + '}';
        },
        array: function(arr, depth) {
            let formatted = '[';
            let lastIndex = arr.length - 1;
            for (let i = 0; i < arr.length; i++) {
                formatted += formatData(arr[i], getType(arr[i]), depth);
                if (i < lastIndex) {
                    formatted += ', ';
                }
            }
            return formatted + ']';
        },
        function: function(func, depth) {
            let lines = (func + '').split('\n');
            if (!lines.length) {
                return '';
            }
            let formatted = lines[0];
            let trimdentation = formatted.length - formatted.trim().length;
            if (lines.length > 1) {
                formatted += '\n';
                if (trimdentation === 0) {
                    let lastLine = lines[lines.length - 1];
                    trimdentation = lastLine.length - lastLine.trim().length;
                }
            }
            for (let i = 1; i < lines.length; i++) {
                formatted += indent(depth) + lines[i].substr(trimdentation);
                if (i < lines.length - 1) {
                    formatted += '\n';
                }
            }
            return formatted;
        },
        regex: function(regex) {
            return '/' + regex.source + '/';
        },
        string: function(str, depth) {
            if (depth > 0 || !str.length) {
                return '"' + str + '"';
            }
            return str;
        },
        boolean: function(bool) {
            return bool ? 'true' : 'false';
        }
    } [type];

    if (format) {
        return format(data, depth);
    } else {
        // null, undefined, window, and numbers end up here
        return data + '';
    }
}

function getType(data) {
    if (data === null) {
        return 'undefined';
    }

    let type = typeof data;
    if (data && type === 'object') {
        if (data.length == null) {
            if (typeof data.getTime === 'function') {
                // Dates get formatted as strings
                return 'string';
            } else if (typeof data.test === 'function') {
                return 'regexp';
            }
            return 'object';
        }
        return 'array';
    }
    return type;
}

function indent(depth) {
    let str = '';
    for (let i = 0; i < depth; i++) {
        str += INDENT;
    }
    return str;
}

function process_log_queue() {
    if (log_queue_processing.length || !log_folder_exists) {
        return new Promise(r => r());
    }

    while (log_queue.length) {
        log_queue_processing.push(log_queue.shift());
    }

    // Just trying to be defensive in case this is ever invoked with log_queue empty
    if (!log_queue_processing.length) {
        return new Promise(r => r());
    }

    let log_line_count = 0;
    let bulk_logs = {};
    for (const [log_file, level, log] of log_queue_processing) {
        // Don't write verbose logs to file.
        if (level !== "VERBOSE") {
            console.log(LOG_LEVEL[level](log));
            log_line_count++;
            bulk_logs[log_file] = (bulk_logs[log_file] || "") + log + "\n";
        } else if (verbose) {
            // Log everything to the console if verbose logs are enabled
            console.log(LOG_LEVEL[level](log));
            if (save_verbose) {
                log_line_count++;
                bulk_logs[log_file] = (bulk_logs[log_file] || "") + log + "\n";
            }
        }
    }

    let bulk_logs_entries = Object.entries(bulk_logs);

    // If we only processed verbose logs then there is
    // nothing to write to the disk and we can exit early
    if (!bulk_logs_entries.length) {
        log_queue_processing = [];
        return new Promise(r => r());
    }

    return Promise.all(bulk_logs_entries.map(function([log_file, logs]) {
        return file_dump(log_file, logs).catch(function(error) {
            if (verbose) {
                console.error(error);
            }
        });
    })).then(function() {
        log_queue_processing = [];
    });
}

function file_dump(log_file, logs) {
    const file_path = path.join(LOGS_ROOT, `${log_file}-${time.toDateString()}.log`);
    return append_file(file_path, logs).then(() => file_path);
}

// data is an array of the things to log
function push_log(context, log_file, level, data) {
    context = context.filter(c => c);
    context = context.length ? " - " + context.join(" ") : "";
    data = `${data.reduce((str, arg) => (str + ` ${formatData(arg, getType(arg), 0)}`), "")}`;
    log_queue.push([log_file, level, `[${time.toShortString()}${context}]${data || ""}`]);
    process_log_queue();
}

module.exports = {
    LOGS_ROOT: LOGS_ROOT,
    process_log_queue: function() {
        log_folder_exists = true;
        return process_log_queue();
    },
    set_verbose: function(options) {
        verbose = options.verbose;
        save_verbose = options["save-verbose"];
    },
    file_dump: file_dump,
    create_logger: function() {
        const context = [...arguments];
        return [
            ["mysql"],
            ["suspicious_activity", "sus"],
            ["general"],
            ["mailgun"]
        ].reduce(function(acc, [log_file, export_name]) {
            return Object.assign(acc, {
                [export_name || log_file]: Object.keys(LOG_LEVEL).reduce(function(acc, level) {
                    return Object.assign(acc, {
                        [level.toLowerCase()]: function( /* log, log, ... */ ) {
                            push_log(context, log_file, level, [...arguments]);
                        }
                    })
                }, {})
            });
        }, {
            // Short for log.general.info(...user_info)
            // Used to mark that an endpoint was hit
            mark: function(req) {
                // We want to log at least one piece of identifying
                // information so that we know who is hitting our endpoints
                let user_info = [];
                if (req.user) {
                    user_info = [req.user.user_id, req.user.email];
                } else if (req.method === "POST") {
                    for (const key of ["user_id", "email"]) {
                        if (req.body[key]) {
                            user_info.push(`${key}: ${req.body[key]}`);
                        }
                    }
                }
                push_log(context, "general", "INFO", user_info);
            },
            // Short for log.general.verbose(...)
            v: function() {
                push_log(context, "general", "VERBOSE", [...arguments]);
            }
        });
    }
};