diff --git a/README.md b/README.md
index 91e2bdd..25c4bf5 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# Undead Logging
-Logging for stupid idiots like me
+Opinionated logging
* Pluggable event listeners
* Per-source hushing, global log type hushing
@@ -39,10 +39,10 @@ log.n("Something happened");
Disable a type of message globally:
```ts
-import Logging, { MessageTypeVisibility } from "@proxnet/undead-logging";
+import Logging, { LoggingConfiguration } from "@proxnet/undead-logging";
// .. logging source "Main" is in the scope
-MessageTypeVisibility.Error = false;
+LoggingConfiguration.MessageTypeVisibility.Error = false;
// somewhere else, could be a different script
log.i("I am visible");
@@ -87,6 +87,8 @@ LoggingListeners.offmsg('basic', cb);
## Time display modes
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
* Unix time (in milliseconds)
* Local [process] time (in milliseconds, from the `performance` API)
@@ -99,6 +101,8 @@ import { LoggingConfiguration, TimeFormat } from "@proxnet/undead-logging";
LoggingConfiguration.timeFormat = TimeFormat.Local;
// or
LoggingConfiguration.timeFormat = TimeFormat.Utc;
+// or even
+LoggingConfiguration.timeFormat = TimeFormat.None; // removes the time and date section of the log line entirely
```
## (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)
## 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)`.
-The corresponding property will be updated on `LoggingConfiguration`:
+You can reset every source's log timing or time format with `LoggingConfiguration.resetX`.
+The corresponding property will be updated on all instances of `Logging`:
```ts
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`:
+
+```ts
+class CustomClass {
+
+ foo: string;
+ bar: number;
+
+ constructor(foo: string, bar: number) {
+ this.foo = foo;
+ this.bar = bar;
+ }
+
+}
+
+LoggingConfiguration.addConversion({
+ 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`.
+
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.
+
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,
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({
+ condition: arg => arg instanceof CustomError,
+ converter: arg => `CustomError: someProperty:${arg.someProperty}; ${arg.stack || arg.message}`,
+ priority: -1
+});
+LoggingConfiguration.addConversion({
+ 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
create an account on gitea.proxnet.dev, fork, then PR
\ No newline at end of file
diff --git a/benchmark/bun.ts b/benchmark/bun.ts
deleted file mode 100644
index 9f0e93b..0000000
--- a/benchmark/bun.ts
+++ /dev/null
@@ -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('');
-}
\ No newline at end of file
diff --git a/benchmark/deno-example.json b/benchmark/deno-example.json
deleted file mode 100644
index cce30f9..0000000
--- a/benchmark/deno-example.json
+++ /dev/null
@@ -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
- }
- }
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/benchmark/deno.ts b/benchmark/deno.ts
deleted file mode 100644
index 74127c2..0000000
--- a/benchmark/deno.ts
+++ /dev/null
@@ -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');
- }
-});
\ No newline at end of file
diff --git a/deno.json b/deno.json
index 30e18c0..668746a 100644
--- a/deno.json
+++ b/deno.json
@@ -6,14 +6,13 @@
"bench": "deno bench -A"
},
"imports": {
- "@std/assert": "jsr:@std/assert@1",
- "chalk": "npm:chalk@^5.3.0"
+ "@neabyte/deno-ansi": "jsr:@neabyte/deno-ansi@^0.1.0"
},
- "exports": "./mod.ts",
- "version": "1.5.0",
+ "exports": {
+ ".": "./src/mod.ts",
+ "./types": "./src/types.ts"
+ },
+ "version": "1.6.0",
"name": "@proxnet/undead-logging",
- "license": "MIT",
- "bench": {
- "include": ["./benchmark/deno.ts"]
- }
+ "license": "MIT"
}
diff --git a/deno.lock b/deno.lock
index a4cb8a4..58c4004 100644
--- a/deno.lock
+++ b/deno.lock
@@ -1,20 +1,12 @@
{
"version": "5",
"specifiers": {
- "jsr:@std/assert@1": "1.0.13",
- "jsr:@std/internal@^1.0.6": "1.0.9",
- "npm:@types/node@*": "22.15.15",
- "npm:chalk@^5.3.0": "5.4.1"
+ "jsr:@neabyte/deno-ansi@0.1": "0.1.0",
+ "npm:@types/node@*": "22.15.15"
},
"jsr": {
- "@std/assert@1.0.13": {
- "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
- "dependencies": [
- "jsr:@std/internal"
- ]
- },
- "@std/internal@1.0.9": {
- "integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8"
+ "@neabyte/deno-ansi@0.1.0": {
+ "integrity": "3affddb394ce77feb2c40b3d9f8ae3c2045f8bc721038c2d6610f09db37edf72"
}
},
"npm": {
@@ -24,17 +16,13 @@
"undici-types"
]
},
- "chalk@5.4.1": {
- "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="
- },
"undici-types@6.21.0": {
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
}
},
"workspace": {
"dependencies": [
- "jsr:@std/assert@1",
- "npm:chalk@^5.3.0"
+ "jsr:@neabyte/deno-ansi@0.1"
]
}
}
diff --git a/mod.ts b/mod.ts
deleted file mode 100644
index 4253365..0000000
--- a/mod.ts
+++ /dev/null
@@ -1,381 +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 {
- 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 = 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,
- {
- condition: arg => arg == null,
- converter: arg => JSON.stringify(arg)
- } as UnknownConversion,
- {
- condition: arg => arg == undefined,
- converter: _arg => 'undefined'
- } as UnknownConversion,
- {
- 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,
- {
- 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,
- {
- condition: arg => typeof arg == 'object',
- converter: arg => JSON.stringify(arg)
- } as UnknownConversion