Files
undead-logging/mod.ts

381 lines
13 KiB
TypeScript

import chalk from "chalk";
import { setImmediate } from "node:timers";
import process from "node:process";
type Message = unknown;
type ChalkFunction = (...msgs: Message[]) => string;
type PrimitiveItems = Message[];
interface UnknownConversion<T> {
condition: (arg: Message) => boolean;
converter: (arg: T) => string;
}
/** How would you like your logs served? */
interface LoggingOptions {
logTiming?: LogTiming,
timeFormat?: TimeFormat,
bright?: boolean,
silent?: boolean
}
/** Control all log sources from this module */
const sources: Set<Logging> = new Set();
/** The first log message across all sources will not contain carriage return + newline control codes */
let first = true;
/**
* A source for pretty and cool and fun logging
*/
class Logging {
/** Can I be seen by the console and listeners? */
visible: boolean
/** What is my name? */
source: string
/** Should I use bright colors over dull ones? */
bright: boolean = true;
/** Control when logs are handled for this source - similar (defaults to) `LoggingConfiguration.logTiming` */
logTiming: LogTiming = LoggingConfiguration.logTiming;
/** Control how time is displayed for this source - similar (defaults to) `LoggingConfiguration.timeFormat` */
timeFormat: TimeFormat = LoggingConfiguration.timeFormat;
/**
* Create a logging source
* ```ts
* const log = new Logging("Main");
*
* log.i("Hello World!");
* ```
* @param source Module identifier. Used in every line to identify the module that sent the message.
* @param silent Set to false to log a message when the logger instantiates. May be useful when debugging.
* @returns A source for logging messages to the console (stdout). Functions for info, warnings, errors, debug statements, and network events are provided and have shorthands.
*/
constructor(source: string, options?: LoggingOptions) {
this.visible = true;
this.source = source;
if (options) {
if (options.bright) this.bright = options.bright;
if (typeof options.silent == 'boolean' && !options.silent) this.info(`Instantiated logging for ${this.source}`);
if (options.logTiming) this.logTiming = options.logTiming;
if (options.timeFormat) this.timeFormat = options.timeFormat;
}
sources.add(this);
}
#conversions(...msgs: PrimitiveItems) {
const conversions = [
{
condition: arg => arg instanceof Error,
converter: arg => arg.stack || arg.message
} as UnknownConversion<Error>,
{
condition: arg => arg == null,
converter: arg => JSON.stringify(arg)
} as UnknownConversion<null>,
{
condition: arg => arg == undefined,
converter: _arg => 'undefined'
} as UnknownConversion<undefined>,
{
condition: arg => arg instanceof Response,
converter: arg => {
try {
const url = arg.url.toString().length == 0 ? '(unknown origin) ' : new URL(arg.url);
const statusText = arg.statusText.length > 0 ? `${arg.statusText} ` : '';
const shouldNewline = Array.from(arg.headers.keys()).length > 0;
const entries = Array.from(arg.headers.entries());
return (
`${arg.status} ${statusText}${url}${shouldNewline ? '\n ' : ''}` +
entries.map(val => `${val[0]}: ${val[1]}`).join(`\n `)
);
} catch {
return String(arg);
}
}
} as UnknownConversion<Response>,
{
condition: arg => arg instanceof Request,
converter: arg => {
try {
const url = new URL(arg.url);
const shouldNewline = Array.from(arg.headers.keys()).length > 0;
const entries = Array.from(arg.headers.entries());
return `${arg.method} ${url}${shouldNewline ? '\n ' : ''}${entries.map(val => `${val[0]}: ${val[1]}`).join(`\n `)}`;
} catch {
return String(arg);
}
}
} as UnknownConversion<Request>,
{
condition: arg => typeof arg == 'object',
converter: arg => JSON.stringify(arg)
} as UnknownConversion<object>,
];
function convertToString(arg: Message) {
for (const conversion of conversions)
if (conversion.condition(arg))
return (conversion.converter as (arg: unknown) => string)(arg);
// gpt-4o idea cuz my brain has the `stupid` type
return String(arg); // fallback
}
return msgs.map(val => convertToString(val)).join(' ');
}
#typeStr(type: MessageType) {
switch (type) {
case MessageType.Info:
return '[INFO]';
case MessageType.Warn:
return '[WARN]';
case MessageType.Error:
return '[ERROR]';
case MessageType.Debug:
return '[DEBUG]';
case MessageType.Network:
return '[NETWORK]';
default:
return '[UNKNOWN]';
}
}
#timeStr(time: Date) {
switch (this.timeFormat) {
case TimeFormat.Unix:
return time.getTime();
case TimeFormat.Local:
return performance.now();
case TimeFormat.RoundedLocal:
return Math.round(performance.now());
default:
return time.toISOString();
}
}
/**
* Don't use this unless you're benchmarking.
*
* Constructs a message. Does not do anything with it, or even return it.
*
* Logging messages during [Deno] benchmarks causes problems.
*/
bench(type: MessageType, chalkColor: ChalkFunction, ...msgs: PrimitiveItems) {
`${chalk.gray(`${this.#timeStr(new Date())} `)} ${chalkColor(chalk.inverse(`${this.source} ${this.#typeStr(type)}`) + ` ${this.#conversions(...msgs)}`)}`;
}
#log(type: MessageType, chalkColor: ChalkFunction, ...msgs: PrimitiveItems) {
if (!this.visible) return;
const func = () => {
const time = new Date();
const str = this.#conversions(...msgs);
const msg = `${chalk.gray(this.#timeStr(time))} ${chalkColor(chalk.inverse(`${this.source} ${this.#typeStr(type)}`) + ` ${str}`)}`
process.stdout.write(`${first ? '' : '\r\n'}${msg}`);
// I don't know if promise-ifying this helps or not.
// deno-lint-ignore require-await
(async () => {
first = false;
})();
try {
LoggingListeners.emit('basic', msg);
LoggingListeners.emit('type', str, type, this.source, time);
} catch (err) {
process.stderr.write(`\r\n(FALLBACK ERROR) Callback failed! Stack: ${(err as Error).stack}`);
}
}
switch (this.logTiming) {
case LogTiming.Sync:
func();
break;
case LogTiming.Deferred:
setImmediate(func);
break;
case LogTiming.Microtask:
if (typeof queueMicrotask == 'function')
queueMicrotask(func);
else
setTimeout(func, 0); // fallback
break;
default:
func();
break;
}
}
/**
*
* @param type The kind of message to log
* @param msgs Your message(s)
*/
log(type: MessageType, ...msgs: PrimitiveItems) {
switch (type) {
case MessageType.Info:
this.info(...msgs);
break;
case MessageType.Warn:
this.warn(...msgs);
break;
case MessageType.Error:
this.error(...msgs);
break;
case MessageType.Debug:
this.debug(...msgs);
break;
case MessageType.Network:
this.network(...msgs);
break;
default:
this.info(...msgs);
break;
}
}
/** Logging Function */
i(...msgs: PrimitiveItems) { this.info(...msgs); }
/** Logging Function */
info(...msgs: PrimitiveItems) {
if (!MessageTypeVisibility.Info) return;
this.#log(MessageType.Info, chalk.whiteBright, ...msgs);
}
/** Logging Function */
w(...msgs: PrimitiveItems) { this.warn(...msgs); }
/** Logging Function */
warn(...msgs: PrimitiveItems) {
if (!MessageTypeVisibility.Warn) return;
this.#log(MessageType.Warn, chalk.yellowBright, ...msgs);
}
/** Logging Function */
e(...msgs: PrimitiveItems) { this.error(...msgs); }
/** Logging Function */
error(...msgs: PrimitiveItems) {
if (!MessageTypeVisibility.Error) return;
this.#log(MessageType.Error, chalk.redBright, ...msgs);
}
/** Logging Function */
d(...msgs: PrimitiveItems) { this.debug(...msgs); }
/** Logging Function */
debug(...msgs: PrimitiveItems) {
if (!MessageTypeVisibility.Debug) return;
this.#log(MessageType.Debug, chalk.greenBright, ...msgs);
}
/** Logging Function */
n(...msgs: PrimitiveItems) { this.network(...msgs); }
/** Logging Function */
network(...msgs: PrimitiveItems) {
if (!MessageTypeVisibility.Network) return;
this.#log(MessageType.Network, chalk.cyanBright, ...msgs);
}
}
/** Useful for conditional logging with the `log` function */
enum MessageType {
Info,
Warn,
Error,
Debug,
Network
}
/** Enable/disable logging of certain message types across all logging sources */
const MessageTypeVisibility = {
Info: true,
Warn: true,
Error: true,
Debug: true,
Network: true
}
// 'On Message' event listeners
type BasicListener = (msg: string) => unknown;
type TypeListener = (msg: string, type: MessageType, source: string, time: Date) => unknown;
type Listener = BasicListener | TypeListener;
type ListenerType = 'basic' | 'type';
const basicListeners = new Set<Listener>();
const typeListeners = new Set<Listener>();
class ListenersBase {
/** Register listener callback */
onmsg(type: 'basic', cb: BasicListener): void
onmsg(type: 'type', cb: TypeListener): void
onmsg(type: ListenerType, cb: Listener) {
if (type == 'basic') basicListeners.add(cb);
else if (type == 'type') typeListeners.add(cb);
}
/** Remove listener callback */
offmsg(type: 'basic', cb: BasicListener): void
offmsg(type: 'type', cb: TypeListener): void
offmsg(type: ListenerType, cb: Listener) {
if (type == 'basic') basicListeners.delete(cb);
else if (type == 'type') typeListeners.delete(cb);
}
/** Emit a log line. Usually not needed outside this module. */
emit(type: 'basic', msg: string, msgtype?: MessageType, source?: string, time?: Date): void
emit(type: 'type', msg: string, msgtype: MessageType, source: string, time: Date): void
emit(type: ListenerType, msg: string, msgtype: MessageType, source: string, time: Date) {
if (type == 'basic')
for (const cb of basicListeners) (cb as BasicListener)(msg);
if (type == 'type')
for (const cb of typeListeners) (cb as TypeListener)(msg, msgtype, source, time);
}
}
/**
* Add/remove callbacks: run when something is logged anywhere
*/
const LoggingListeners: ListenersBase = new ListenersBase();
/**
* Specify when, in/around the event loop, logs are sent
*/
enum LogTiming {
Sync,
Deferred,
Microtask
}
/**
* Specify the format that loggers use to display time
*/
enum TimeFormat {
Utc,
Unix,
Local,
RoundedLocal
}
class LoggingConfigurationBase {
#logTiming: LogTiming = LogTiming.Sync;
#timeFormat: TimeFormat = TimeFormat.Utc;
get timeFormat(): TimeFormat { return this.#timeFormat }
set timeFormat(data: TimeFormat) { this.#timeFormat = data }
get logTiming(): LogTiming { return this.#logTiming; }
set logTiming(data: LogTiming) { this.#logTiming = data }
set resetTimeFormat(data: TimeFormat) { for (const source of sources.values()) source.timeFormat = data; this.#timeFormat = data; }
set resetLogTiming(data: LogTiming) { for (const source of sources.values()) source.logTiming = data; this.#logTiming = data; }
}
/**
* Configure time display format and log timing here
*/
const LoggingConfiguration: LoggingConfigurationBase = new LoggingConfigurationBase();
export { MessageType, TimeFormat, LogTiming, MessageTypeVisibility, LoggingListeners, LoggingConfiguration };
export default Logging;