diff --git a/package.json b/package.json index 7e426a83d..c75775019 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "probot": "^12.2.4", "smee-client": "^2.0.0", "tsx": "^3.12.7", + "ubiquibot-logger": "^0.3.0", "yaml": "^2.2.2", "zlib": "^1.0.5" }, diff --git a/src/adapters/adapters.ts b/src/adapters/adapters.ts index c3857d53a..a0ccb9592 100644 --- a/src/adapters/adapters.ts +++ b/src/adapters/adapters.ts @@ -2,7 +2,7 @@ import { createClient } from "@supabase/supabase-js"; import { Access } from "./supabase/helpers/tables/access"; import { Label } from "./supabase/helpers/tables/label"; import { Locations } from "./supabase/helpers/tables/locations"; -import { Logs } from "./supabase/helpers/tables/logs"; +import { Logs } from "ubiquibot-logger"; import { Settlement } from "./supabase/helpers/tables/settlement"; import { Super } from "./supabase/helpers/tables/super"; import { User } from "./supabase/helpers/tables/user"; diff --git a/src/adapters/supabase/helpers/pretty-logs.ts b/src/adapters/supabase/helpers/pretty-logs.ts deleted file mode 100644 index 0e02046c8..000000000 --- a/src/adapters/supabase/helpers/pretty-logs.ts +++ /dev/null @@ -1,188 +0,0 @@ -import util from "util"; -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -type PrettyLogsWithOk = "ok" | LogLevel; -export class PrettyLogs { - constructor() { - this.ok = this.ok.bind(this); - this.info = this.info.bind(this); - this.error = this.error.bind(this); - this.fatal = this.fatal.bind(this); - this.debug = this.debug.bind(this); - this.verbose = this.verbose.bind(this); - } - public fatal(message: string, metadata?: any) { - this._logWithStack(LogLevel.FATAL, message, metadata); - } - - public error(message: string, metadata?: any) { - this._logWithStack(LogLevel.ERROR, message, metadata); - } - - public ok(message: string, metadata?: any) { - this._logWithStack("ok", message, metadata); - } - - public info(message: string, metadata?: any) { - this._logWithStack(LogLevel.INFO, message, metadata); - } - - public debug(message: string, metadata?: any) { - this._logWithStack(LogLevel.DEBUG, message, metadata); - } - - public verbose(message: string, metadata?: any) { - this._logWithStack(LogLevel.VERBOSE, message, metadata); - } - - private _logWithStack(type: "ok" | LogLevel, message: string, metadata?: Metadata | string) { - this._log(type, message); - if (typeof metadata === "string") { - this._log(type, metadata); - return; - } - if (metadata) { - let stack = metadata?.error?.stack || metadata?.stack; - if (!stack) { - // generate and remove the top four lines of the stack trace - const stackTrace = new Error().stack?.split("\n"); - if (stackTrace) { - stackTrace.splice(0, 4); - stack = stackTrace.filter((line) => line.includes(".ts:")).join("\n"); - } - } - const newMetadata = { ...metadata }; - delete newMetadata.message; - delete newMetadata.name; - delete newMetadata.stack; - - if (!this._isEmpty(newMetadata)) { - this._log(type, newMetadata); - } - - if (typeof stack == "string") { - const prettyStack = this._formatStackTrace(stack, 1); - const colorizedStack = this._colorizeText(prettyStack, Colors.dim); - this._log(type, colorizedStack); - } else if (stack) { - const prettyStack = this._formatStackTrace((stack as unknown as string[]).join("\n"), 1); - const colorizedStack = this._colorizeText(prettyStack, Colors.dim); - this._log(type, colorizedStack); - } else { - throw new Error("Stack is null"); - } - } - } - - private _colorizeText(text: string, color: Colors): string { - if (!color) { - throw new Error(`Invalid color: ${color}`); - } - return color.concat(text).concat(Colors.reset); - } - - private _formatStackTrace(stack: string, linesToRemove = 0, prefix = ""): string { - const lines = stack.split("\n"); - for (let i = 0; i < linesToRemove; i++) { - lines.shift(); // Remove the top line - } - return lines - .map((line) => `${prefix}${line.replace(/\s*at\s*/, " ↳ ")}`) // Replace 'at' and prefix every line - .join("\n"); - } - - private _isEmpty(obj: Record) { - return !Reflect.ownKeys(obj).some((key) => typeof obj[String(key)] !== "function"); - } - - private _log(type: PrettyLogsWithOk, message: any) { - const defaultSymbols: Record = { - fatal: "×", - ok: "✓", - error: "⚠", - info: "›", - debug: "››", - verbose: "💬", - }; - - const symbol = defaultSymbols[type]; - - // Formatting the message - const messageFormatted = - typeof message === "string" - ? message - : util.inspect(message, { showHidden: true, depth: null, breakLength: Infinity }); - // const messageFormatted = - // typeof message === "string" ? message : JSON.stringify(Logs.convertErrorsIntoObjects(message)); - - // Constructing the full log string with the prefix symbol - const lines = messageFormatted.split("\n"); - const logString = lines - .map((line, index) => { - // Add the symbol only to the first line and keep the indentation for the rest - const prefix = index === 0 ? `\t${symbol}` : `\t${" ".repeat(symbol.length)}`; - return `${prefix} ${line}`; - }) - .join("\n"); - - const fullLogString = logString; - - const colorMap: Record = { - fatal: ["error", Colors.fgRed], - ok: ["log", Colors.fgGreen], - error: ["warn", Colors.fgYellow], - info: ["info", Colors.dim], - debug: ["debug", Colors.fgMagenta], - verbose: ["debug", Colors.dim], - }; - - const _console = console[colorMap[type][0] as keyof typeof console] as (...args: string[]) => void; - if (typeof _console === "function") { - _console(this._colorizeText(fullLogString, colorMap[type][1])); - } else { - throw new Error(fullLogString); - } - } -} -interface Metadata { - error?: { stack?: string }; - stack?: string; - message?: string; - name?: string; - [key: string]: any; -} - -enum Colors { - reset = "\x1b[0m", - bright = "\x1b[1m", - dim = "\x1b[2m", - underscore = "\x1b[4m", - blink = "\x1b[5m", - reverse = "\x1b[7m", - hidden = "\x1b[8m", - - fgBlack = "\x1b[30m", - fgRed = "\x1b[31m", - fgGreen = "\x1b[32m", - fgYellow = "\x1b[33m", - fgBlue = "\x1b[34m", - fgMagenta = "\x1b[35m", - fgCyan = "\x1b[36m", - fgWhite = "\x1b[37m", - - bgBlack = "\x1b[40m", - bgRed = "\x1b[41m", - bgGreen = "\x1b[42m", - bgYellow = "\x1b[43m", - bgBlue = "\x1b[44m", - bgMagenta = "\x1b[45m", - bgCyan = "\x1b[46m", - bgWhite = "\x1b[47m", -} -export enum LogLevel { - FATAL = "fatal", - ERROR = "error", - INFO = "info", - VERBOSE = "verbose", - DEBUG = "debug", -} diff --git a/src/adapters/supabase/helpers/tables/logs.ts b/src/adapters/supabase/helpers/tables/logs.ts deleted file mode 100644 index 4ac3393a2..000000000 --- a/src/adapters/supabase/helpers/tables/logs.ts +++ /dev/null @@ -1,376 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// This is disabled because logs should be able to log any type of data -// Normally this is forbidden - -import { SupabaseClient } from "@supabase/supabase-js"; -import { Context as ProbotContext } from "probot"; -import Runtime from "../../../../bindings/bot-runtime"; -import { COMMIT_HASH } from "../../../../commit-hash"; -import { Database } from "../../types/database"; - -import { LogLevel, PrettyLogs } from "../pretty-logs"; - -type LogFunction = (message: string, metadata?: any) => void; -type LogInsert = Database["public"]["Tables"]["logs"]["Insert"]; -type LogParams = { - level: LogLevel; - consoleLog: LogFunction; - logMessage: string; - metadata?: any; - postComment?: boolean; - type: PublicMethods; -}; -export class LogReturn { - logMessage: LogMessage; - metadata?: any; - - constructor(logMessage: LogMessage, metadata?: any) { - this.logMessage = logMessage; - this.metadata = metadata; - } -} - -type FunctionPropertyNames = { - [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; -}[keyof T]; - -type PublicMethods = Exclude, "constructor" | keyof object>; - -export type LogMessage = { raw: string; diff: string; level: LogLevel; type: PublicMethods }; - -export class Logs { - private _supabase: SupabaseClient; - private _context: ProbotContext | null = null; - - private _maxLevel = -1; - private _queue: LogInsert[] = []; // Your log queue - private _concurrency = 6; // Maximum concurrent requests - private _retryDelay = 1000; // Delay between retries in milliseconds - private _throttleCount = 0; - private _retryLimit = 0; // Retries disabled by default - - static console: PrettyLogs; - - private _log({ level, consoleLog, logMessage, metadata, postComment, type }: LogParams): LogReturn | null { - if (this._getNumericLevel(level) > this._maxLevel) return null; // filter out more verbose logs according to maxLevel set in config - - // needs to generate three versions of the information. - // they must all first serialize the error object if it exists - // - the comment to post on supabase (must be raw) - // - the comment to post on github (must include diff syntax) - // - the comment to post on the console (must be colorized) - - consoleLog(logMessage, metadata || undefined); - - if (this._context && postComment) { - const colorizedCommentMessage = this._diffColorCommentMessage(type, logMessage); - const commentMetaData = metadata ? Logs._commentMetaData(metadata, level) : null; - this._postComment(metadata ? [colorizedCommentMessage, commentMetaData].join("\n") : colorizedCommentMessage); - } - - const toSupabase = { log: logMessage, level, metadata } as LogInsert; - - this._save(toSupabase); - - return new LogReturn( - { - raw: logMessage, - diff: this._diffColorCommentMessage(type, logMessage), - type, - level, - }, - metadata - ); - } - private _addDiagnosticInformation(metadata: any) { - // this is a utility function to get the name of the function that called the log - // I have mixed feelings on this because it manipulates metadata later possibly without the developer understanding why and where, - // but seems useful for the metadata parser to understand where the comment originated from - - if (!metadata) { - metadata = {}; - } - if (typeof metadata == "string" || typeof metadata == "number") { - // TODO: think i need to support every data type - metadata = { message: metadata }; - } - - const stackLines = new Error().stack?.split("\n") || []; - if (stackLines.length > 3) { - const callerLine = stackLines[3]; // .replace(process.cwd(), ""); - const match = callerLine.match(/at (\S+)/); - if (match) { - metadata.caller = match[1]; - } - } - - const gitCommit = COMMIT_HASH?.substring(0, 7) ?? null; - metadata.revision = gitCommit; - - return metadata; - } - - public ok(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.INFO, - consoleLog: Logs.console.ok, - logMessage: log, - metadata, - postComment, - type: "ok", - }); - } - - public info(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.INFO, - consoleLog: Logs.console.info, - logMessage: log, - metadata, - postComment, - type: "info", - }); - } - - public error(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.ERROR, - consoleLog: Logs.console.error, - logMessage: log, - metadata, - postComment, - type: "error", - }); - } - - public debug(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.DEBUG, - consoleLog: Logs.console.debug, - logMessage: log, - metadata, - postComment, - type: "debug", - }); - } - - public fatal(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - if (!metadata) { - metadata = Logs.convertErrorsIntoObjects(new Error(log)); - const stack = metadata.stack as string[]; - stack.splice(1, 1); - metadata.stack = stack; - } - if (metadata instanceof Error) { - metadata = Logs.convertErrorsIntoObjects(metadata); - const stack = metadata.stack as string[]; - stack.splice(1, 1); - metadata.stack = stack; - } - - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.FATAL, - consoleLog: Logs.console.fatal, - logMessage: log, - metadata, - postComment, - type: "fatal", - }); - } - - verbose(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.VERBOSE, - consoleLog: Logs.console.verbose, - logMessage: log, - metadata, - postComment, - type: "verbose", - }); - } - - constructor(supabase: SupabaseClient, retryLimit: number, logLevel: LogLevel, context: ProbotContext | null) { - this._supabase = supabase; - this._context = context; - this._retryLimit = retryLimit; - this._maxLevel = this._getNumericLevel(logLevel); - Logs.console = new PrettyLogs(); - } - - private async _sendLogsToSupabase(log: LogInsert) { - const { error } = await this._supabase.from("logs").insert(log); - if (error) throw Logs.console.fatal("Error logging to Supabase:", error); - } - - private async _processLogs(log: LogInsert) { - try { - await this._sendLogsToSupabase(log); - } catch (error) { - Logs.console.fatal("Error sending log, retrying:", error); - return this._retryLimit > 0 ? await this._retryLog(log) : null; - } - } - - private async _retryLog(log: LogInsert, retryCount = 0) { - if (retryCount >= this._retryLimit) { - Logs.console.fatal("Max retry limit reached for log:", log); - return; - } - - await new Promise((resolve) => setTimeout(resolve, this._retryDelay)); - - try { - await this._sendLogsToSupabase(log); - } catch (error) { - Logs.console.fatal("Error sending log (after retry):", error); - await this._retryLog(log, retryCount + 1); - } - } - - private async _processLogQueue() { - while (this._queue.length > 0) { - const log = this._queue.shift(); - if (!log) { - continue; - } - await this._processLogs(log); - } - } - - private async _throttle() { - if (this._throttleCount >= this._concurrency) { - return; - } - - this._throttleCount++; - try { - await this._processLogQueue(); - } finally { - this._throttleCount--; - if (this._queue.length > 0) { - await this._throttle(); - } - } - } - - private async _addToQueue(log: LogInsert) { - this._queue.push(log); - if (this._throttleCount < this._concurrency) { - await this._throttle(); - } - } - - private _save(logInsert: LogInsert) { - this._addToQueue(logInsert) - .then(() => void 0) - .catch(() => Logs.console.fatal("Error adding logs to queue")); - - Logs.console.ok(logInsert.log, logInsert); - } - - static _commentMetaData(metadata: any, level: LogLevel) { - Runtime.getState().logger.debug("the main place that metadata is being serialized as an html comment"); - const prettySerialized = JSON.stringify(metadata, null, 2); - // first check if metadata is an error, then post it as a json comment - // otherwise post it as an html comment - if (level === LogLevel.FATAL) { - return ["```json", prettySerialized, "```"].join("\n"); - } else { - return [""].join("\n"); - } - } - - private _diffColorCommentMessage(type: string, message: string) { - const diffPrefix = { - fatal: "-", // - text in red - ok: "+", // + text in green - error: "!", // ! text in orange - // info: "#", // # text in gray - // debug: "@@@@",// @@ text in purple (and bold)@@ - // error: null, - // warn: null, - // info: null, - // verbose: "#", - // debug: "#", - }; - const selected = diffPrefix[type as keyof typeof diffPrefix]; - - if (selected) { - message = message - .trim() // Remove leading and trailing whitespace - .split("\n") - .map((line) => `${selected} ${line}`) - .join("\n"); - } else if (type === "debug") { - // debug has special formatting - message = message - .split("\n") - .map((line) => `@@ ${line} @@`) - .join("\n"); // debug: "@@@@", - } else { - // default to gray - message = message - .split("\n") - .map((line) => `# ${line}`) - .join("\n"); - } - - const diffHeader = "```diff"; - const diffFooter = "```"; - - return [diffHeader, message, diffFooter].join("\n"); - } - - private _postComment(message: string) { - // post on issue - if (!this._context) return; - this._context.octokit.issues - .createComment({ - owner: this._context.issue().owner, - repo: this._context.issue().repo, - issue_number: this._context.issue().issue_number, - body: message, - }) - // .then((x) => console.trace(x)) - .catch((x) => console.trace(x)); - } - - private _getNumericLevel(level: LogLevel) { - switch (level) { - case LogLevel.FATAL: - return 0; - case LogLevel.ERROR: - return 1; - case LogLevel.INFO: - return 2; - case LogLevel.VERBOSE: - return 4; - case LogLevel.DEBUG: - return 5; - default: - return -1; // Invalid level - } - } - static convertErrorsIntoObjects(obj: any): any { - // this is a utility function to render native errors in the console, the database, and on GitHub. - if (obj instanceof Error) { - return { - message: obj.message, - name: obj.name, - stack: obj.stack ? obj.stack.split("\n") : null, - }; - } else if (typeof obj === "object" && obj !== null) { - const keys = Object.keys(obj); - keys.forEach((key) => { - obj[key] = this.convertErrorsIntoObjects(obj[key]); - }); - } - return obj; - } -} diff --git a/src/bindings/bot-runtime.ts b/src/bindings/bot-runtime.ts index 650fc0925..8fadaba14 100644 --- a/src/bindings/bot-runtime.ts +++ b/src/bindings/bot-runtime.ts @@ -1,5 +1,5 @@ import { createAdapters } from "../adapters/adapters"; -import { Logs } from "../adapters/supabase/helpers/tables/logs"; +import { Logs } from "ubiquibot-logger"; class Runtime { private static _instance: Runtime; diff --git a/src/bindings/event.ts b/src/bindings/event.ts index 434299c77..d578e74b5 100644 --- a/src/bindings/event.ts +++ b/src/bindings/event.ts @@ -2,7 +2,7 @@ import OpenAI from "openai"; import { Context as ProbotContext } from "probot"; import zlib from "zlib"; import { createAdapters, supabaseClient } from "../adapters/adapters"; -import { LogMessage, LogReturn, Logs } from "../adapters/supabase/helpers/tables/logs"; +import { LogMessage, LogReturn, Logs } from "ubiquibot-logger"; import { processors, wildcardProcessors } from "../handlers/processors"; import { validateConfigChange } from "../handlers/push/push"; import structuredMetadata from "../handlers/shared/structured-metadata"; diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index 767d3dab7..c2d075166 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -1,4 +1,4 @@ -import { LogReturn } from "../adapters/supabase/helpers/tables/logs"; +import { LogReturn } from "ubiquibot-logger"; import { Context } from "../types/context"; import { HandlerReturnValuesNoVoid } from "../types/handlers"; import { StreamlinedComment } from "../types/openai"; diff --git a/src/types/configuration-types.ts b/src/types/configuration-types.ts index 0246c7000..c1c417c57 100644 --- a/src/types/configuration-types.ts +++ b/src/types/configuration-types.ts @@ -1,6 +1,6 @@ import { ObjectOptions, Static, StaticDecode, StringOptions, TProperties, Type as T } from "@sinclair/typebox"; import ms from "ms"; -import { LogLevel } from "../adapters/supabase/helpers/pretty-logs"; +import { LogLevel } from "ubiquibot-logger/pretty-logs"; import { userCommands } from "../handlers/comment/handlers/comment-handler-main"; import { validHTMLElements } from "../handlers/comment/handlers/issue/valid-html-elements"; import { ajv } from "../utils/ajv"; diff --git a/src/types/context.ts b/src/types/context.ts index 6db26f5b1..e6f4bf3a5 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -2,7 +2,7 @@ import { Context as ProbotContext, ProbotOctokit } from "probot"; import OpenAI from "openai"; import { BotConfig } from "./configuration-types"; import { Payload } from "./payload"; -import { Logs } from "../adapters/supabase/helpers/tables/logs"; +import { Logs } from "ubiquibot-logger"; export interface Context { event: ProbotContext; diff --git a/src/types/handlers.ts b/src/types/handlers.ts index a8c6cfaa8..698f47e34 100644 --- a/src/types/handlers.ts +++ b/src/types/handlers.ts @@ -1,4 +1,4 @@ -import { LogReturn } from "../adapters/supabase/helpers/tables/logs"; +import { LogReturn } from "ubiquibot-logger"; import { Context } from "./context"; export type HandlerReturnValuesNoVoid = null | string | LogReturn; diff --git a/yarn.lock b/yarn.lock index d7b4cead2..20d593683 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4227,6 +4227,11 @@ dotenv@*, dotenv@^8.2.0: resolved "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz" integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== +dotenv@^16.3.1: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" @@ -7183,7 +7188,7 @@ pino-pretty@^6.0.0: split2 "^3.1.1" strip-json-comments "^3.1.1" -pino-std-serializers@*, pino-std-serializers@^6.0.0: +pino-std-serializers@*, pino-std-serializers@^6.0.0, pino-std-serializers@^6.2.2: version "6.2.2" resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz" integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== @@ -8295,6 +8300,16 @@ typescript@^4.9.5: resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +ubiquibot-logger@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/ubiquibot-logger/-/ubiquibot-logger-0.3.0.tgz#ae296958daae3169bfde2866379216621c7c41f5" + integrity sha512-F3BExAEPfQrHK2GvR/T+9F+F5Q2dlv2dwiSkVdww4sqtj25oOpeL/uT8mUSI6xGWoh4Oo41KvoECTw1tELcn0Q== + dependencies: + "@supabase/supabase-js" "^2.4.0" + dotenv "^16.3.1" + pino-std-serializers "^6.2.2" + probot "^12.2.4" + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz"