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.iso()}.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.now().format(time.format.short)}${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"]
].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.username];
} else if (req.method === "POST") {
for (const key of ["user_id", "username"]) {
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]);
}
});
}
};