diff --git a/src/LoggerClient.ts b/src/LoggerClient.ts index c0ba04f..5dbf102 100644 --- a/src/LoggerClient.ts +++ b/src/LoggerClient.ts @@ -1,13 +1,20 @@ import Defaults, { LoggerClientOptions } from './Defaults.js'; import { inspect } from 'node:util'; -import { LogFunction, WriteOptions } from './Types.js'; +import { LogFunction, Loggable, WriteOptions } from './Types.js'; import { Logger } from './LoggerInterface.js'; -import { makePlainError } from './Shared.js'; +import { isWriteOptions, makePlainError } from './Shared.js'; type TransportOptions = { - type: string, labels?: string[] -} +} & WriteOptions + +const validKeys = [ 'labels' ]; +const isTransportOpts = (obj: object): obj is TransportOptions => +{ + const isWriteOption = isWriteOptions(obj); + const keys = Object.keys(obj); + return isWriteOption || keys.some(key => validKeys.includes(key)); +}; class LoggerClient implements Logger { @@ -45,11 +52,11 @@ class LoggerClient implements Logger if (typeof this.#_logLevelMapping[type] === 'undefined') throw new Error(`Missing logLevelMapping for type ${type}`); Object.defineProperty(this, type, { - value: (msg: string, o?: WriteOptions) => - { - const { labels = [], ...writeOpts } = o ?? {}; - this.#transport(msg, { ...writeOpts, type, labels: [ ...this.#labels, ...labels ] }); - } + value: (...rest: [...entries: Loggable[], options: TransportOptions]) => this.#transport(type, ...rest) + // { + // const { labels = [], ...writeOpts } = o ?? {}; + // this.#transport(msg, { ...writeOpts, type, labels: [ ...this.#labels, ...labels ] }); + // } }); } } @@ -74,50 +81,64 @@ class LoggerClient implements Logger throw new Error(`Invalid log level type, expected string or number, got ${typeof level}`); } - #transport (message: string | object | Error, opts: TransportOptions) + #transport (type = 'info', ...args: [...entries: Loggable[], options: TransportOptions]) { - - if (this.#_logLevelMapping[opts.type] < this.#_logLevel) + if (this.#_logLevelMapping[type] < this.#_logLevel) return; - if (message instanceof Error) - message = makePlainError(message); - - if (typeof message !== 'string') - message = inspect(message); + const last = args[args.length - 1]; + let opts: TransportOptions = {}; + if (typeof last === 'object' && isTransportOpts(last)) + { + opts = last; + args.pop(); + } + opts.labels = opts.labels ? [ ...opts.labels, ...this.#labels ] : this.#labels; + + let message = ''; + for (const entry of args as Loggable[]) + { + if (entry instanceof Error) + message += inspect(makePlainError(entry)) + ' '; + else if (typeof entry === 'string' || typeof entry === 'number') + message += entry + ' '; + else + message += inspect(entry) + ' '; + } + const spacer = ' '.repeat(LoggerClient.MaxChars - this.#_name.length); const header = `${`[${this.#_name.substring(0, LoggerClient.MaxChars)}]${spacer}`} `; if (!process.send || !process.connected) throw new Error('Missing connection to master proces'); else - process.send({ [this.#_guard]: true, header, message, ...opts }); + process.send({ [this.#_guard]: true, type, header, message, ...opts }); } // These methods are dynamically implemented by the constructor, simply here to provide IDE hints // eslint-disable-next-line @typescript-eslint/no-unused-vars - error (_str: string | object | Error, _opts?: WriteOptions): void + error (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - warn (_str: string | object | Error, _opts?: WriteOptions): void + warn (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - status (_str: string | object | Error, _opts?: WriteOptions): void + status (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - info (_str: string | object | Error, _opts?: WriteOptions): void + info (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - debug (_str: string | object | Error, _opts?: WriteOptions): void + debug (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } diff --git a/src/LoggerInterface.ts b/src/LoggerInterface.ts index 75a377a..0e3b8ae 100644 --- a/src/LoggerInterface.ts +++ b/src/LoggerInterface.ts @@ -1,9 +1,9 @@ -import { WriteOptions } from './Types'; +import { Loggable, WriteOptions } from './Types'; export interface Logger { - error(str: string, opts?: WriteOptions): void - warn(str: string, opts?: WriteOptions): void - status(str: string, opts?: WriteOptions): void - info(str: string, opts?: WriteOptions): void - debug(str: string, opts?: WriteOptions): void + error(...args: [...entries: Loggable[], options: WriteOptions]): void + warn(...args: [...entries: Loggable[], options: WriteOptions]): void + status(...args: [...entries: Loggable[], options: WriteOptions]): void + info(...args: [...entries: Loggable[], options: WriteOptions]): void + debug(...args: [...entries: Loggable[], options: WriteOptions]): void } \ No newline at end of file diff --git a/src/MasterLogger.ts b/src/MasterLogger.ts index 24f44c1..a524cd0 100644 --- a/src/MasterLogger.ts +++ b/src/MasterLogger.ts @@ -10,10 +10,10 @@ import { inspect } from 'node:util'; // Own import DiscordWebhook from '@navy.gif/discord-webhook'; import Defaults, { LogLevel } from './Defaults.js'; -import { IPCMessage, LogFunction, Shard, WriteOptions } from './Types.js'; +import { IPCMessage, LogFunction, Loggable, Shard, WriteOptions } from './Types.js'; import { addLogLevel } from '../index.js'; import { Logger } from './LoggerInterface.js'; -import { makePlainError } from './Shared.js'; +import { isWriteOptions, makePlainError } from './Shared.js'; const DAY = 1000 * 60 * 60 * 24; @@ -85,7 +85,8 @@ class MasterLogger implements Logger if (typeof this.#_logLevelMapping[type] === 'undefined') throw new Error(`Missing logLevelMapping for type ${type}`); Object.defineProperty(this, type, { - value: (msg: string, opts?: WriteOptions) => this.write(type, msg, opts) + // value: (msg: string, opts?: WriteOptions) => this.write(type, msg, opts) + value: (...rest: [...entries: Loggable[], options: WriteOptions]) => this.write(type, ...rest) }); } this.#colours = { ...Defaults.Colours, ...customColours }; @@ -207,17 +208,31 @@ class MasterLogger implements Logger }); } - write (type = 'info', text: string | object | Error, { subheader = '', shard, broadcast = false, labels = [] }: WriteOptions = {}) + write (type = 'info', ...args: [...entries: Loggable[], options: WriteOptions]) { + const last = args[args.length - 1]; + let { subheader = '', shard, broadcast = false, labels = [] }: WriteOptions = {}; + if (typeof last === 'object' && isWriteOptions(last)) + { + ({ subheader = '', shard, broadcast = false, labels =[] } = last); + args.pop(); + } + let colour = this.#colourFuncs[type]; if (!colour) colour = this.#colourFuncs.info; - if (text instanceof Error) - text = makePlainError(text); - - if (typeof text !== 'string') - text = inspect(text); + let text = ''; + for (const entry of args as Loggable[]) + { + if (entry instanceof Error) + text += inspect(makePlainError(entry)) + ' '; + else if (typeof entry === 'string' || typeof entry === 'number') + text += entry + ' '; + else + text += inspect(text) + ' '; + } + text = text.trim(); const header = `[${this.date}] [${this._shard(shard)}]`; const maxChars = Math.max(...this.#types.map(t => t.length)); @@ -233,7 +248,7 @@ class MasterLogger implements Logger } if ((broadcast || (this.#_broadcastLevel <= this.#_logLevelMapping[type])) && this.#webhook) { - const description = (subheader.length ? `**${subheader}**: ${process.env.NODE_ENV ?? 'production'}\n` : '') + `\`\`\`${text}\`\`\``; + const description = (subheader.length ? `**${subheader.trim()}**: ${process.env.NODE_ENV ?? 'production'}\n` : '') + `\`\`\`${text}\`\`\``; this.#webhook.send({ embeds: [{ title: `[__${type.toUpperCase()}__] ${this._shard(shard)}`, @@ -303,27 +318,27 @@ class MasterLogger implements Logger // These methods are dynamically implemented by the constructor // eslint-disable-next-line @typescript-eslint/no-unused-vars - error (_str: string | object | Error, _opts?: WriteOptions): void + error (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - warn (_str: string | object | Error, _opts?: WriteOptions): void + warn (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - status (_str: string | object | Error, _opts?: WriteOptions): void + status (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - info (_str: string | object | Error, _opts?: WriteOptions): void + info (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - debug (_str: string | object | Error, _opts?: WriteOptions): void + debug (..._args: [...entries: Loggable[], options: WriteOptions]): void { throw new Error('Method not implemented.'); } diff --git a/src/Shared.ts b/src/Shared.ts index a0a1b59..97ce379 100644 --- a/src/Shared.ts +++ b/src/Shared.ts @@ -1,3 +1,5 @@ +import { WriteOptions } from './Types'; + export const makePlainError = (err: Error) => { return { @@ -5,4 +7,16 @@ export const makePlainError = (err: Error) => message: err.message, stack: err.stack }; +}; + +const validKeys = [ 'subheader', 'shard', 'broadcast', 'labels' ]; +export const isWriteOptions = (obj: object, extended = false): obj is WriteOptions => +{ + const keys = Object.keys(obj); + // Check for invalid keys, in some cases an arbitrary object might share keys + // while still allowing for an option to be extended + if (!extended && keys.some(key => !validKeys.includes(key))) + return false; + // Make sure it's not an empty object + return keys.some(key => validKeys.includes(key)); }; \ No newline at end of file diff --git a/src/Types.ts b/src/Types.ts index a810ddf..186ea60 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -23,4 +23,6 @@ type IPCMessage = { type LogFunction = (str: string, opts?: WriteOptions) => void -export { WriteOptions, Shard, IPCMessage, LogFunction }; \ No newline at end of file +type Loggable = string | number | object | Error + +export { WriteOptions, Shard, IPCMessage, LogFunction, Loggable }; \ No newline at end of file