@obsidize/rx-console

@obsidize/rx-console

A logging library that is tiny, highly configurable, and platform-agnostic.

More configurable than modules like debug, without the restrictiveness / platform-locking of modules like winston.

Highlights

  • Zero dependencies
  • Compatible on anything that runs JavaScript
  • Compact (~7Kb es5 file)

Goals

  • Allow runtime configuration of log levels
  • Ability to intercept log events, so they can be transported to a persistence source
  • Make no assumptions about the persistence layer (i.e. no dependency declarations that lock this module to node or the browser, like node 'fs' or 'stream' packages)
  • Optional integration with RxJS
  • Configurability at each abstraction layer (example code shown further down)

Installation

npm install -P -E @obsidize/rx-console

API

Source documentation can be found here

Usage (NodeJS / Vanilla JavaScript)

The below snippet can be tested with runkit on NPM.

const {
Logger,
LogLevel,
getPrimaryLoggerTransport
} = require('@obsidize/rx-console');

// get a reference to the root log event transport
getPrimaryLoggerTransport()
// set custom filter for what events get emitted
.setFilter(ev => ev.level >= LogLevel.DEBUG)
// turn on default global `console` variable usage
.setDefaultBroadcastEnabled(true);

const logger = new Logger('RunKitLogger');

logger.debug('test');
// "2021-03-15T21:13:42.356Z [DEBUG] [RunKitLogger] test"

const someObj = {
myValueIs: 42,
isOptionalParam: true,
someOtherValue: 'yep'
};

logger.info('some object info: ', someObj);
// "2021-03-15T21:13:42.360Z [INFO] [RunKitLogger] some object info: "
// Object {myValueIs: 42, isOptionalParam: true, someOtherValue: "yep"}

logger.warn('something unusual happened');
// "2021-03-15T21:13:42.363Z [WARN] [RunKitLogger] something unusual happened

logger.fatal('EXPLOSIONS?!?!?!');
// "2021-03-15T21:13:42.366Z [FATAL] [RunKitLogger] EXPLOSIONS?!?!?!"

logger.verbose('im obnoxious');
// Does not log anything because VERBOSE < DEBUG

Usage (TypeScript)

TypeScript usage is virtually identical to vanilla JavaScript.

Here's an example of using a logger within the context of a class:

import { Logger, LogLevel, getPrimaryLoggerTransport } from '@obsidize/rx-console';

// NOTE: this only needs to be done once, as a part of your app's main setup routine
getPrimaryLoggerTransport()
.setFilter(ev => ev.level >= LogLevel.DEBUG)
// can also be fed in a 'prod' flag from your app
.setDefaultBroadcastEnabled(true);

class MyServiceThing {

private readonly logger = new Logger('MyServiceThing');

test(): void {
this.logger.debug('test!');
}
}

const service = new MyServiceThing();
service.test(); // 2021-02-16T00:42:20.777Z [DEBUG] [MyServiceThing] test!

Usage (RxJS)

Log events can be redirected to an rxjs stream like so:

import { Observable, fromEventPattern, inverval } from 'rxjs';
import { buffer, map, filter } from 'rxjs/operators';
import { LogEvent, getPrimaryLoggerTransport, stringifyLogEvent } from '@obsidize/rx-console';

// no-op function for example purposes
const writeToFile = (..._args: any[]) => { };

// combine the buffered events into a single string
const serializeEvents = (events: LogEvent[]): string => {
// customize this however you want
return events.map(ev => stringifyLogEvent(ev)).join('\n') + '\n';
};

// Wraps the transport's EventEmitter as an rxjs observable using
// the `fromEventPattern` rxjs creator function.
const eventStream = getPrimaryLoggerTransport()
.events() // get a reference to the transport's EventEmitter instance
.asObservable<Observable<LogEvent>>(fromEventPattern);

eventStream.pipe(

// required if event caching is not disabled
// (caching is enabled by default)
map((event: LogEvent) => event.clone()),

// accumulate log events for 1 second
buffer(interval(1000)),

// if we didn't get any new logs after 5 seconds, just skip this round
filter((events: LogEvent[]) => events.length > 0),

// stringify and concatenate the buffered events
map((events: LogEvent[]) => serializeEvents(events))

).subscribe(outputString => {

// Dump the buffer to a file, or send to a server, or wherever.
writeToFile(outputString);
});

Runtime Event Filtering

Both LoggerTransport and Logger extend the LogEventGuardContext class, which allows you to suppress events both on the transport side (shared between loggers that use the transport) and on the logger side (one-off filtering).

import { Logger, LogLevel, getPrimaryLoggerTransport } from '@obsidize/rx-console';

getPrimaryLoggerTransport()
.setFilter(ev => ev.level >= LogLevel.DEBUG);

const logger1 = new Logger('TestLogger')
.setFilter(ev => ev.level >= LogLevel.TRACE);

const logger2 = new Logger('TestLogger')
.setFilter(ev => ev.level >= LogLevel.INFO);

logger1.trace('trace test'); // suppressed because the transport's guard caught it
logger1.debug('debug test'); // "debug test"

logger2.debug('debug test'); // suppressed because this logger's guard caught it
logger2.info('info test'); // "info test"

logger2.setEnabled(false);
logger2.error('BOOM!!!'); // suppressed because the logger is disabled

Custom Serialization

The default serialization options can be configured via the shared Config instance in this module:

import { Config } from '@obsidize/rx-console';

const sharedConfig = Config.sharedInstance;
sharedConfig.parameterSeparator = ', '; // join separator for additional log arguments (default ' :: ')
sharedConfig.stringifyMaxLength = 2000; // set 2000 characters before truncation (default 250)
sharedConfig.levelNameMap.update({ CUSTOM_LEVEL_NAME: 123 }); // Uses `LogLevelNameMap.main` by default

// optionally reset to defaults if needed
sharedConfig.reset();

Custom Extensions

This module is fully customizable via class extensions:

import { LogEvent, LoggerTransport, Logger, getPrimaryLoggerTransport } from '@obsidize/rx-console';

// Create a custom event type
class CustomLogEvent extends LogEvent {

// Add some special sauce to your custom event instances.
specialSauceData: number = 42;
}

// Create a custom transport that knows how to create your custom event type
class CustomTransport extends LoggerTransport {

// Create a main instance for your loggers to report back to
public static readonly main = new CustomTransport();

// override to return your custom event type
public createEvent(level: number, tag: string, message: string, params: any[]): CustomLogEvent {
return new CustomLogEvent(level, tag, message, params);
}
}

// Create a custom logger that uses your custom transport as the default
class CustomLogger extends Logger {

constructor(
name: string,
transport: CustomTransport = CustomTransport.main
) {
super(name, transport);
}
}

const transport = CustomTransport.main;

transport.addListener((ev: CustomLogEvent) => {
console.log(ev.message); // 'custom log'
console.log(ev.specialSauceData); // 42
});

// NOTE: You can also wire your custom transport back into the default instance
transport.pipeToDefault();

const logger = new CustomLogger('TestLogger');
logger.info('custom log');

// you can also break the connection to the default instance later on
transport.unpipeFromDefault();

Examples

Generated using TypeDoc