Files
undead-logging/README.md

7.2 KiB

Undead Logging

Opinionated logging

  • Pluggable event listeners
  • Per-source hushing, global log type hushing
  • Log timing control
  • Time display modes
import Logging from "@proxnet/undead-logging"

const log = new Logging("Main");

log.i("Hello World!");

Disable a source:

// .. logging source is in the scope
log.visible = false;

log.debug("I can't be seen!");

// no output

Change a source's name:

// .. 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:

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:

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):

LoggingListeners.onmsg('type', (msg, type, source, time) => {
    // msg: string
    // type: MessageType - import { MessageType } from "@proxnet/undead-logging";
    // source: string
    // time: Date
});

Remove callback:

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:

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

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:

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:

// 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<T>:

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.
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 Errors rather than the base Error class.

You can configure converter priority using Conversion.priority:

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

create an account on gitea.proxnet.dev, fork, then PR