2.2.0 • Published 6 months ago

locustjs-logging v2.2.0

Weekly downloads
12
License
MIT
Repository
github
Last release
6 months ago

locustjs-logging

This is a logging library that aims to provide logging in a more flexible way.

It uses an abstract class named LoggerBase as the base class for all of its loggers.

This is done with loose coupling in mind, so that applications do not couple or depend on a specific logging implementation.

There are a few number of loggers that the library provides. All of them can be employed in any type of javascript application.

Log Object Structure

Using locustjs-logging, log messages are not limited to only strings. They are instances of a class named Log that has the following structure.

propertytypedescription
datedatelog date.
hoststringcustom string specifies in what host the log was created (explained later).
scopestringdefines in what scope the log was created (explained later).
datadataOne or more items that are logged.
exceptionExceptionoptional error object that was logged together with data.
batchboolwhether there were more than one item when logging (true) or not (false).

Log Types

There are 11 log types or log levels in locustjs-logging:

{
    info: 0,
    log: 1,
    debug: 2,
    trace: 3,
    warn: 4,
    danger: 5,
    success: 6,
    fail: 7,
    abort: 8,
    suggest: 9,
    cancel: 10,
}

There is an enum defined for the above log types named LogType that is also exported by the library.

Abstraction

LoggerBase

This is the base logger class that defines structure of all loggers. It is an abstract class from which no instance should be created (doing so, generates an error). It should be used just as a base class when implementing a logger.

Public Methods

methoddescription
constructor(options: object)options could be an object with the shape { host: string, scope: string } at bare minimum level. child loggers could define other properties in options object based on their business.
getScope():stringreturns log scope
enterScope(string \| function):voidpushes current scope to logger's internal scopes stack and sets scope to the given value
exitScope():voidpops one scope from logger's internal scopes stack and sets current scope to the poped value
log(...items):voidLogs items as a Log instance with LogType.log type
debug(...items):voidLogs items as a Log instance with LogType.debug type
warn(...items):voidLogs items as a Log instance with LogType.warn type
danger(...items):voidLogs items as a Log instance with LogType.danger type
info(...items):voidLogs items as a Log instance with LogType.info type
success(...items):voidLogs items as a Log instance with LogType.success type
fail(...items):voidLogs items as a Log instance with LogType.fail type
abort(...items):voidLogs items as a Log instance with LogType.abort type
suggest(...items):voidLogs items as a Log instance with LogType.suggest type
trace(...items):voidLogs items as a Log instance with LogType.trace type
cancel(...items):voidLogs items as a Log instance with LogType.cancel type

Public Properties

methodtypedescription
hoststringhost info
scopestringscope info
optionsobjectlogger's options object given to its constructor upon instantiation.

Public Abstract Methods

The following methods should be implemented by a child class derived from LoggerBase. All of these methods by default throw a NotImplementedException error.

methoddescriptionimplementation
_logInternal(log: Log): voidthis is the method that should perform the actual logging. log argument is an instance of Log class. The method is automatically invoked by LoggerBase when any of public logging methods (log(), debug(), danger(), etc.) are called.mandatory
clear():voidthis method is expected to clear logs.optional
getAll():Log[]this method is expected to return back all logs that the logger collected.optional

Host & Scope

As logs can play a helpful role in troubleshooting and debugging, two properties are provided for a log object (in Log class) in addition to logged data themselves. This way, a log object reflects better where it is created at. These two properties are host and scope, both of them are of string type.

  • host: host, is expected to reflect name of the environment or platform where log object was created.
  • scope: scope plays a different role than host. It is meant to reflect in what method or function the log is created.

Usually, host is just needed to be set once for a logger class for the whole app life-cycle. This is usually done through the logger options passed to a logger's constructor.

On the other hand, scope plays a more active role and should be set whenever we step in a function or method and should be cleared when we are stepping out. The helper enterScope() and exitScope() methods are provided for the same very job.

Example:

const logger = new ConsoleLogger({ host: 'Win10-x64' });

function foo(...args) {
    logger.enterScope(foo);

    try {
        logger.debug('input args', ...args);

        bar();

        logger.success('done');
    } catch (e) {
        logger.danger(e);
    }

    logger.exitScope();
}

function bar() {
    logger.enterScope(bar);

    // do something

    logger.exitScope();
}

Implementations

locustjs-logging provides a few implementations (sub-classes) for LoggerBase that can be handy in different scenarios. The hierarchical structure is as follows:

  • LoggerBase
    • ChainLogger
      • ArrayLogger
        • StorageLogger
          • LocalStorageLogger
          • SessionStorageLogger
      • ConsoleLogger
      • DOMLogger
    • NullLogger
    • DynamicLogger

The loggers are described in the following table.

