npm package: @intermattory/logging
Description: JavaScript message logging system
Author: Tomasz Kulik
Latest version: 2.0.3

# Intermattory Logging
JavaScript message logging system.


- [Features](#features)
- [Simplest setup](#simplest-setup)
- [Typical setup](#typical-setup)
- [Configuration](#configuration)
  - [Log targets](#log-targets)
  - [Log number](#log-number)
  - [Logger arguments](#logger-arguments)
- [LogLevel](#loglevel)
- [Logger](#logger)
  - [Logger methods](#logger-methods)
- [LogEntry](#logentry)
- [Creating custom message formatter](#creating-custom-message-formatter)
- [Creating custom log target](#creating-custom-log-target)
- [Demo](#demo)

## Features
* Numbering logs
* Custom log numbers
* Predefined log levels: DEBUG, INFO, WARN, ERROR, FATAL
* Custom log levels support
* Multiple log targets
* Logging errors
* Log configuration per target
* Filtering log entries


To use the logger you need to first set it up. The best way to do that is to create a separate JavaScript module.
The simplest logger setup would look like this:

## Simplest setup

logging/online-shop-logger.js
```javascript
import { Log } from '@intermattory/logging';

const log = Log()
log.start()
export const Logger = log.Logger
```

you can than use it like so:

```javascript
import { Logger } from 'logging/online-shop-logger.js'

function ShoppingCart(items) {
    const logger = Logger(ShoppingCart)
    logger.info('Initializing shopping cart with items', items)
}
```
By default, the Logger logs all messages to the console.

## Typical setup
Usually you need more than this. Intermattory logging system lets you configure multiple log targets, where you can
log messages of different levels, with custom formatting, custom log number pattern, filtered with given filter function.

logging/online-shop-logger.js
```javascript
import { Log, ArrayTarget, ConsoleTarget } from '@intermattory/logging';
import { Config } from './config.js';
import { LogServiceTarget } from './logging/log-service.js'

const consoleTarget = ConsoleTarget()
const arrayTarget = ArrayTarget()

const configuration = {
  DEV: {
    logTargets: [
      {
          target: consoleTarget,
          level: LogLevel.ALL
      },
      {
          target: arrayTarget,
          level: LogLevel.ALL
      }
    ]},
  STAGE: {
    logTargets: [
      {
          target: consoleTarget,
          level: LogLevel.WARN,
          filter: logEntry => logEntry.module !== 'authorization'
      },
      {
          target: arrayTarget,
          level: LogLevel.ALL
      }
  ]},
  PROD: {
    logTargets: [
      {
          target: consoleTarget,
          level: LogLevel.ERROR,
          filter: logEntry => logEntry.module !== 'authorization'
      },
      {
          target: arrayTarget,
          level: LogLevel.ALL
      },
      {
          target: LogServiceTarget(),
          level: LogLevel.WARN
      }
  ]}
}

const log = Log(configuration[Config.environment])
log.start()

export const Logger = (source, module) => log.Logger(source, { module, application: 'Web application' })
export const getLogs = () => arrayTarget.array
```

Usage example:

```javascript
import { Logger } from 'logging/online-shop-logger.js'

function ShoppingCart(items) {
    const logger = Logger(ShoppingCart, 'eCommerce')
    logger.info('Initializing shopping cart with items', items)
    
    const getDiscount = items => service.getDiscount(items)
        .then(discount => {
            logger.debug('Discount:', discount)
            if (discount > 50)
                logger.warn('Discount calculation formula is probably wrong')
        })
        .catch(error => {
            const logEntry = logger.error.withError(error, 'Service failed to calculate the discount')
            showAlert(`Your discount could not be calculated. Please try again later. See log #${logEntry.logNumber} for details`)
        })
    // ...
}
```
Intermattory logging doesn't provide the `ServiceTarget`. If you need one, check [Creating custom log target](#creating-custom-log-target).

## Configuration

By default, the logger logs all messages to the console
```javascript
const DEFAULT_LOGGER_CONFIGURATION = {
  logNumberGenerator: DEFAULT_LOG_NUMBER_GENERATOR,
  logTargets: [
    {
      target: ConsoleTarget(),
      level: LogLevel.ALL
    }
  ]
}
```

to change that, pass your configuration to `Log`'s "constructor":
```javascript
const consoleTarget = ConsoleTarget()
const arrayTarget = ArrayTarget()
const configuration = {
  logTargets: [
    {
        target: consoleTarget,
        level: LogLevel.ALL,
        filter: logEntry => logEntry.source !== 'IgnoredSource'
    },
    {
        target: arrayTarget,
        level: LogLevel.INFO
    }
]}
const log = Log(configuration)
log.start()
```

### Log targets

`ArrayTarget` is useful to list the logs in GUI. Check out the [demo](https://intermattory.bitbucket.io/logging/) to see it in action.
Get the array from `arrayTarget.array`.

`ConsoleTarget` can be initialized with an optional `messageFormatter` argument.
It lets you set the template of a message and customize information it contains. `ConsoleTarget` uses `TemplateMessageFormatter` as default.
You can also create your own formatter - check out [Creating custom message formatter](#creating-custom-message-formatter).

You can create your own log targets. Check out [Creating custom log target](#creating-custom-log-target) to find out how.

Use `filter` field to exclude the messages you don't want to log. It expects a function which returns `true` or `false`.
Filtering doesn't affect the log number incrementation. 

### Log number

By default, the log number is an integer incremented by 1 with every new log entry. Counting starts at the moment you call `log.start()`.

You can customize it by setting the `logNumberGenerator` field to a function which generates unique IDs.
The function takes the default integer log number and a date as arguments. It can return a `number` or `string`.
Logger does not validate the result of the function, so make sure you return unique values.

All log targets use the same log number generator.

### Logger arguments
`Logger` function takes 2 arguments: `source` and optional `parameters`.

The `parameters` are sort of tags which can help you filter the log entries.
In different projects or even modules of the same project, you may need to log different information. 
Sometimes you may want to log only the source of the message, sometimes the module name or message category.
You can define your Logger arguments by wrapping the original `log.Logger` in a Higher Order Function.
You can also create multiple loggers with different signature:

logging/online-shop-logger.js
```javascript
const log = Log()
log.start()

export const Logger = (source) => log.Logger(source, { application: 'Web application', workstationId: getWorkstaionId() })
export const ModuleLogger = (source, module) => log.Logger(source, { module, application: 'Web application' })
export const CategoryLogger = (source, category) => log.Logger(source, { category })
```

## LogLevel
Log level is a number.

LogLevel object provides predefined log levels:

* LogLevel.ALL = 0
* LogLevel.DEBUG = 100
* LogLevel.INFO = 200
* LogLevel.WARN = 300
* LogLevel.ERROR = 400
* LogLevel.FATAL = 1000

## Logger
Once you instantiated the Log you can use the Logger:

```javascript
const log = Log()
const logger = log.Logger('LogSource')
```

`Logger` function takes 2 arguments:
- mandatory `source` (function or string)
- optional `parameters` (object)

#### source
If you pass a function as source, the logger will log its name.
Be aware that if you minify your code, function names may be minified too and a string may be more reliable.

#### parameters
This can be anything. If you would like to log more information and don't want to pass them every time you log a message,
you can pass them as `parameters` to the Logger:
```javascript
const logger = Logger(ShoppingCart, { module: 'eCommerce'})
```

---
**NOTE**
OK, it can't be anything...
You can give your parameters any valid JavaScript name except `date, logNumber, level, error, message, source`.
These are standard fields which describe every log entry.

---

### Logger methods
Here are all available methods:
```javascript
const logEntry = logger.log(customLevel, 'Message')
```
```javascript
const logEntry = logger.fatal('Message')
```
```javascript
const logEntry = logger.error('Message')
```
```javascript
const logEntry = logger.warn('Message')
```
```javascript
const logEntry = logger.info('Message')
```
```javascript
const logEntry = logger.debug('Message')
```
With error:
```javascript
const logEntry = logger.log.withError(error, LogLevel.ERROR, 'Message')
```
```javascript
const logEntry = logger.fatal.withError(error, 'Message')
```
```javascript
const logEntry = logger.error.withError(error, 'Message')
```
```javascript
const logEntry = logger.warn.withError(error, 'Message')
```
```javascript
const logEntry = logger.info.withError(error, 'Message')
```
```javascript
const logEntry = logger.debug.withError(error, 'Message')
```

## LogEntry
Every method returns a log entry, which of the following structure:
```
{
    logNumber: number | string,
    level: string,
    message: [],
    error?: {*},
    date: Date,
    source: function | string
}
```

This is very useful, if after an action, you need to give the user a feedback with some technical details, like the log number or error:
```javascript
const logEntry = logger.error.withError(error, 'Parsing error')
showAlert(`An error occurred while opening the document (${logEntry.errorMessage}). For technical details go to the log No.${logEntry.logNumber}`)
```

## TemplateMessageFormatter

```javascript
import { TemplateMessageFormatter, ConsoleTarget } from '@inttermattory/logging'

const consoleMessageFormatFunction = (entry, formattedEntry) => `#${formattedLogEntry.logNumber} [${formattedLogEntry.level}][${formattedLogEntry.time}]${formattedLogEntry.parameters ? `[${formattedLogEntry.parameters}]` : ''} ${formattedLogEntry.source}: ${formattedLogEntry.message}${logEntry.error ? ` ${formattedLogEntry.errorName}
    stack trace:
    ${formattedEntry.stackTrace}
` : ''}`
const consoleMessageTemplate = TemplateMessageFormatter(consoleMessageFormatFunction)
const consoleTarget = ConsoleTarget(consoleMessageTemplate)
```

The default formatter is stored in `TemplateMessageFormatter.DEFAULT_MESSAGE_TEMPLATE` 
and here is how it looks like:
```javascript
(logEntry, formattedLogEntry) => `#${formattedLogEntry.logNumber} [${formattedLogEntry.level}][${formattedLogEntry.time}]${formattedLogEntry.parameters ? `[${formattedLogEntry.parameters}]` : ''} ${formattedLogEntry.source}: ${formattedLogEntry.message}${logEntry.error ? ' ' + formattedLogEntry.error: ''}`
```

# Creating custom message formatter
A message formatter has a very simple interface:
It must implement a `format` method and return an array. `format` takes one argument: a log entry
Example log entry:
```
{
    date: [object Date],
    logNumber: 11,
    error: [object Error],
    level: 300,
    message: [object Array],
    source: 'Demo',
    ...parameters
}
```

Your formatter may look like this:

```javascript
const MyMessageFormatter = {
    format(entry) {
        let message = [];

        // your implementation

        return message;
    }
}
```
It is important that the `format` method returns an array, even if the formatted message is a string.

# Creating custom log target
Every log target must have `log` method, which takes 1 argument: log entry object.
Example log entry:
```
{
    date: [object Date],
    logNumber: 11,
    error: [object Error],
    level: 300,
    message: [object Array],
    source: 'Demo',
    ...parameters
}
```
Your log target may have the following shape:
```javascript
const MyLogTarget = {
    log(entry) {
        // your implementation
    }
}
```
You may want to use a message formatter. If so, please refer to [ConsoleTarget](#log-targets)

## Demo
https://intermattory.bitbucket.io/logging/

## What's included?
* ArrayTarget
* ConsoleTarget
* [TemplateMessageFormatter](#creating-custom-message-formatter)

## What's not included (yet)?
* Colors
* FileTarget

## Extending
You can easily extend package features:
* [Creating custom log target](#creating-custom-log-target)
* [Creating custom message formatter](#creating-custom-message-formatter)


## Tests
Run tests with ```npm test```


## License

MIT