handlers/LoggingHandler.js

'use strict';

const winston = require("winston");
const Transport = require("winston-transport");
const { format } = winston;
const { format: prettyFormat } = require("pretty-format");
const { getLocalisation: getText } = require("./LanguageHandler")
const { MessageEmbed } = require("discord.js");

/**
 * Class that handles logging related tasks.
 */
class LoggingHandler {
    /**
     * Reference to a Winston logger
     * @type {import("winston").Logger}
     * @static
     * @private
     */
    static #logger;

    /**
     * Create, set up and return a Winston Logger.
     * @param {import("discord.js").Client} client The Discord client the Logger will be attached to.
     * @returns {import("winston").Logger} The created Logger.
     * @static
     */
    static createLogger(client) {
        const logger = winston.createLogger({
            // setup transports
            transports: [
                // console output
                new winston.transports.Console({
                    format: format.combine(
                        format.colorize({
                            all: true
                        })
                    ),
                    // send all messages, down to debug level
                    level: "debug"
                }),
                // file output
                new winston.transports.File({
                    // ensure all filenames are unique
                    filename: `logs/${require("../../package.json").name}-${new Date().getUTCFullYear()}.${new Date().getUTCMonth() + 1}.${new Date().getUTCDate()}-${new Date().valueOf()}.log`,
                    // send messages of importance "info" and above
                    level: "info"
                }),
                // discord error channel output
                new DiscordLogger({
                    client: client,
                    channel: client.config.errorChannel,
                    level: "error"
                })
            ],
            // setup logging levels
            levels: {
                critical: 0,
                error: 1,
                warn: 2,
                info: 3,
                debug: 4
            },
            // use timestamps
            timestamp: true,
            // setup format
            format: format.combine(
                format.splat(),
                format.timestamp({
                    format: "YYYY/MM/DD, HH:mm:ss"
                }),
                format.errors({
                    stack: true
                }),
                format.printf(msg => `[${client.getText(client.consoleLang, ["console", "logging", msg.level.toLowerCase()])}] ${msg.timestamp}\t» ${typeof msg.message == "string" ? msg.message.replace(new RegExp(require("os").userInfo().username, "g"), "X") : "\n" + prettyFormat(msg.message).replace(new RegExp(require("os").userInfo().username, "g"), "X")}${msg.stack ? `\n${msg.stack.replace(new RegExp(require("os").userInfo().username, "g"), "X")}` : ""}`)
            )
        });
        // setup colours
        winston.addColors({
            critical: "bold red",
            error: "bold red blackBG",
            warn: "bold yellow blackBG",
            info: "cyan blackBG",
            debug: "grey blackBG"
        });
        // add uncaught exception logger
        process.on("uncaughtException", e => {
            logger.critical(`Uncaught exception:\n${e.stack}`);
        });

        logger.debug(client.getText(client.consoleLang, ["console", "logging", "startMessage"], client.getText(client.consoleLang, ["meta", "name"]), client.getText(client.consoleLang, ["meta", "author"])));

        logger.info(client.getText(client.consoleLang, ["console", "logging", "loggerSetup"]));

        LoggingHandler.#logger = logger;
        return logger;
    }

    /**
     * Log at debug level.
     * @param {String|Object} x The content to log.
     */
    static debug = x => LoggingHandler.#logger.debug(x);
    /**
     * Log at info level.
     * @param {String|Object} x The content to log.
     */
    static info = x => LoggingHandler.#logger.info(x);
    /**
     * Log at warn level.
     * @param {String|Object} x The content to log.
     */
    static warn = x => LoggingHandler.#logger.warn(x);
    /**
     * Log at error level.
     * @param {String|Object} x The content to log.
     */
    static error = x => LoggingHandler.#logger.error(x);
    /**
     * Log at critical level.
     * @param {String|Object} x The content to log.
     */
    static critical = x => LoggingHandler.#logger.critical(x);
}

module.exports = LoggingHandler

/**
 * Thing that logs stuff to discord
 * @ignore
 */
class DiscordLogger extends Transport {
    constructor(opts) {
        super(opts);
        this.client = opts.client;
        this.channel = opts.channel;
    }

    async log(info, callback) {
        let embed = new MessageEmbed()
            .setColor("#eb4034")
            .setTitle(`\\❌ ${info.message.replace(new RegExp(require("os").userInfo().username, "g"), "X").length < 100 ? info.message.replace(new RegExp(require("os").userInfo().username, "g"), "X") : getText(this.client?.consoleLang, ["generic", "logging", "genericError"])}`)
            .setDescription(`\`\`\`js\n${getMessage(info).length < 4096 ? getMessage(info) : getText(this.client?.consoleLang, ["generic", "logging", "tooLong"])}\`\`\``)
            .setTimestamp()
            .setFooter({
                text: getText(this.client?.consoleLang, ["generic", "logging", "footer"])
            });

        if (info.source) {
            embed.addField(getText(this.client?.consoleLang, ["generic", "logging", "user"]), `${info.source.user?.tag?.replace(/[_~>*`\\]/g, "\\$&")} (${info.source.user?.id})`, true);
            embed.addField(getText(this.client?.consoleLang, ["generic", "logging", "channel"]), `#${info.source.channel?.name?.replace(/[_~>*`\\]/g, "\\$&")} <#${info.source.channel?.id}> (${info.source.channel?.id})`, true);
            embed.addField(getText(this.client?.consoleLang, ["generic", "logging", "server"]), `${info.source.guild?.name?.replace(/[_~>*`\\]/g, "\\$&")} (${info.source.user?.id})`, true);
        }

        if (this.client?.user) {
            try {
                await (await this.client.channels.fetch(this.channel)).send({
                    embeds: [
                        embed
                    ]
                })
            } catch (e) {
                LoggingHandler.warn(getText(this.client?.consoleLang, ["generic", "logging", "messageError"]));
            };
        }

        this.emit("logged", info);

        callback();
    }
}

function getMessage(info) {
    return `${typeof info.message == "string" ? info.message.replace(new RegExp(require("os").userInfo().username, "g"), "X") : "\n" + prettyFormat(info.message).replace(new RegExp(require("os").userInfo().username, "g"), "X")}${info.stack ? `\n${info.stack.replace(new RegExp(require("os").userInfo().username, "g"), "X")}` : ""}`;
}