250 lines
7.2 KiB
Markdown
250 lines
7.2 KiB
Markdown
# 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<br>
|
|
you'd like to prevent slowing down I/O.<br>
|
|
You might also prefer to reset all sources to synchronous logging during app shutdown.<br>
|
|
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`.<br>
|
|
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<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
|
|
create an account on gitea.proxnet.dev, fork, then PR |