Compare commits

...

2 Commits

15 changed files with 638 additions and 813 deletions

109
README.md
View File

@@ -1,5 +1,5 @@
# Undead Logging # Undead Logging
Logging for stupid idiots like me Opinionated logging
* Pluggable event listeners * Pluggable event listeners
* Per-source hushing, global log type hushing * Per-source hushing, global log type hushing
@@ -39,10 +39,10 @@ log.n("Something happened");
Disable a type of message globally: Disable a type of message globally:
```ts ```ts
import Logging, { MessageTypeVisibility } from "@proxnet/undead-logging"; import Logging, { LoggingConfiguration } from "@proxnet/undead-logging";
// .. logging source "Main" is in the scope // .. logging source "Main" is in the scope
MessageTypeVisibility.Error = false; LoggingConfiguration.MessageTypeVisibility.Error = false;
// somewhere else, could be a different script // somewhere else, could be a different script
log.i("I am visible"); log.i("I am visible");
@@ -87,6 +87,8 @@ LoggingListeners.offmsg('basic', cb);
## Time display modes ## Time display modes
You can display four different formats for time: You can display four different formats for time:
* No time display
- Can be useful when running a service that already logs time and date, such as systemd services
* UTC * UTC
* Unix time (in milliseconds) * Unix time (in milliseconds)
* Local [process] time (in milliseconds, from the `performance` API) * Local [process] time (in milliseconds, from the `performance` API)
@@ -99,6 +101,8 @@ import { LoggingConfiguration, TimeFormat } from "@proxnet/undead-logging";
LoggingConfiguration.timeFormat = TimeFormat.Local; LoggingConfiguration.timeFormat = TimeFormat.Local;
// or // or
LoggingConfiguration.timeFormat = TimeFormat.Utc; LoggingConfiguration.timeFormat = TimeFormat.Utc;
// or even
LoggingConfiguration.timeFormat = TimeFormat.None; // removes the time and date section of the log line entirely
``` ```
## (advanced) Logging timing ## (advanced) Logging timing
@@ -118,10 +122,10 @@ You might also prefer to reset all sources to synchronous logging during app shu
This can help debug issues with how your app closes. (see example below) This can help debug issues with how your app closes. (see example below)
## Global resets ## Global resets
Every source is tracked by this module in a `Set`. Every source is tracked by `LoggingConfiguration` in a `Set`.
You can reset every source's log timing or time format with `LoggingConfiguration.reset(something)`.<br> You can reset every source's log timing or time format with `LoggingConfiguration.resetX`.<br>
The corresponding property will be updated on `LoggingConfiguration`: The corresponding property will be updated on all instances of `Logging`:
```ts ```ts
import Logging, { LoggingConfiguration, LogTiming } from "@proxnet/undead-logging"; import Logging, { LoggingConfiguration, LogTiming } from "@proxnet/undead-logging";
@@ -149,7 +153,98 @@ Deno.addSignalListener('SIGINT', () => {
}); });
``` ```
## [Benchmarks](./BENCH.md) ## Class/Object Conversion
Any type of object can be given to a logging function. "[object Object]" may not always be desirable.
You can automatically convert any object to a string of any format using a `Conversion<T>`:
```ts
class CustomClass {
foo: string;
bar: number;
constructor(foo: string, bar: number) {
this.foo = foo;
this.bar = bar;
}
}
LoggingConfiguration.addConversion<CustomClass>({
condition: arg => arg instanceof CustomClass,
converter: arg => `CustomClass; foo:${arg.foo}; bar:${arg.bar};`
});
/*
log an instance of `CustomClass`:
2026-01-04T23:56:49.156Z ConvertTest [DEBUG] CustomClass; foo:baz; bar:39;
*/
```
Promises are honored when using them in the `condition` and/or the `converter`.
<br>As such, you can use your own async in the conversion should you ever want to.
Some classes and objects are converted by default, such as `Response` and `Request` standard APIs.
<br>They are demonstrated in `tests/convert.ts`, though they appear as:
```
2026-01-04T23:56:48.965Z Web [INFO] GET http://example.com/?hello=world
key1: value1
key2: value2
2026-01-04T23:56:49.156Z Web [INFO] 200 OK https://example.com/
age: 8217
...
server: cloudflare
vary: Accept-Encoding
```
You can remove these default conversions using `LoggingConfiguration.clearConversions()` before your program starts.
### Converter priority
In semi-rare cases, you may want to prefer using some converters over others,<br>such as converting custom `Error`s rather than the base `Error` class.
You can configure converter *priority* using `Conversion.priority`:
```ts
class CustomError extends Error {
someProperty: string
constructor(someProp: string) {
super("Something went wrong.");
this.someProperty = someProp;
}
}
LoggingConfiguration.clearConversions();
LoggingConfiguration.addConversion<CustomError>({
condition: arg => arg instanceof CustomError,
converter: arg => `CustomError: someProperty:${arg.someProperty}; ${arg.stack || arg.message}`,
priority: -1
});
LoggingConfiguration.addConversion<Error>({
condition: arg => arg instanceof Error,
converter: arg => `${arg.stack || arg.message}`,
priority: 1
});
/*
CustomError at 1, Error at -1:
2026-01-05T00:09:18.176Z PriorityTest [INFO] CustomError: someProperty:'Hello World!'; Error: Something went wrong.
at file:///C:/Users/zombieb/undead-logging/tests/priority.ts:28:7
*/
/*
CustomError at -1, Error at 1:
2026-01-05T00:09:34.040Z PriorityTest [INFO] Error: Something went wrong.
at file:///C:/Users/zombieb/undead-logging/tests/priority.ts:28:7
*/
```
Converters with numbers closer to `Infinity` have a higher priority. As such, you can choose your numerical layout for converter priority.
## Contributing ## Contributing
create an account on gitea.proxnet.dev, fork, then PR create an account on gitea.proxnet.dev, fork, then PR

View File

@@ -1,64 +0,0 @@
import chalk from "chalk";
import Logging, { MessageType } from "../mod.ts";
interface BenchStats {
avg: number,
med: number,
rng: number,
min: number,
max: number,
}
function createBench(n: number) {
const log = new Logging("Bench");
const data: number[] = [];
for (let i = 0; i < 30; i++) {
const last = performance.now();
for (let i = 0; i < n; i++)
log.bench(MessageType.Info, chalk.black, 'a');
data.push(performance.now() - last);
}
const sorted = [...data].sort((a, b) => a - b);
return {
n,
avg: data.reduce((sum, val) => sum + val, 0) / data.length,
med: sorted.length % 2 === 0
? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
: sorted[Math.floor(sorted.length / 2)],
rng: Math.max(...data) - Math.min(...data),
min: Math.min(...data),
max: Math.max(...data)
} as BenchStats;
}
const benches = [
createBench(1),
createBench(100),
createBench(1000),
createBench(10000),
createBench(100000),
];
function trimNumber(n: number) {
const split = n.toString().split('.');
if (!split[1]) return n;
else if (split[1].length > 4) return `${split[0]}.${split[1].substring(0, 4)}`;
else if (split[1].length == 0) return n.toString();
else return split.join('.');
}
for (const stats of benches) {
for (const value of Object.values(stats)) {
const label = value < 1 ? 'µs' : 'ms';
const val = value < 1 ? value * 1000 : value;
const i = Object.values(stats).indexOf(value);
const key = Object.keys(stats).find((_val, index) => index == i);
console.log(`${key}: ${trimNumber(val)}${key !== 'n' ? label : ''}`);
}
console.log('');
}

View File

@@ -1,227 +0,0 @@
{
"version": 1,
"runtime": "Deno/2.4.1 x86_64-pc-windows-msvc",
"cpu": "unknown",
"benches": [
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-sync",
"name": "1 Log",
"baseline": false,
"results": [
{
"ok": {
"n": 42,
"min": 1547.66,
"max": 1867.93,
"avg": 1596.3097619047621,
"p75": 1602.51,
"p99": 1867.93,
"p995": 1867.93,
"p999": 1867.93,
"highPrecision": false,
"usedExplicitTimers": false
}
}
]
},
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-sync",
"name": "100 Logs",
"baseline": false,
"results": [
{
"ok": {
"n": 3227,
"min": 149200.0,
"max": 277100.0,
"avg": 155426.0,
"p75": 151700.0,
"p99": 230500.0,
"p995": 233300.0,
"p999": 250500.0,
"highPrecision": true,
"usedExplicitTimers": false
}
}
]
},
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-sync",
"name": "1k Logs",
"baseline": false,
"results": [
{
"ok": {
"n": 332,
"min": 1492200.0,
"max": 2041200.0,
"avg": 1553763.0,
"p75": 1581400.0,
"p99": 1678900.0,
"p995": 1847700.0,
"p999": 2041200.0,
"highPrecision": true,
"usedExplicitTimers": false
}
}
]
},
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-sync",
"name": "10k Logs",
"baseline": false,
"results": [
{
"ok": {
"n": 43,
"min": 15433100.0,
"max": 15875500.0,
"avg": 15512717.0,
"p75": 15516000.0,
"p99": 15875500.0,
"p995": 15875500.0,
"p999": 15875500.0,
"highPrecision": true,
"usedExplicitTimers": false
}
}
]
},
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-sync",
"name": "100k Logs",
"baseline": false,
"results": [
{
"ok": {
"n": 14,
"min": 154448800.0,
"max": 162192300.0,
"avg": 156260422.0,
"p75": 156585200.0,
"p99": 162192300.0,
"p995": 162192300.0,
"p999": 162192300.0,
"highPrecision": true,
"usedExplicitTimers": false
}
}
]
},
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-deferred",
"name": "1 Log",
"baseline": false,
"results": [
{
"ok": {
"n": 42,
"min": 1553.59,
"max": 1623.61,
"avg": 1581.7592857142856,
"p75": 1594.76,
"p99": 1623.61,
"p995": 1623.61,
"p999": 1623.61,
"highPrecision": false,
"usedExplicitTimers": false
}
}
]
},
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-deferred",
"name": "100 Logs",
"baseline": false,
"results": [
{
"ok": {
"n": 3205,
"min": 148800.0,
"max": 357900.0,
"avg": 156507.0,
"p75": 152200.0,
"p99": 233300.0,
"p995": 241400.0,
"p999": 273500.0,
"highPrecision": true,
"usedExplicitTimers": false
}
}
]
},
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-deferred",
"name": "1k Logs",
"baseline": false,
"results": [
{
"ok": {
"n": 330,
"min": 1504500.0,
"max": 1744900.0,
"avg": 1564083.0,
"p75": 1595100.0,
"p99": 1668200.0,
"p995": 1732400.0,
"p999": 1744900.0,
"highPrecision": true,
"usedExplicitTimers": false
}
}
]
},
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-deferred",
"name": "10k Logs",
"baseline": false,
"results": [
{
"ok": {
"n": 43,
"min": 15435800.0,
"max": 15980800.0,
"avg": 15641247.0,
"p75": 15771900.0,
"p99": 15980800.0,
"p995": 15980800.0,
"p999": 15980800.0,
"highPrecision": true,
"usedExplicitTimers": false
}
}
]
},
{
"origin": "file:///path/to/undead-logging/benchmark/deno.ts",
"group": "timing-deferred",
"name": "100k Logs",
"baseline": false,
"results": [
{
"ok": {
"n": 14,
"min": 154775900.0,
"max": 159622500.0,
"avg": 156075222.0,
"p75": 156708400.0,
"p99": 159622500.0,
"p995": 159622500.0,
"p999": 159622500.0,
"highPrecision": true,
"usedExplicitTimers": false
}
}
]
}
]
}

View File

