# Undead Logging Opinionated logging * Pluggable event listeners * Per-source hushing, global log type hushing * Log timing control * Time display modes ```ts import Logging from "@proxnet/undead-logging" const log = new Logging("Main"); log.i("Hello World!"); ``` Disable a source: ```ts // .. logging source is in the scope log.visible = false; log.debug("I can't be seen!"); // no output ``` Change a source's name: ```ts // .. logging source "Main" is in the scope log.n("Network is networking"); log.source = "Main2"; log.n("Something happened"); // output: // 2024-11-14T01:21:40.350Z Main [NETWORK] Network is networking // 2024-11-14T01:21:40.350Z Main2 [NETWORK] Something happened ``` Disable a type of message globally: ```ts import Logging, { LoggingConfiguration } from "@proxnet/undead-logging"; // .. logging source "Main" is in the scope LoggingConfiguration.MessageTypeVisibility.Error = false; // somewhere else, could be a different script log.i("I am visible"); log.error("I am not visible"); // output: // 2024-11-14T01:21:40.350Z Main [INFO] I am visible ``` ## Event Listeners Event listeners are called when a message is logged on any logging source An event callback can either receive messages as either: * Whole lines, including formatting, colors, excluding newlines or * Individual `msg` (string), `type` (MessageType), `source` (string), and `time` (Date) arguments Callback ran when a message is logged anywhere: ```ts LoggingListeners.onmsg('basic', msg => { // `msg` is a string containing the entire line w/ formatting }); ``` Callback ran when a message is logged anywhere (this time components are split up): ```ts LoggingListeners.onmsg('type', (msg, type, source, time) => { // msg: string // type: MessageType - import { MessageType } from "@proxnet/undead-logging"; // source: string // time: Date }); ``` Remove callback: ```ts const cb = msg => { /* do something with msg: string */ }; LoggingListeners.onmsg('basic', cb); 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) * 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; // 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 You can control when log functions are executed using `LoggingConfiguration.logTiming`. Logs are sent synchronously by default. You can optionally defer logs with `setImmediate` using `LogTiming.Deferred` ```ts LoggingConfiguration.logTiming = LogTiming.Sync; // or LoggingConfiguration.logTiming = LogTiming.Deferred; ``` You might prefer to defer logs in asynchronous situations where order does not matter and
you'd like to prevent slowing down I/O.
You might also prefer to reset all sources to synchronous logging during app shutdown.
This can help debug issues with how your app closes. (see example below) ## Global resets Every source is tracked by `LoggingConfiguration` in a `Set`. 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"; const log = new Logging("Logger"); log.logTiming = LogTiming.Sync; // the default LoggingConfiguration.resetLogTiming = LogTiming.Deferred; // both are LogTiming.Deferred log.d(`My log timing: LogTiming.${LogTiming[log.logTiming]}`); log.d(`Global: LogTiming.${LogTiming[LoggingConfiguration.logTiming]}`); ``` This is useful when you want to use synchronous logging during a shutdown: ```ts // something is keeping the event loop alive (HTTP server, a database, some I/O?) LoggingConfiguration.logTiming = LogTiming.Deferred; const log = new Logging("Main"); Deno.addSignalListener('SIGINT', () => { LoggingConfiguration.resetLogTiming = LogTiming.Sync; // shut down I/O here }); ``` ## 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