breaking change. it works better.
This commit is contained in:
375
src/mod.ts
Normal file
375
src/mod.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import { setImmediate } from "node:timers";
|
||||
import process from "node:process";
|
||||
import type { Conversion, LoggingOptions, Message } from "./types.ts";
|
||||
import { TimeFormat, MessageType, LogTiming } from "./types.ts";
|
||||
import * as ANSI from "@neabyte/deno-ansi";
|
||||
|
||||
/**
|
||||
* A source for pretty and cool and fun logging
|
||||
*/
|
||||
class Logging {
|
||||
|
||||
/**
|
||||
* Gets the line ending for this platform.
|
||||
*/
|
||||
static getNewline() {
|
||||
return Deno.build.os === 'windows' ? '\r\n' : '\n'
|
||||
}
|
||||
|
||||
static timeStr(format: TimeFormat, time: Date) {
|
||||
switch (format) {
|
||||
case TimeFormat.None:
|
||||
return '';
|
||||
case TimeFormat.Unix:
|
||||
return `${time.getTime()} `;
|
||||
case TimeFormat.Local:
|
||||
return `${performance.now()} `;
|
||||
case TimeFormat.RoundedLocal:
|
||||
return `${Math.round(performance.now())} `;
|
||||
case TimeFormat.Utc:
|
||||
return `${time.toISOString()} `;
|
||||
}
|
||||
}
|
||||
static 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]';
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
/** Delimiter between message arguments, default to space */
|
||||
join: string = ' ';
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
LoggingConfiguration.sources.add(this);
|
||||
}
|
||||
|
||||
async #conversions(...msgs: Message[]) {
|
||||
return await Promise.all<string>(msgs.map(async msg => {
|
||||
for (const converter of LoggingConfiguration.getConversions()
|
||||
.values()
|
||||
.toArray()
|
||||
.sort((a, b) => -(typeof a.priority == 'undefined' ? 0 : a.priority) + (typeof b.priority == 'undefined' ? 0 : b.priority))
|
||||
)
|
||||
// condition may or may not be async
|
||||
if (await Promise.resolve(converter.condition(msg))) return await converter.converter(msg);
|
||||
|
||||
return String(msg); // fallback to JS string
|
||||
}));
|
||||
}
|
||||
|
||||
#log(type: MessageType, ...msgs: Message[]) {
|
||||
if (!this.visible) return;
|
||||
const func = async () => {
|
||||
|
||||
const time = new Date();
|
||||
|
||||
const str = (await this.#conversions(...msgs)).join(this.join);
|
||||
|
||||
const timeFormatted = ANSI.Colors.dim(Logging.timeStr(this.timeFormat, time));
|
||||
|
||||
let color: number;
|
||||
switch (type) {
|
||||
case MessageType.Info:
|
||||
color = this.bright ? ANSI.Colors.BRIGHT_WHITE : ANSI.Colors.WHITE;
|
||||
break;
|
||||
case MessageType.Warn:
|
||||
color = this.bright ? ANSI.Colors.BRIGHT_YELLOW : ANSI.Colors.YELLOW;
|
||||
break;
|
||||
case MessageType.Error:
|
||||
color = this.bright ? ANSI.Colors.BRIGHT_RED : ANSI.Colors.RED;
|
||||
break;
|
||||
case MessageType.Debug:
|
||||
color = this.bright ? ANSI.Colors.BRIGHT_GREEN : ANSI.Colors.GREEN;
|
||||
break;
|
||||
case MessageType.Network:
|
||||
color = this.bright ? ANSI.Colors.BRIGHT_CYAN : ANSI.Colors.CYAN;
|
||||
break;
|
||||
}
|
||||
const targetStr = `${ANSI.Colors.inverse(ANSI.Colors.fg(`${this.source} ${Logging.typeStr(type)}`, color))} ${ANSI.Colors.fg(str, color)}`;
|
||||
const msg = `${timeFormatted}${targetStr}`;
|
||||
|
||||
process.stdout.write(`${msg}${Logging.getNewline()}`);
|
||||
|
||||
try {
|
||||
LoggingListeners.emit('basic', msg);
|
||||
LoggingListeners.emit('type', str, type, this.source, time);
|
||||
} catch (err) {
|
||||
process.stderr.write(`(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: Message[]) {
|
||||
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: Message[]) { this.info(...msgs); }
|
||||
/** Logging Function */
|
||||
info(...msgs: Message[]) {
|
||||
if (!LoggingConfiguration.MessageTypeVisibility.Info) return;
|
||||
this.#log(MessageType.Info, ...msgs);
|
||||
}
|
||||
|
||||
/** Logging Function */
|
||||
w(...msgs: Message[]) { this.warn(...msgs); }
|
||||
/** Logging Function */
|
||||
warn(...msgs: Message[]) {
|
||||
if (!LoggingConfiguration.MessageTypeVisibility.Warn) return;
|
||||
this.#log(MessageType.Warn, ...msgs);
|
||||
}
|
||||
|
||||
/** Logging Function */
|
||||
e(...msgs: Message[]) { this.error(...msgs); }
|
||||
/** Logging Function */
|
||||
error(...msgs: Message[]) {
|
||||
if (!LoggingConfiguration.MessageTypeVisibility.Error) return;
|
||||
this.#log(MessageType.Error, ...msgs);
|
||||
}
|
||||
|
||||
/** Logging Function */
|
||||
d(...msgs: Message[]) { this.debug(...msgs); }
|
||||
/** Logging Function */
|
||||
debug(...msgs: Message[]) {
|
||||
if (!LoggingConfiguration.MessageTypeVisibility.Debug) return;
|
||||
this.#log(MessageType.Debug, ...msgs);
|
||||
}
|
||||
|
||||
/** Logging Function */
|
||||
n(...msgs: Message[]) { this.network(...msgs); }
|
||||
/** Logging Function */
|
||||
network(...msgs: Message[]) {
|
||||
if (!LoggingConfiguration.MessageTypeVisibility.Network) return;
|
||||
this.#log(MessageType.Network, ...msgs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// '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';
|
||||
|
||||
export class ListenersBase {
|
||||
|
||||
listeners = new Set<{ cb: Listener, type: ListenerType }>();
|
||||
|
||||
/** Register listener callback */
|
||||
onmsg(type: 'basic', cb: BasicListener): void
|
||||
onmsg(type: 'type', cb: TypeListener): void
|
||||
onmsg(type: ListenerType, cb: Listener) {
|
||||
if (type == 'basic') this.listeners.add({ cb, type: "basic" });
|
||||
else if (type == 'type') this.listeners.add({ cb, type: "type" });
|
||||
}
|
||||
|
||||
/** Remove listener callback */
|
||||
offmsg(type: 'basic', cb: BasicListener): void
|
||||
offmsg(type: 'type', cb: TypeListener): void
|
||||
offmsg(type: ListenerType, cb: Listener) {
|
||||
if (type == 'basic') this.listeners.delete({ cb, type: "basic" });
|
||||
else if (type == 'type') this.listeners.delete({ cb, type: "type" });
|
||||
}
|
||||
|
||||
/** Emit a log line */
|
||||
emit(type: 'basic', msg: string): 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 this.listeners.values().filter(listener => listener.type == 'basic')) (cb.cb as BasicListener)(msg);
|
||||
if (type == 'type') {
|
||||
if ([msgtype, source, time].some(val => typeof val == 'undefined')) throw new Error("Missing arguments for type log emission");
|
||||
for (const cb of this.listeners.values().filter(listener => listener.type == "type")) (cb.cb as TypeListener)(msg, msgtype!, source!, time!);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Add/remove callbacks: run when something is logged anywhere
|
||||
*/
|
||||
const LoggingListeners: ListenersBase = new ListenersBase();
|
||||
|
||||
class LoggingConfigurationBase {
|
||||
|
||||
#logTiming: LogTiming = LogTiming.Sync;
|
||||
|
||||
#timeFormat: TimeFormat = TimeFormat.Utc;
|
||||
|
||||
sources: Set<Logging> = new Set();
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
#conversions: Set<Conversion<any>> = new Set(); // ¯\_(ツ)_/¯
|
||||
|
||||
constructor() {
|
||||
|
||||
this.addConversion<Error>({
|
||||
condition: arg => arg instanceof Error,
|
||||
converter: arg => arg.stack || arg.message
|
||||
});
|
||||
this.addConversion<null>({
|
||||
condition: arg => arg == null,
|
||||
converter: () => `null`
|
||||
});
|
||||
this.addConversion<undefined>({
|
||||
condition: arg => arg == undefined,
|
||||
converter: () => `undefined`
|
||||
});
|
||||
this.addConversion<Response>({
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.addConversion<Request>({
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
clearConversions() {
|
||||
this.#conversions.clear();
|
||||
}
|
||||
addConversion<T>(con: Conversion<T>) {
|
||||
this.#conversions.add(con);
|
||||
}
|
||||
getConversions() {
|
||||
return this.#conversions;
|
||||
}
|
||||
|
||||
getAllLoggers() {
|
||||
return this.sources.values();
|
||||
}
|
||||
|
||||
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 this.sources.values()) source.timeFormat = data; this.#timeFormat = data; }
|
||||
set resetLogTiming(data: LogTiming) { for (const source of this.sources.values()) source.logTiming = data; this.#logTiming = data; }
|
||||
|
||||
/** Enable/disable logging of certain message types across all logging sources */
|
||||
MessageTypeVisibility = {
|
||||
Info: true,
|
||||
Warn: true,
|
||||
Error: true,
|
||||
Debug: true,
|
||||
Network: true
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Configure global logging configuration, such as converters and visibility
|
||||
*/
|
||||
const LoggingConfiguration: LoggingConfigurationBase = new LoggingConfigurationBase();
|
||||
|
||||
export { MessageType, TimeFormat, LogTiming, LoggingListeners, LoggingConfiguration };
|
||||
export default Logging;
|
||||
74
src/types.ts
Normal file
74
src/types.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Specify when, in/around the event loop, logs are sent
|
||||
*/
|
||||
export enum LogTiming {
|
||||
Sync,
|
||||
Deferred,
|
||||
Microtask
|
||||
}
|
||||
/**
|
||||
* Specify the format that loggers use to display time
|
||||
*/
|
||||
export enum TimeFormat {
|
||||
None,
|
||||
Utc,
|
||||
Unix,
|
||||
Local,
|
||||
RoundedLocal
|
||||
}
|
||||
|
||||
// 'On Message' event listeners
|
||||
export type BasicListener = (msg: string) => unknown;
|
||||
export type TypeListener = (msg: string, type: MessageType, source: string, time: Date) => unknown;
|
||||
export type Listener = BasicListener | TypeListener;
|
||||
export type ListenerType = 'basic' | 'type';
|
||||
|
||||
/** Useful for conditional logging with the `log` function */
|
||||
export enum MessageType {
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Debug,
|
||||
Network
|
||||
}
|
||||
|
||||
/** Type for any object given to a logging function. Not really much different from `any`. */
|
||||
export type Message = null | undefined | boolean | number | bigint | string | symbol | object;
|
||||
|
||||
/** Convert objects of any certain kind of value to a string. */
|
||||
export interface Conversion<T = Message> {
|
||||
/**
|
||||
* Given a message value, determine whether this converter is capable of converting to a string or not.
|
||||
*
|
||||
* Promises are honored.
|
||||
*/
|
||||
condition: (arg: unknown) => Promise<boolean> | boolean,
|
||||
/**
|
||||
* Runtime type of object is guaranteed when the condition passes.
|
||||
*
|
||||
* Promises are honored.
|
||||
*/
|
||||
converter: (arg: T) => Promise<string> | string,
|
||||
/**
|
||||
* Defaults to 0.
|
||||
*
|
||||
* Since converters are checked sequentially in order of priority, generic converters should have higher priority.
|
||||
*
|
||||
* Converters with a priority closer to 0 have a higher priority.
|
||||
*
|
||||
* For example, when:
|
||||
* * a converter that converts `Error` with priority 39, and
|
||||
* * a converter that converts `CustomError` with priority 38,
|
||||
*
|
||||
* priority 1 will be resolved first to determine converter compatibility with the message value.
|
||||
*/
|
||||
priority?: number
|
||||
}
|
||||
|
||||
/** How would you like your logs served? */
|
||||
export interface LoggingOptions {
|
||||
logTiming?: LogTiming,
|
||||
timeFormat?: TimeFormat,
|
||||
bright?: boolean,
|
||||
silent?: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user