@@ -1,109 +0,0 @@
import Logging, { LogTiming, MessageType } from "@proxnet/undead-logging";
import chalk from "chalk";
// SYNC
Deno.bench({
name: "1 Log",
group: "timing-sync",
fn: () => {
const log = new Logging("Bench");
log.bench(MessageType.Info, chalk.black, 'a');
}
});
Deno.bench({
name: "100 Logs",
group: "timing-sync",
fn: () => {
const log = new Logging("Bench");
for (let i = 0; i < 100; i++)
log.bench(MessageType.Info, chalk.black, 'a');
}
});
Deno.bench({
name: "1k Logs",
group: "timing-sync",
fn: () => {
const log = new Logging("Bench");
for (let i = 0; i < 1_000; i++)
log.bench(MessageType.Info, chalk.black, 'a');
}
});
Deno.bench({
name: "10k Logs",
group: "timing-sync",
fn: () => {
const log = new Logging("Bench");
for (let i = 0; i < 10_000; i++)
log.bench(MessageType.Info, chalk.black, 'a');
}
});
Deno.bench({
name: "100k Logs",
group: "timing-sync",
fn: () => {
const log = new Logging("Bench");
log.logTiming = LogTiming.Deferred;
for (let i = 0; i < 100_000; i++)
log.bench(MessageType.Info, chalk.black, 'a');
}
});
// DEFERRED
Deno.bench({
name: "1 Log",
group: "timing-deferred",
fn: () => {
const log = new Logging("Bench");
log.logTiming = LogTiming.Deferred;
log.bench(MessageType.Info, chalk.black, 'a');
}
});
Deno.bench({
name: "100 Logs",
group: "timing-deferred",
fn: () => {
const log = new Logging("Bench");
log.logTiming = LogTiming.Deferred;
for (let i = 0; i < 100; i++)
log.bench(MessageType.Info, chalk.black, 'a');
}
});
Deno.bench({
name: "1k Logs",
group: "timing-deferred",
fn: () => {
const log = new Logging("Bench");
log.logTiming = LogTiming.Deferred;
for (let i = 0; i < 1_000; i++)
log.bench(MessageType.Info, chalk.black, 'a');
}
});
Deno.bench({
name: "10k Logs",
group: "timing-deferred",
fn: () => {
const log = new Logging("Bench");
log.logTiming = LogTiming.Deferred;
for (let i = 0; i < 10_000; i++)
log.bench(MessageType.Info, chalk.black, 'a');
}
});
Deno.bench({
name: "100k Logs",
group: "timing-deferred",
fn: () => {
const log = new Logging("Bench");
for (let i = 0; i < 100_000; i++)
log.bench(MessageType.Info, chalk.black, 'a');
}
});

View File

@@ -7,14 +7,18 @@
"bench": "deno bench -A" "bench": "deno bench -A"
}, },
"imports": { "imports": {
"@std/assert": "jsr:@std/assert@1", "@neabyte/deno-ansi": "jsr:@neabyte/deno-ansi@^0.1.0"
"chalk": "npm:chalk@^5.3.0"
}, },
<<<<<<< HEAD
"exports": {
".": "./src/mod.ts",
"./types": "./src/types.ts"
},
"version": "1.6.0",
=======
"exports": "./mod.ts", "exports": "./mod.ts",
"version": "1.5.1", "version": "1.5.1",
>>>>>>> 8073e044a7e840abdfc8c779abf2ac240b6dc4c1
"name": "@proxnet/undead-logging", "name": "@proxnet/undead-logging",
"license": "MIT", "license": "MIT"
"bench": {
"include": ["./benchmark/deno.ts"]
}
} }

22
deno.lock generated
View File

@@ -1,20 +1,12 @@
{ {
"version": "5", "version": "5",
"specifiers": { "specifiers": {
"jsr:@std/assert@1": "1.0.13", "jsr:@neabyte/deno-ansi@0.1": "0.1.0",
"jsr:@std/internal@^1.0.6": "1.0.9", "npm:@types/node@*": "22.15.15"
"npm:@types/node@*": "22.15.15",
"npm:chalk@^5.3.0": "5.4.1"
}, },
"jsr": { "jsr": {
"@std/assert@1.0.13": { "@neabyte/deno-ansi@0.1.0": {
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", "integrity": "3affddb394ce77feb2c40b3d9f8ae3c2045f8bc721038c2d6610f09db37edf72"
"dependencies": [
"jsr:@std/internal"
]
},
"@std/internal@1.0.9": {
"integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8"
} }
}, },
"npm": { "npm": {
@@ -24,17 +16,13 @@
"undici-types" "undici-types"
] ]
}, },
"chalk@5.4.1": {
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="
},
"undici-types@6.21.0": { "undici-types@6.21.0": {
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
} }
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@std/assert@1", "jsr:@neabyte/deno-ansi@0.1"
"npm:chalk@^5.3.0"
] ]
} }
} }

373
mod.ts
View File

@@ -1,373 +0,0 @@
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();
/**
* 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(`${msg}\r\n`);
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}\r\n`);
}
}
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;

375
src/mod.ts Normal file
View 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
View 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
}

7
tests/bright.ts Normal file
View File

@@ -0,0 +1,7 @@
import Logging from "../src/mod.ts";
const log = new Logging("NotBright");
log.bright = false;
log.i("Not Bright");

View File

@@ -1,16 +1,14 @@
import Logging, { LoggingListeners } from "../mod.ts"; import Logging, { LoggingConfiguration, LoggingListeners } from "../src/mod.ts";
import process from "node:process";
const debug = process.argv[process.argv.length - 1] == 'true'; const debug = Deno.args[Deno.args.length - 1] === 'true';
console.debug(`Debug mode: ${debug}`); console.debug(`Debug mode: ${debug}`);
const changeTimeFormat = process.argv[process.argv.length - 2] == 'true';
console.debug(`changeTimeFormat: ${changeTimeFormat}`);
if (debug) { if (debug) {
LoggingListeners.onmsg('basic', msg => { LoggingListeners.onmsg('basic', msg => {
console.debug(`\r\n[d] ${msg}`); console.debug(`[d] ${msg}`);
}); });
LoggingListeners.onmsg('type', (msg, type, source, time) => { LoggingListeners.onmsg('type', (msg, type, source, time) => {
console.debug(`[D] M:'${msg}' T:${type} S:'${source}' TM:${time.getTime()}`); console.debug(`[D] M:'${msg}' T:${type} S:'${source}' TM:${time.getTime()}${Logging.getNewline()}`);
}); });
} }
@@ -18,4 +16,25 @@ const webLog = new Logging("Web");
webLog.d(`Following is a Request`); webLog.d(`Following is a Request`);
webLog.i(new Request('http://example.com?hello=world', { headers: { 'key1': 'value1', 'key2': 'value2' }})); webLog.i(new Request('http://example.com?hello=world', { headers: { 'key1': 'value1', 'key2': 'value2' }}));
webLog.d(`Following is a Response`); webLog.d(`Following is a Response`);
webLog.i(await fetch('https://example.com')); webLog.i(await fetch('https://example.com'));
class CustomClass {
foo: string;
bar: number;
constructor(foo: string, bar: number) {
this.foo = foo;
this.bar = bar;
}
}
LoggingConfiguration.addConversion<CustomClass>({
condition: arg => arg instanceof CustomClass,
converter: arg => `CustomClass; foo:${arg.foo}; bar:${arg.bar};`
});
const log = new Logging("ConvertTest");
log.d(new CustomClass("baz", 39));

View File

@@ -1,4 +1,4 @@
import Logging, { LoggingConfiguration, LogTiming } from "@proxnet/undead-logging"; import Logging, { LoggingConfiguration, LogTiming } from "./../src/mod.ts";
const log = new Logging("Logger"); const log = new Logging("Logger");

28
tests/priority.ts Normal file
View File

@@ -0,0 +1,28 @@
import Logging, { LoggingConfiguration } from "@proxnet/undead-logging";
class CustomError extends Error {
someProperty: string
constructor(someProp: string) {
super("Something went wrong.");
this.someProperty = someProp;
}
}
LoggingConfiguration.clearConversions();
LoggingConfiguration.addConversion<CustomError>({
condition: arg => arg instanceof CustomError,
converter: arg => `CustomError: someProperty:${arg.someProperty}; ${arg.stack || arg.message}`,
priority: -1
});
LoggingConfiguration.addConversion<Error>({
condition: arg => arg instanceof Error,
converter: arg => `Error: ${arg.stack || arg.message}`,
priority: 1
});
const log = new Logging("PriorityTest");
log.i(new CustomError("'Hello World!'"));

8
tests/time.ts Normal file
View File

@@ -0,0 +1,8 @@
import Logging, { LoggingConfiguration } from "../src/mod.ts";
import { TimeFormat } from "../src/types.ts";
LoggingConfiguration.timeFormat = TimeFormat.None;
const log = new Logging("NoDate");
log.i('No Date');

View File

@@ -1,7 +1,7 @@
import Logging, { import Logging, {
LoggingConfiguration, LoggingConfiguration,
LogTiming, LogTiming,
} from "../mod.ts"; } from "./../src/mod.ts";
LoggingConfiguration.logTiming = LogTiming.Microtask; LoggingConfiguration.logTiming = LogTiming.Microtask;
// high-priority logs // high-priority logs