Sub-ClassDescription
ChainLoggerThis abstract logger makes use of Chain of Responsibility design pattern and enables chaining loggers together. This way, chained loggers can be configured in a way that each one logs a specific types of logs. For example logging danger and fail log types could be assigned to ConsoleLogger and debug and trace log types to DOMLogger. This will be shown a little later.
ArrayLoggerThis logger is used as a cache and stores logs in an array in memory for later inspection (probably by invoking getAll() method).
StorageLoggerThis abstract logger, stores logs in a StorageBase store.
LocalStorageLoggerThis logger is derived from StorageLogger and logs items in localStorage in JSON format
SessionStorageLoggerThis logger is derived from StorageLogger and logs items in sessionStorage in JSON format
ConsoleLoggerThis logger logs items in console. It uses a colored logging output so that logs could be more readable. It supports both NodeJs and Web Browsers (its logs can be seen in Developer Tools).
DOMLoggerThis logger logs item into an HTML table in DOM. It also provides a few options to customize logs.
NullLoggerThis logger does not log anything and is meant to be used as a safe logger when we do not want our logs be exposed.
DynamicLoggerAs the name implies, this is a dynamic and flexible logger that provides a type property and could be turned into another logger based on the value specified for type.

Chaining Loggers Together

Based on Chain Of Responsibility design pattern implemented in ChainLogger, we can chain different loggers together and create a more advanced logger based on our need.

Options

A ChainLogger class can receive the following options through its constructor:

{
  next: LoggerBase, // next logger in the chain to which logs should be passed
  filter: array | string,  // list of log types to be logged; default = '*' i.e. all lgo types
  unattended: bool, // whether or not this logger acts in an unattended behavior,
                    // i.e. it actually logs, but pretends it does not, thus, logging is passed 
                    // to the next logger in the chain; default = undefiend = false
}

Example:

const logger = new ArrayLogger({
    unattended: true,
    next: new LocalStorageLogger({
        filter: 'suggest',
        next: new DOMLogger({
            filter: 'debug,danger,fail',
            next: new ConsoleLogger({
                filter: 'info,log',
                next: new NullLogger()
            })
        })
    })
})

The above logger is a chain of loggers that provides the following logging features:

  • An ArrayLogger logs everything sent through the chain (logs everything).
  • suggest logs are logged in localStorage
  • debug, danger and fail logs are logged in DOM (in an HTML table)
  • log and info logs are logged in console.
  • finally a NullLogger logger prevents unhandled errors to leak out of the chain and crash the whole application.

ConsoleLogger

ConsoleLogger receives an options object through its constructor by which we can customize it.

{
    styles: object,  // color customization object
    dateType: 'utc' | 'current' (default)
    env: 'node' | 'web' (default)
}

Color Customization

Web

If our environment is web, we can customize colors with an object as below:

{
    styles: {
        info: {
            label: {},
            scope: {},
            host: {},
            date: {}
        },
        debug: {
            label: {},
            scope: {},
            host: {},
            date: {}
        },
        ...
    }
}

Each key in styles object points to a specific log type. Using a custom object, we can customize each part of a log object, like its date, host, scope. The value provided for each property is an object whose keys are CSS style names.

{
    color: 'red',
    'background-color': 'white',
    'font-weight': 'bold',
    'font-size': '14px'
}

If no value is specified in any part of the styles, the default will be used.

Example: Customizing debug logs label to be displayed with white fore-color and blue back-color in browser web developer tools.

{
    styles: {
        debug: {
            label: {
                color: 'white',
                'background-color': 'blue'
            }
        },
        ...
    }
}

NodeJs

The color customization object in nodejs is similar to web:

{
    styles: {
        info: {
            label: {},
            scope: {},
            host: {},
            date: {}
        },
        debug: {
            label: {},
            scope: {},
            host: {},
            date: {}
        },
        ...
    }
}

The only difference is the color objects specified for each part.

{ fc: 'fore-color', bc: 'back-color' }

Nodejs does not support CSS styles as web developer tools in browsers. Moreover, its color codes conforms to ANSI colors. So, customizing colors in a nodejs environment is different than web. We can use ConsoleColors object exported by locustjs-logging to access a predefined set of console colors.

Example: Customizing debug logs label to be displayed with white fore-color and blue back-color.

import { ConsoleLogger, ConsoleColors } from 'locustjs-logging';

const consoleLogger = new ConsoleLogger({
    styles: {
        debug: {
            label: {
                fc: ConsoleColors.ForeColor.White,
                bc: ConsoleColors.BackColor.Blue,
            }
        },
        ...
    }
});

There is a picture in the DOMLogger section that shows how different generated logs in a Browser Developer Tools look like.

StorageLogger

Storage loggers are best suited in web scenarios when application's URL is changed, user is redirected to another route or page in the application or the page refreshes or posted to another route.

In all of these scenarios, the javascript code of application will be unloaded and loaded again. So, all logs stored in previous loggers described earlier will be lost. StorageLogger comes to rescue in these scenarios.

Since StorageLogger stores the logs in a storage, the logs will be persisted and will not be lost. StorageLogger can receive an options object through its constructor in the following structure:

{
    storeKey: string // storage item key where logs will be stored at. default = 'logs'
    store: 
    bufferSize: number // number of logs to be buffered before flushing into store. default = 5
}

