From abb548f5ec961b2443090c89173435b847e55ead Mon Sep 17 00:00:00 2001 From: Clayton Wilson Date: Tue, 12 Mar 2024 01:06:33 -0500 Subject: [PATCH] Refactor codebase to improve output flexibility --- src/Logger.ts | 296 +++++------------------------------- src/interface/output.ts | 11 ++ src/lib/utilities.ts | 16 ++ src/outputs/Console.ts | 76 +++++++++ src/outputs/Tampermonkey.ts | 142 +++++++++++++++++ src/types/logger.ts | 160 +------------------ src/types/output.ts | 125 +++++++++++++++ 7 files changed, 413 insertions(+), 413 deletions(-) create mode 100644 src/interface/output.ts create mode 100644 src/outputs/Console.ts create mode 100644 src/outputs/Tampermonkey.ts create mode 100644 src/types/output.ts diff --git a/src/Logger.ts b/src/Logger.ts index 03386b5..ca0f0b1 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -1,287 +1,63 @@ -import { - LogLabel, - LogLevel, - StrictLogConfig, - StrictLogOutputs, -} from "./types/logger"; -import { gzip, randomString, stringifyInstance, ungzip } from "./lib/utilities"; - -import { LogContext, BucketInfo, LogMeta, LogConfig } from "./types/logger"; - -const MESSAGE_STYLE = "background: inherit; color: inherit;"; - -function getLabel(level: number) { - if (level <= LogLevel.TRACE) { - return LogLabel.TRACE; - } else if (level <= LogLevel.DEBUG) { - return LogLabel.DEBUG; - } else if (level <= LogLevel.INFO) { - return LogLabel.INFO; - } else if (level <= LogLevel.WARN) { - return LogLabel.WARN; - } else { - return LogLabel.FATAL; - } -} +import { LogContext, LogLevel, LogMeta } from "./types/logger"; +import { Loggable, Storable } from "./interface/output"; export class Logger { - private buffer: string[]; - private bufferLength: number; - private bucketIndex: BucketInfo[]; - private outputs: StrictLogOutputs; - private bufferCapacity: number; + private outputs: Loggable[] = []; + private storage: Storable | undefined; + constructor() {} - constructor(config: LogConfig = {}) { - this.buffer = []; - this.bufferLength = 0; - const parsedConfig = StrictLogConfig.parse(config); - this.bufferCapacity = parsedConfig.bufferCapacity; - this.outputs = parsedConfig.outputs; - - if (this.outputs.tampermonkey.enabled) { - this.bucketIndex = JSON.parse( - GM_getValue(this.outputs.tampermonkey.bucketIndexKey, "[]") - ); - } else { - this.bucketIndex = []; - } + addOutput(output: Loggable) { + this.outputs.push(output); + return this; } - log(message: string, context: LogContext = {}) { - const level = context.level ? context.level : LogLevel.FATAL; - const label = getLabel(level); + setStorage(storage: Storable) { + this.storage = storage; + return this; + } + + ok() { + // TODO: Check all outputs and storage for required props + return this; + } + + log(message: string, level: LogLevel, context: LogContext) { + // const label = getLabel(level); const meta: LogMeta = { - context, - time: new Date().valueOf(), + level, + time: new Date(), }; - if (this.outputs.console.enabled) { - this.consolePrint(label, message, meta); + for (const output of this.outputs) { + output.write(message, meta, context); } - const textOutput = `${new Date( - meta.time - ).toISOString()} [${label}] ${message} - ${stringifyInstance( - meta.context - )}`; - - this.buffer.push(textOutput); - this.bufferLength += textOutput.length; - - if (this.outputs.callback) { - this.outputs.callback(textOutput); - } - - if (this.bufferLength >= this.bufferCapacity) { - if (this.outputs.tampermonkey.enabled) { - this.flush(); - } else { - while (this.bufferLength >= this.bufferCapacity) { - const stale = this.buffer.shift(); - const offset = stale ? stale.length : 0; - this.bufferLength -= offset; - } - } + if (this.storage) { + this.storage.write(message, meta, context); } } - trace(message: string, context?: Object) { - this.log(message, { - level: LogLevel.TRACE, + trace(message: string, context: LogContext = {}) { + this.log(message, LogLevel.TRACE, { stacktrace: new Error().stack?.slice(13), // Remove the "Error\n at " ...context, }); } - debug(message: string, context?: Object) { - this.log(message, { level: LogLevel.DEBUG, ...context }); + debug(message: string, context: LogContext = {}) { + this.log(message, LogLevel.DEBUG, context); } - info(message: string, context?: Object) { - this.log(message, { level: LogLevel.INFO, ...context }); + info(message: string, context: LogContext = {}) { + this.log(message, LogLevel.INFO, context); } - warn(message: string, context?: Object) { - this.log(message, { level: LogLevel.WARN, ...context }); + warn(message: string, context: LogContext = {}) { + this.log(message, LogLevel.WARN, context); } - fatal(message: string, context?: Object) { - this.log(message, { level: LogLevel.FATAL, ...context }); - } - - private consolePrint(label: LogLabel, message: string, meta: LogMeta) { - const styleFormatter = `background: ${this.outputs.console.style[label].backgroundColor}; color: ${this.outputs.console.style[label].textColor}; font-weight: bold; border-radius: 4px;`; - - switch (label) { - case LogLabel.TRACE: - console.trace( - `%c ${label} ` + `%c ${message}`, - styleFormatter, - MESSAGE_STYLE, - meta - ); - break; - case LogLabel.DEBUG: - console.debug( - `%c ${label} ` + `%c ${message}`, - styleFormatter, - MESSAGE_STYLE, - meta - ); - break; - case LogLabel.INFO: - console.info( - `%c ${label} ` + `%c ${message}`, - styleFormatter, - MESSAGE_STYLE, - meta - ); - break; - case LogLabel.WARN: - console.warn( - `%c ${label} ` + `%c ${message}`, - styleFormatter, - MESSAGE_STYLE, - meta - ); - break; - case LogLabel.FATAL: - console.error( - `%c ${label} ` + `%c ${message}`, - styleFormatter, - MESSAGE_STYLE, - meta - ); - break; - default: - console.log( - `%c ${label} ` + `%c ${message}`, - styleFormatter, - MESSAGE_STYLE, - meta - ); - break; - } - } - - async flush() { - // Clear buffer - const stringifiedBuffer = JSON.stringify(this.buffer); - this.buffer = []; - this.bufferLength = 0; - - // Don't flush unless tampermonkey output is enabled - if (!this.outputs.tampermonkey.enabled) { - return; - } - - // Generate non-clashing name - let newBucketName = randomString(10).toLowerCase(); - while (GM_getValue(newBucketName, undefined) !== undefined) { - newBucketName = randomString(10); - } - - // GZip data - const gzipped = await gzip(stringifiedBuffer); - - // Update bucketIndex with info - const newBucket: BucketInfo = { - name: newBucketName, - size: gzipped.length, - createdAt: new Date().valueOf(), - }; - - // Write bucketIndex to disk - this.bucketIndex.push(newBucket); - GM_setValue( - this.outputs.tampermonkey.bucketIndexKey, - JSON.stringify(this.bucketIndex) - ); - - // Write gzipped data to new bucket - GM_setValue(newBucketName, gzipped); - - if (this.bucketIndex.length <= this.outputs.tampermonkey.maxBuckets) { - return; - } - - // Delete old buckets if the number is too large - let oldBuckets = this.bucketIndex - .sort((a, b) => a.createdAt - b.createdAt) - .slice(0, -this.bufferCapacity); - - oldBuckets.forEach((oldBucket) => { - GM_deleteValue(oldBucket.name); - let deleteIndex = this.bucketIndex.findIndex( - (indexBucket) => indexBucket.name === oldBucket.name - ); - - if (deleteIndex === -1) { - console.error("Invalid index for bucket"); - return; - } - - this.bucketIndex.splice(deleteIndex, 1); - }); - - // Update tampermonkey bucket index - GM_setValue( - this.outputs.tampermonkey.bucketIndexKey, - JSON.stringify(this.bucketIndex) - ); - } - - async export(amount: number) { - // Check if the buffer has the requested amount - if (this.buffer.length >= amount) { - return this.buffer.slice(this.buffer.length - amount); - } - - // Only return buffer if tamppermonkey is disabled - if (!this.outputs.tampermonkey.enabled) { - return [...this.buffer]; - } - - let logs = [...this.buffer]; - - for (const bucket of this.bucketIndex) { - // Get data from bucket - const gzipped = GM_getValue(bucket.name, undefined); - if (gzipped === undefined) { - console.error("Bucket does not exist on disk", bucket); - continue; - } - // Ungzip and parse - const ungzipped = await ungzip(gzipped); - let lines: string[] = []; - try { - lines = JSON.parse(ungzipped) as string[]; - } catch (err) { - // Bucket has invalid or empty data - lines = []; - } - // prepend to logs up to amount - if (logs.length + lines.length < amount) { - // Need to grab more from storage - logs.unshift(...lines); - } else if (logs.length + lines.length == amount) { - // Have the exact amount - logs.unshift(...lines); - break; - } else { - // Grab a slice of the exact amount needed - logs.unshift(...lines.slice(0, amount - logs.length)); - break; - } - } - - return logs; - } - - async exportGzipped(amount: number) { - const lines = await this.export(amount); - const gzipped = await gzip(JSON.stringify(lines)); - return gzipped; + fatal(message: string, context: LogContext = {}) { + this.log(message, LogLevel.FATAL, context); } } diff --git a/src/interface/output.ts b/src/interface/output.ts new file mode 100644 index 0000000..a91d58e --- /dev/null +++ b/src/interface/output.ts @@ -0,0 +1,11 @@ +import { LogContext, LogMeta } from "../types/logger"; + +export abstract class Loggable { + abstract write(message: string, meta: LogMeta, context: LogContext): void; +} + +export abstract class Storable extends Loggable { + abstract read( + entries: number + ): Promise<{ message: string; meta: LogMeta; context: LogContext }[]>; +} diff --git a/src/lib/utilities.ts b/src/lib/utilities.ts index 0487a83..30cd4c5 100644 --- a/src/lib/utilities.ts +++ b/src/lib/utilities.ts @@ -1,3 +1,5 @@ +import { LogLabel, LogLevel } from "logger"; + export function blobToBase64(blob: Blob) { return new Promise((resolve, _) => { const reader = new FileReader(); @@ -71,3 +73,17 @@ export function randomString(length: number) { } return result; } + +export function getLabel(level: number) { + if (level <= LogLevel.TRACE) { + return LogLabel.TRACE; + } else if (level <= LogLevel.DEBUG) { + return LogLabel.DEBUG; + } else if (level <= LogLevel.INFO) { + return LogLabel.INFO; + } else if (level <= LogLevel.WARN) { + return LogLabel.WARN; + } else { + return LogLabel.FATAL; + } +} diff --git a/src/outputs/Console.ts b/src/outputs/Console.ts new file mode 100644 index 0000000..6426d68 --- /dev/null +++ b/src/outputs/Console.ts @@ -0,0 +1,76 @@ +import { LogContext, LogLabel, LogMeta } from "../types/logger"; +import { Loggable } from "../interface/output"; +import { + ConsoleOutputConfig, + StrictConsoleOutputConfig, +} from "../types/output"; +import { getLabel } from "../lib/utilities"; + +const MESSAGE_STYLE = "background: inherit; color: inherit;"; + +export default class Console implements Loggable { + private style; + enabled: boolean; + constructor(config: ConsoleOutputConfig = {}) { + const parsedConfig = StrictConsoleOutputConfig.parse(config); + this.style = parsedConfig.style; + this.enabled = parsedConfig.enabled; + } + write(message: string, meta: LogMeta, context: LogContext): void { + if (!this.enabled) return; + + const label = getLabel(meta.level); + const styleFormatter = `background: ${this.style[label].backgroundColor}; color: ${this.style[label].textColor}; font-weight: bold; border-radius: 4px;`; + + switch (label) { + case LogLabel.TRACE: + console.trace( + `%c ${label} ` + `%c ${message}`, + styleFormatter, + MESSAGE_STYLE, + context + ); + break; + case LogLabel.DEBUG: + console.debug( + `%c ${label} ` + `%c ${message}`, + styleFormatter, + MESSAGE_STYLE, + context + ); + break; + case LogLabel.INFO: + console.info( + `%c ${label} ` + `%c ${message}`, + styleFormatter, + MESSAGE_STYLE, + context + ); + break; + case LogLabel.WARN: + console.warn( + `%c ${label} ` + `%c ${message}`, + styleFormatter, + MESSAGE_STYLE, + context + ); + break; + case LogLabel.FATAL: + console.error( + `%c ${label} ` + `%c ${message}`, + styleFormatter, + MESSAGE_STYLE, + context + ); + break; + default: + console.log( + `%c ${label} ` + `%c ${message}`, + styleFormatter, + MESSAGE_STYLE, + context + ); + break; + } + } +} diff --git a/src/outputs/Tampermonkey.ts b/src/outputs/Tampermonkey.ts new file mode 100644 index 0000000..06c3558 --- /dev/null +++ b/src/outputs/Tampermonkey.ts @@ -0,0 +1,142 @@ +import { Storable } from "../interface/output"; +import { + StrictTampermonkeyOutputConfig, + TampermonkeyBucketInfo, + TampermonkeyOutputConfig, +} from "../types/output"; +import { gzip, randomString, ungzip } from "../lib/utilities"; +import { LogContext, LogData, LogMeta } from "../types/logger"; + +export default class Tampermonkey implements Storable { + private buffer: LogData[]; + private bucketList: TampermonkeyBucketInfo[]; + private bufferCapacity: number; + private bucketListKey: string; + private maxBuckets: number; + enabled: boolean; + + constructor(config: TampermonkeyOutputConfig = {}) { + this.buffer = []; + + const parsedConfig = StrictTampermonkeyOutputConfig.parse(config); + this.bucketListKey = parsedConfig.bucketListKey; + this.enabled = parsedConfig.enabled; + this.bufferCapacity = parsedConfig.bufferCapacity; + this.maxBuckets = parsedConfig.maxBuckets; + + this.bucketList = GM_getValue(this.bucketListKey, []); + } + + write(message: string, meta: LogMeta, context: LogContext): void { + if (!this.enabled) return; + this.buffer.push({ message, meta, context }); + + if (this.buffer.length > this.bufferCapacity) { + this.flush(); + } + } + + async read( + entries: number + ): Promise<{ message: string; meta: LogMeta; context: LogContext }[]> { + if (!this.enabled) return Promise.resolve([]); + + // Check if the buffer has the requested amount + if (this.buffer.length >= entries) { + return Promise.resolve(this.buffer.slice(this.buffer.length - entries)); + } + + let logs = [...this.buffer]; + + for (const bucket of this.bucketList) { + // Get data from bucket + const gzipped = GM_getValue(bucket.name, undefined); + if (gzipped === undefined) { + console.error("Bucket does not exist on disk", bucket); + continue; + } + // Ungzip and parse + const ungzipped = await ungzip(gzipped); + let lines: LogData[]; + try { + lines = JSON.parse(ungzipped) as LogData[]; + } catch (err) { + // Bucket has invalid or empty data + lines = []; + } + // prepend to logs up to amount + if (logs.length + lines.length < entries) { + // TODO: Need to grab more from storage + logs.unshift(...lines); + } else if (logs.length + lines.length == entries) { + // Have the exact amount + logs.unshift(...lines); + break; + } else { + // Grab a slice of the exact amount needed + logs.unshift(...lines.slice(0, entries - logs.length)); + break; + } + } + + return logs; + } + + async flush() { + // Clear buffer + const stringifiedBuffer = JSON.stringify(this.buffer); + this.buffer = []; + + // Don't flush unless tampermonkey output is enabled + if (!this.enabled) return; + + // Generate non-clashing name + let newBucketName = randomString(10).toLowerCase(); + while (GM_getValue(newBucketName, undefined) !== undefined) { + newBucketName = randomString(10); + } + + // GZip data + const gzipped = await gzip(stringifiedBuffer); + + // Update bucketList with info + const newBucket: TampermonkeyBucketInfo = { + name: newBucketName, + size: gzipped.length, + createdAt: new Date().valueOf(), + }; + + // Write bucketList to disk + this.bucketList.push(newBucket); + GM_setValue(this.bucketListKey, this.bucketList); + + // Write gzipped data to new bucket + GM_setValue(newBucketName, gzipped); + + if (this.bucketList.length <= this.maxBuckets) { + return; + } + + // Delete old buckets if the number is too large + let oldBuckets = this.bucketList + .sort((a, b) => a.createdAt - b.createdAt) + .slice(0, -this.bufferCapacity); + + oldBuckets.forEach((oldBucket) => { + GM_deleteValue(oldBucket.name); + let deleteIndex = this.bucketList.findIndex( + (indexBucket) => indexBucket.name === oldBucket.name + ); + + if (deleteIndex === -1) { + console.error("Invalid index for bucket"); + return; + } + + this.bucketList.splice(deleteIndex, 1); + }); + + // Update tampermonkey bucket index + GM_setValue(this.bucketListKey, this.bucketList); + } +} diff --git a/src/types/logger.ts b/src/types/logger.ts index a357ce5..21a818e 100644 --- a/src/types/logger.ts +++ b/src/types/logger.ts @@ -1,5 +1,3 @@ -import { z } from "zod"; - export enum LogLevel { TRACE = 10, DEBUG = 20, @@ -16,161 +14,17 @@ export enum LogLabel { FATAL = "fatal", } -export interface TampermonkeyOutputOpts { - enabled: boolean; - maxBuckets?: number; - bucketIndexKey?: string; -} - -export interface ConsoleOutputOpts { - enabled: boolean; -} - -export const ConsoleStyles = z - .object({ - trace: z - .object({ - backgroundColor: z.string().default("#949494").optional(), - textColor: z.string().default("#fff").optional(), - }) - .default({}) - .optional(), - debug: z - .object({ - backgroundColor: z.string().default("#fe7bf3").optional(), - textColor: z.string().default("#fff").optional(), - }) - .default({}) - .optional(), - info: z - .object({ - backgroundColor: z.string().default("#65f10e").optional(), - textColor: z.string().default("#fff").optional(), - }) - .default({}) - .optional(), - warn: z - .object({ - backgroundColor: z.string().default("#faf200").optional(), - textColor: z.string().default("#000").optional(), - }) - .default({}) - .optional(), - fatal: z - .object({ - backgroundColor: z.string().default("#cc0018").optional(), - textColor: z.string().default("#fff").optional(), - }) - .default({}) - .optional(), - }) - .optional(); - -export type ConsoleStyles = z.infer; - -export const StrictConsoleStyles = z - .object({ - trace: z - .object({ - backgroundColor: z.string().default("#949494"), - textColor: z.string().default("#fff"), - }) - .default({}), - debug: z - .object({ - backgroundColor: z.string().default("#fe7bf3"), - textColor: z.string().default("#fff"), - }) - .default({}), - info: z - .object({ - backgroundColor: z.string().default("#65f10e"), - textColor: z.string().default("#fff"), - }) - .default({}), - warn: z - .object({ - backgroundColor: z.string().default("#faf200"), - textColor: z.string().default("#000"), - }) - .default({}), - fatal: z - .object({ - backgroundColor: z.string().default("#cc0018"), - textColor: z.string().default("#fff"), - }) - .default({}), - }) - .default({}); - -export const LogOutputs = z.object({ - console: z - .object({ - enabled: z.boolean().default(true).optional(), - style: ConsoleStyles, - }) - .default({}) - .optional(), - tampermonkey: z - .object({ - enabled: z.boolean().default(false).optional(), - maxBuckets: z.number().default(10).optional(), - bucketIndexKey: z.string().default("bucket_index").optional(), - }) - .default({}) - .optional(), - callback: z.function().args(z.string()).optional(), -}); - -export type LogOutputs = z.infer; - -export const LogConfig = z.object({ - outputs: LogOutputs.default({}).optional(), - bufferCapacity: z.number().default(100000).optional(), -}); - -export type LogConfig = z.infer; - -export const StrictLogOutputs = z.object({ - console: z - .object({ - enabled: z.boolean().default(true), - style: StrictConsoleStyles, - }) - .default({}), - tampermonkey: z - .object({ - enabled: z.boolean().default(false), - maxBuckets: z.number().default(10), - bucketIndexKey: z.string().default("bucket_index"), - }) - .default({}), - callback: z.function().args(z.string()).optional(), -}); - -export type StrictLogOutputs = z.infer; - -export const StrictLogConfig = z.object({ - outputs: StrictLogOutputs.default({}), - bufferCapacity: z.number().default(100000), -}); - -export type StrictLogConfig = z.infer; - export interface LogContext { - level?: number; [key: string]: any; } export interface LogMeta { + level: LogLevel; + time: Date; +} + +export interface LogData { + message: string; + meta: LogMeta; context: LogContext; - time: number; } - -export interface BucketInfo { - name: string; - size: number; - createdAt: number; -} - -export type StrictConsoleStyles = z.infer; diff --git a/src/types/output.ts b/src/types/output.ts new file mode 100644 index 0000000..36c2a75 --- /dev/null +++ b/src/types/output.ts @@ -0,0 +1,125 @@ +import { z } from "zod"; + +export const ConsoleStyles = z + .object({ + trace: z + .object({ + backgroundColor: z.string().default("#949494").optional(), + textColor: z.string().default("#fff").optional(), + }) + .default({}) + .optional(), + debug: z + .object({ + backgroundColor: z.string().default("#fe7bf3").optional(), + textColor: z.string().default("#fff").optional(), + }) + .default({}) + .optional(), + info: z + .object({ + backgroundColor: z.string().default("#65f10e").optional(), + textColor: z.string().default("#fff").optional(), + }) + .default({}) + .optional(), + warn: z + .object({ + backgroundColor: z.string().default("#faf200").optional(), + textColor: z.string().default("#000").optional(), + }) + .default({}) + .optional(), + fatal: z + .object({ + backgroundColor: z.string().default("#cc0018").optional(), + textColor: z.string().default("#fff").optional(), + }) + .default({}) + .optional(), + }) + .optional(); + +export type ConsoleStyles = z.infer; + +export const StrictConsoleStyles = z + .object({ + trace: z + .object({ + backgroundColor: z.string().default("#949494"), + textColor: z.string().default("#fff"), + }) + .default({}), + debug: z + .object({ + backgroundColor: z.string().default("#fe7bf3"), + textColor: z.string().default("#fff"), + }) + .default({}), + info: z + .object({ + backgroundColor: z.string().default("#65f10e"), + textColor: z.string().default("#fff"), + }) + .default({}), + warn: z + .object({ + backgroundColor: z.string().default("#faf200"), + textColor: z.string().default("#000"), + }) + .default({}), + fatal: z + .object({ + backgroundColor: z.string().default("#cc0018"), + textColor: z.string().default("#fff"), + }) + .default({}), + }) + .default({}); + +export const ConsoleOutputConfig = z.object({ + enabled: z.boolean().default(true).optional(), + style: ConsoleStyles, +}); + +export type ConsoleOutputConfig = z.infer; + +export const StrictConsoleOutputConfig = z.object({ + enabled: z.boolean().default(true), + style: StrictConsoleStyles, +}); + +export type StrictConsoleOutputConfig = z.infer< + typeof StrictConsoleOutputConfig +>; + +export const TampermonkeyOutputConfig = z + .object({ + enabled: z.boolean().default(false).optional(), + maxBuckets: z.number().default(10).optional(), + bufferCapacity: z.number().default(10000).optional(), + bucketListKey: z.string().default("bucket_index").optional(), + }) + .default({}) + .optional(); + +export type TampermonkeyOutputConfig = z.infer; + +export const StrictTampermonkeyOutputConfig = z + .object({ + enabled: z.boolean().default(true), + maxBuckets: z.number().default(10), + bufferCapacity: z.number().default(10000), + bucketListKey: z.string().default("bucket_index"), + }) + .default({}); + +export type StrictTampermonkeyOutputConfig = z.infer< + typeof StrictTampermonkeyOutputConfig +>; + +export interface TampermonkeyBucketInfo { + name: string; + size: number; + createdAt: number; +}