diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..40b878d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/BENCH.md b/BENCH.md
new file mode 100644
index 0000000..30d1a4a
--- /dev/null
+++ b/BENCH.md
@@ -0,0 +1,49 @@
+## Benchmarks
+[<-- Back to README](./README.md)
+
+Deno 2.4.1 can't find my CPU for some reason.
+
+Ran with an AMD Ryzen 7 5800X @ 3.6GHz
+
+`benchmark/deno.ts`:
+```
+| benchmark | time/iter (avg) | iter/s | (min … max) | p75 | p99 | p995 |
+| ----------- | --------------- | ------------- | --------------------- | -------- | -------- | -------- |
+
+group timing-sync
+| 1 Log | 1.6 µs | 643,000 | ( 1.5 µs … 1.9 µs) | 1.5 µs | 1.9 µs | 1.9 µs |
+| 100 Logs | 153.4 µs | 6,520 | (147.1 µs … 279.5 µs) | 150.2 µs | 229.9 µs | 234.5 µs |
+| 1k Logs | 1.5 ms | 650.2 | ( 1.5 ms … 1.7 ms) | 1.6 ms | 1.7 ms | 1.7 ms |
+| 10k Logs | 15.4 ms | 65.0 | ( 15.2 ms … 16.1 ms) | 15.4 ms | 16.1 ms | 16.1 ms |
+| 100k Logs | 153.5 ms | 6.5 | (152.9 ms … 154.8 ms) | 153.7 ms | 154.8 ms | 154.8 ms |
+
+summary
+ 1 Log
+ 98.62x faster than 100 Logs
+ 989.00x faster than 1k Logs
+ 9887x faster than 10k Logs
+ 98680x faster than 100k Logs
+
+group timing-deferred
+| 1 Log | 1.6 µs | 644,200 | ( 1.5 µs … 1.6 µs) | 1.6 µs | 1.6 µs | 1.6 µs |
+| 100 Logs | 153.2 µs | 6,525 | (147.5 µs … 269.1 µs) | 150.6 µs | 228.1 µs | 230.8 µs |
+| 1k Logs | 1.5 ms | 653.9 | ( 1.5 ms … 1.7 ms) | 1.6 ms | 1.6 ms | 1.6 ms |
+| 10k Logs | 15.3 ms | 65.3 | ( 15.2 ms … 15.7 ms) | 15.3 ms | 15.7 ms | 15.7 ms |
+| 100k Logs | 153.7 ms | 6.5 | (152.9 ms … 155.9 ms) | 154.0 ms | 155.9 ms | 155.9 ms |
+
+summary
+ 1 Log
+ 98.72x faster than 100 Logs
+ 985.10x faster than 1k Logs
+ 9865x faster than 10k Logs
+ 99040x faster than 100k Logs
+```
+
+### TL;DR / What does this mean?
+
+You can, *in theory* and *on average*, construct 650,000 messages per second.
+
+I don't know how to benchmark a logger without logging messages to stdout,
+so I created a dedicated `bench` function on each source that just *constructs* them.
+
+Deferred is not really much faster, but can be helpful in some scenarios. I think. Maybe.
\ No newline at end of file
diff --git a/README.md b/README.md
index bf7fab7..e20c53c 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ Callback ran when a message is logged anywhere (this time components are split u
```ts
LoggingListeners.onmsg('type', (msg, type, source, time) => {
// msg: string
- // type: MessageType - import { MessageType } from "@proxnet/undead-logging";)
+ // type: MessageType - import { MessageType } from "@proxnet/undead-logging";
// source: string
// time: Date
});
@@ -74,25 +74,26 @@ LoggingListeners.onmsg('type', (msg, type, source, time) => {
Remove callback:
```ts
-const cb = msg => { /* do something with msg */ };
+const cb = msg => { /* do something with msg: string */ };
LoggingListeners.onmsg('basic', cb);
LoggingListeners.offmsg('basic', cb);
```
## Time display modes
-You can display three different formats for time:
+You can display four different formats for time:
* UTC
* Unix time (in milliseconds)
-* Local [process] time (in seconds, from the `performance` API)
+* Local [process] time (in milliseconds, from the `performance` API)
+* RoundedLocal: Same as above, but rounded to the nearest whole ms
Set the time format:
```ts
import { LoggingConfiguration, TimeFormat } from "@proxnet/undead-logging";
-LoggingConfiguration.timeFormat = TimeFormat.Local
+LoggingConfiguration.timeFormat = TimeFormat.Local;
// or
-LoggingConfiguration.timeFormat = TimeFormat.Utc
+LoggingConfiguration.timeFormat = TimeFormat.Utc;
```
## (advanced) Logging timing
@@ -101,6 +102,14 @@ You can control when log functions are executed using `LoggingConfiguration.logT
Logs are sent synchronously by default. You can optionally defer logs with `setImmediate` using `LogTiming.Deferred`
```ts
-LoggingConfiguration.logTiming = LogTiming.Sync
-LoggingConfiguration.logTiming = LogTiming.Deferred
-```
\ No newline at end of file
+LoggingConfiguration.logTiming = LogTiming.Sync;
+// or
+LoggingConfiguration.logTiming = LogTiming.Deferred;
+```
+
+## [Benchmarks](./BENCH.md)
+
+## Contributing
+Are you crazy? This ol' thing is just.. "meh" at most. You're sure?
+
+... anyway, 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
new file mode 100644
index 0000000..9f0e93b
--- /dev/null
+++ b/benchmark/bun.ts
@@ -0,0 +1,64 @@
+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
new file mode 100644
index 0000000..ab79b42
--- /dev/null
+++ b/benchmark/deno-example.json
@@ -0,0 +1,227 @@
+{
+ "version": 1,
+ "runtime": "Deno/2.4.1 x86_64-pc-windows-msvc",
+ "cpu": "unknown",
+ "benches": [
+ {
+ "origin": "file:///C:/Users/Keagan/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:///C:/Users/Keagan/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:///C:/Users/Keagan/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:///C:/Users/Keagan/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:///C:/Users/Keagan/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:///C:/Users/Keagan/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:///C:/Users/Keagan/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:///C:/Users/Keagan/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:///C:/Users/Keagan/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:///C:/Users/Keagan/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
new file mode 100644
index 0000000..74127c2
--- /dev/null
+++ b/benchmark/deno.ts
@@ -0,0 +1,109 @@
+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 6096327..e6f4fe4 100644
--- a/deno.json
+++ b/deno.json
@@ -1,14 +1,19 @@
{
"tasks": {
"debug": "deno run -A test.ts true",
- "plain": "deno run -A test.ts"
+ "plain": "deno run -A test.ts",
+ "dev": "deno run plain",
+ "bench": "deno bench -A"
},
"imports": {
"@std/assert": "jsr:@std/assert@1",
"chalk": "npm:chalk@^5.3.0"
},
"exports": "./mod.ts",
- "version": "1.3.0",
+ "version": "1.4.0",
"name": "@proxnet/undead-logging",
- "license": "MIT"
+ "license": "MIT",
+ "bench": {
+ "include": ["./benchmark/deno.ts"]
+ }
}
diff --git a/deno.lock b/deno.lock
index d86c006..a4cb8a4 100644
--- a/deno.lock
+++ b/deno.lock
@@ -1,24 +1,34 @@
{
- "version": "4",
+ "version": "5",
"specifiers": {
- "jsr:@std/assert@1": "1.0.8",
- "jsr:@std/internal@^1.0.5": "1.0.5",
- "npm:chalk@^5.3.0": "5.3.0"
+ "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": {
- "@std/assert@1.0.8": {
- "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b",
+ "@std/assert@1.0.13": {
+ "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
"dependencies": [
"jsr:@std/internal"
]
},
- "@std/internal@1.0.5": {
- "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
+ "@std/internal@1.0.9": {
+ "integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8"
}
},
"npm": {
- "chalk@5.3.0": {
- "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
+ "@types/node@22.15.15": {
+ "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==",
+ "dependencies": [
+ "undici-types"
+ ]
+ },
+ "chalk@5.4.1": {
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="
+ },
+ "undici-types@6.21.0": {
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
}
},
"workspace": {
diff --git a/mod.ts b/mod.ts
index c134ad3..559488e 100644
--- a/mod.ts
+++ b/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 {
- 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,
+ {
+ 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