bufferSize enables the logger to buffer logs and then write them into storage. This is to reduce the number of times storage is hit. A zero value will disable buffering.

DOMLogger

DOMLogger receives an options object in the following structure:

{
    target: string (CSS selector) | object,
    className: string,
    onInit: function(source: DOMLogger): string | HtmlTable,
    onNewLog: function(source: DOMLogger, log: Log, count: int): HtmlTableRow,
    format: function(source: DOMLogger, log: Log, type: string): string,
}
PropertyTypeDescription
targetstring | Nodea DOM node or CSS selector of a node where logs table should be created in (default = #dom-logger). it could be a simple <div id="dom-logger"></div> element.
onInitfunction(source: DOMLogger): string \| HtmlTablecustom html table creator function. It is called once when logger is constructed and also every time clear() method is called.
onNewLogfunction(source: DOMLogger, log: Log, row: int): HtmlTableRowcustom function to create a new table row in logs table. it should return a &<TR> node.
classNamestringCSS className of logs table (default = "dom-logger").
formatfunction(source: DOMLogger, log: Log, type: string): stringcustom formatter function to format data and exception properties of a log. by default the two proeprties are serialized in JSON and displayed in this format in logs table.

DOMLogger by default creates an HTML table in the target node specified in its options. The table has the following structure:

    <table class="${className}">
        <thead>
            <tr>
            <th>Row</th>
            <th>Type</th>
            <th>Date</th>
            <th>Scope/Host</th>
            <th>Data/Exception</th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>

Also, each log row is created in the following structure:

<tr>
	<td><div>{row}</div></td>
	<td><div>{log.type}</div></td>
	<td><div>{log.date}</div></td>
	<td>
		<span class="scope">{log.scope}</span>
		<span class="host">{log.scope}</span>
	</td>
	<td>
		<div class="data" title="{data}">{data}</div>
		<div class="exception" title="{exception}">{exception}</div>
	</td>
</tr>

data and exception are html-encoded by default (to counter XSS exploit). If a format function is given to DOMLogger using its constructor's options object argument, DOMLogger calls it once to get formatted data and again to get formatted exception. No html-encoding is performed if custom format function is used.

By default, DOMLogger applies no CSS styling to the generated HTML table. This is assigned to the user to apply any visual decoration to the logs table based on his choice.

There is a sample CSS styles file in tests folder in the repository of this library that decorates logs table with a few coloring and borders as shown in the following picture.

DOMLogger & ConsoleLogger coloring example

DynamicLogger

As it was stated earlier, DynamicLogger is a dynamic logger that changes its behavior based on the value specified for its type proeprty. This way, in a Web app for example, we can vastly use logging without fearing that our logs will be leaked out to console, while we also have the choice to switch our logger to a different one anytime we want upon debugging or troubleshooting our app.

Possible values for DynamicLogger.type property are as follows:

valueinternal logger
nullNullLogger
consoleConsoleLogger
domDOMLogger
arrayArrayLogger
localstorageLocalStorageLogger
sessionstorageSessionStorageLogger
consoleConsoleLogger
other valuescustomized logger by factory option (explained later)

The values are case-insensitive.

By default DynamicLogger acts as NullLogger.

Options

DynamicLogger can receive an options object in the following strcuture through its constructor:

{
    dom: object, // custom options for DOMLogger
    store: object, // custom options for store logger types
    console: object, // custom options for console logger
    array: object, // custom options for array logger
    factory: function(source: DynamicLogger, type: string): LoggerBase // custom factory to create loggers
    next: LoggerBase // default next logger
}

The dom, store, array and console values are used as options argument when instantiating loggers based on the type specified for DynamicLogger upon chaning its new type value.

Custom Logger

Using factory function, it is possible to override or extend the behavior of DynamicLogger. If factory is a function, it is invoked upon chaning DynamicLogger.type property and the new type is passed to it. It is expected for the factory function to return a LoggerBase instance. If it does not return anything (null or undefined), the default loggers will be used. If no logger could be found for the given type value, an InvalidLoggerTypeException error will be raised.

In the following example, we created a DynamicLogger that supports a redux type which logs items in a Redux store.

import { LoggerBase } from "locustjs-logging";
import store, { clearLogs } from './store'; // => redux store

class ReduxLogger extends LoggerBase {
    _logInternal(log) {
        store.dispatch(logAdded(log));
    }
    getAll() {
        return store.getState().logs;
    }
    clear() {
        store.dispatch(clearLogs());
    }
}

const logger = new DynamicLogger({
    factory: (source, type) => {
        if (type == 'redux') {
            return new ReduxLogger();
        }
    }
});
2.2.0

6 months ago

2.1.0

1 year ago

2.0.0

1 year ago

1.0.14

3 years ago

1.0.11

3 years ago

1.0.10

3 years ago

1.0.13

3 years ago

1.0.12

3 years ago

1.0.9

3 years ago

1.0.8

3 years ago

1.0.7

3 years ago

1.0.6

3 years ago

1.0.5

3 years ago

1.0.4

4 years ago

1.0.3

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago