/* * The Peacock Project - a HITMAN server replacement. * Copyright (C) 2021-2023 The Peacock Project Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import type { NextFunction, Response } from "express" import type { RequestWithJwt } from "./types/types" import picocolors from "picocolors" /** * Represents the different log levels. */ export enum LogLevel { /** * For errors. Displays in red. */ ERROR, /** * For warnings. Displays in yellow. */ WARN, /** * For information. Displays in blue. * This is also the fallback for invalid log level values. */ INFO, /** * For debugging. * Displays in light blue, but only if the `DEBUG` environment variable is set to "*", "yes", "true", or "peacock". */ DEBUG, /** * For outputting stacktraces. */ TRACE, } const isDebug = ["*", "true", "peacock", "yes"].includes( process.env.DEBUG || "false", ) /** * Adds leading zeros to a number so that the length of the string will always * be the number of places specified. * * @param num The number. * @param places The intended width of the number (character count). * @example * zeroPad(5, 2) // -> "05" */ const zeroPad = (num: string | number, places: number) => String(num).padStart(places, "0") /** * Outputs all given arguments as a debug level indented JSON-message to the console. * * @param args The values to log. */ export function logDebug(...args: unknown[]): void { log(LogLevel.DEBUG, JSON.stringify(args, undefined, " ")) } /** * Outputs a log message to the console. * * @param level The message's level. * @param data The data to output. * @see LogLevel */ export function log(level: LogLevel, data: string): void { const m = data ?? "No message specified" const now = new Date() const stampParts: number[] = [ now.getHours(), now.getMinutes(), now.getSeconds(), ] const millis = zeroPad(now.getMilliseconds(), 3) const timestamp = `${stampParts .map((part) => zeroPad(part, 2)) .join(":")}:${millis}` const header = picocolors.gray(timestamp) let outputTransport: ( message?: unknown, ...optionalParams: unknown[] ) => void let levelString: string switch (level) { case LogLevel.ERROR: outputTransport = console.error levelString = picocolors.red("Error") break case LogLevel.WARN: outputTransport = console.warn levelString = picocolors.yellow("Warn") break case LogLevel.INFO: default: outputTransport = console.log levelString = picocolors.blue("Info") break case LogLevel.DEBUG: if (!isDebug) { return } outputTransport = console.log levelString = picocolors.blueBright("Debug") break case LogLevel.TRACE: outputTransport = console.trace levelString = picocolors.bgYellow("Trace") break } outputTransport(`[${header}] [${levelString}] ${m}`) } /** * Express middleware that logs all requests and their details with the info log level. * * @param req The Express request object. * @param res The Express response object. * @param next The Express next function. * @see LogLevel.INFO */ export function loggingMiddleware( req: RequestWithJwt, res: Response, next?: NextFunction, ): void { log( LogLevel.INFO, `${picocolors.green(req.method)} ${picocolors.underline(req.url)}`, ) next?.() }