README fixes, new RoundedLocal time format, more local source options
* i wanted to make sure `undefined` was handled properly. seems like it is. * tested with Bun, works. Deno runs faster though, by about 1.8µs/message construction! * Removed skeletons from closet (bug fixes) * Sources can now optionally specify their own time format and/or log timing * Requests and Responses now can be converted and displayed (no bodies)
This commit is contained in:
262
mod.ts
262
mod.ts
@@ -1,14 +1,25 @@
|
||||
import chalk from "chalk";
|
||||
import { setImmediate } from "node:timers";
|
||||
import process from "node:process";
|
||||
|
||||
type ChalkFunction = (...msgs: unknown[]) => string;
|
||||
type PrimitiveItems = unknown[];
|
||||
type Message = unknown;
|
||||
type ChalkFunction = (...msgs: Message[]) => string;
|
||||
type PrimitiveItems = Message[];
|
||||
|
||||
interface UnknownConversion<T> {
|
||||
condition: (arg: unknown) => boolean;
|
||||
condition: (arg: Message) => boolean;
|
||||
converter: (arg: T) => string;
|
||||
}
|
||||
|
||||
interface WebLike {
|
||||
url?: string,
|
||||
headers?: Headers,
|
||||
method?: string
|
||||
}
|
||||
|
||||
/** 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
|
||||
*/
|
||||
@@ -21,6 +32,11 @@ class Logging {
|
||||
/** 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
|
||||
@@ -29,8 +45,8 @@ class Logging {
|
||||
* 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. Useful for debugging.
|
||||
* @returns A source for logging messages to the console. Functions for info, warnings, errors, debug statements, and network events are provided and have shorthands.
|
||||
* @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, silent?: boolean, bright?: boolean) {
|
||||
this.visible = true;
|
||||
@@ -40,81 +56,130 @@ class Logging {
|
||||
if (typeof silent == 'boolean' && !silent) this.info(`Instantiated logging for ${this.source}`);
|
||||
}
|
||||
|
||||
#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 = () => {
|
||||
|
||||
let typestr: string;
|
||||
switch (type) {
|
||||
case MessageType.Info:
|
||||
typestr = '[INFO]';
|
||||
break;
|
||||
case MessageType.Warn:
|
||||
typestr = '[WARN]';
|
||||
break;
|
||||
case MessageType.Error:
|
||||
typestr = '[ERROR]';
|
||||
break;
|
||||
case MessageType.Debug:
|
||||
typestr = '[DEBUG]';
|
||||
break;
|
||||
case MessageType.Network:
|
||||
typestr = '[NETWORK]';
|
||||
break;
|
||||
default:
|
||||
typestr = '[UNKNOWN]';
|
||||
break;
|
||||
}
|
||||
|
||||
const time = new Date();
|
||||
let timeFormatted;
|
||||
switch (LoggingConfiguration.timeFormat) {
|
||||
case TimeFormat.Unix:
|
||||
timeFormatted = time.getTime();
|
||||
break;
|
||||
case TimeFormat.Local:
|
||||
timeFormatted = performance.now();
|
||||
break;
|
||||
default:
|
||||
timeFormatted = time.toISOString();
|
||||
}
|
||||
|
||||
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 => typeof arg == 'object',
|
||||
converter: arg => JSON.stringify(arg)
|
||||
} as UnknownConversion<object>,
|
||||
];
|
||||
function convertToString(arg: unknown) {
|
||||
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
|
||||
const str = this.#conversions(...msgs);
|
||||
const msg = `${chalk.gray(this.#timeStr(time))} ${chalkColor(chalk.inverse(`${this.source} ${this.#typeStr(type)}`) + ` ${str}`)}`
|
||||
|
||||
return String(arg); // fallback
|
||||
}
|
||||
|
||||
const str = msgs.map(val => convertToString(val)).join(' ');
|
||||
const msg = chalk.gray(`${timeFormatted} `) + chalkColor(chalk.inverse(`${this.source} ${typestr}`) + ` ${str}`);
|
||||
console.log(msg);
|
||||
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) {
|
||||
console.error(`(FALLBACK ERROR) Callback failed! Stack: ${(err as Error).stack}`);
|
||||
process.stderr.write(`\r\n(FALLBACK ERROR) Callback failed! Stack: ${(err as Error).stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (LoggingConfiguration.logTiming == LogTiming.Sync) func();
|
||||
if (this.logTiming == LogTiming.Sync) func();
|
||||
else setImmediate(func);
|
||||
}
|
||||
|
||||
@@ -124,47 +189,62 @@ class Logging {
|
||||
* @param msgs Your message(s)
|
||||
*/
|
||||
log(type: MessageType, ...msgs: PrimitiveItems) {
|
||||
if (type == MessageType.Info) this.i(...msgs);
|
||||
if (type == MessageType.Warn) this.w(...msgs);
|
||||
if (type == MessageType.Error) this.e(...msgs);
|
||||
if (type == MessageType.Debug) this.d(...msgs);
|
||||
if (type == MessageType.Network) this.n(...msgs);
|
||||
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);}
|
||||
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);}
|
||||
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);}
|
||||
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);}
|
||||
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);}
|
||||
n(...msgs: PrimitiveItems) { this.network(...msgs); }
|
||||
/** Logging Function */
|
||||
network(...msgs: PrimitiveItems) {
|
||||
if (!MessageTypeVisibility.Network) return;
|
||||
@@ -201,13 +281,15 @@ 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) {
|
||||
@@ -215,25 +297,36 @@ class ListenersBase {
|
||||
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')
|
||||
if (type == 'basic')
|
||||
for (const cb of basicListeners) (cb as BasicListener)(msg);
|
||||
if (type == 'type')
|
||||
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
|
||||
}
|
||||
/**
|
||||
* Specify the format that loggers use to display time
|
||||
*/
|
||||
enum TimeFormat {
|
||||
Utc,
|
||||
Unix,
|
||||
Local
|
||||
Local,
|
||||
RoundedLocal
|
||||
}
|
||||
|
||||
class LoggingConfigurationBase {
|
||||
@@ -249,6 +342,9 @@ class LoggingConfigurationBase {
|
||||
set logTiming(data: LogTiming) { this.#timing = data }
|
||||
|
||||
}
|
||||
/**
|
||||
* Configure time display format and log timing here
|
||||
*/
|
||||
const LoggingConfiguration: LoggingConfigurationBase = new LoggingConfigurationBase();
|
||||
|
||||
export { MessageType, TimeFormat, LogTiming, MessageTypeVisibility, LoggingListeners, LoggingConfiguration };
|
||||
|
||||
Reference in New Issue
Block a user