An open API service indexing awesome lists of open source software.

https://github.com/simplyhexagonal/logger

Extensible debug logger with singleton capabilities, made to easily broadcast to multiple communication channels/transports
https://github.com/simplyhexagonal/logger

debug hacktoberfest logger npm typescript

Last synced: about 2 months ago
JSON representation

Extensible debug logger with singleton capabilities, made to easily broadcast to multiple communication channels/transports

Awesome Lists containing this project

README

          

# Simply Hexagonal Logger
![Tests](https://github.com/simplyhexagonal/logger/workflows/tests/badge.svg)
[![Try logger on RunKit](https://badge.runkitcdn.com/@simplyhexagonal/logger.svg)](https://npm.runkit.com/@simplyhexagonal/logger)

Extensible asynchronous debug logger with singleton capabilities, developed to easily broadcast to
multiple communication channels/transports.

```ts
import Logger from '@simplyhexagonal/logger';

const logger = new Logger({});

logger.debug('Trying to teach', 2, 'tooters', {to: 'toot'});
```

## Open source notice

This project is open to updates by its users, [I](https://github.com/jeanlescure) ensure that PRs are relevant to the community.
In other words, if you find a bug or want a new feature, please help us by becoming one of the
[contributors](#contributors-) ✌️ ! See the [contributing section](#contributing)

## Like this module? ❤

Please consider:

- [Buying me a coffee](https://www.buymeacoffee.com/jeanlescure) ☕
- Supporting Simply Hexagonal on [Open Collective](https://opencollective.com/simplyhexagonal) 🏆
- Starring this repo on [Github](https://github.com/simplyhexagonal/logger) 🌟

## Features

- Define different communication channels per log level (i.e. send `debug` messages to console and `error` messages to Slack)
- Define multiple communication channels per log level
- Specify channel names and then simply use the `.channel()` function to send messages to any specific channel
- Extend your logging capabilities with officially supported transports for: Slack, Discord, Email, SMS, Socket
- Easily make your own transports by implementing and extending the [base LoggerTransport class type](https://github.com/simplyhexagonal/logger/blob/main/src/transports/base.ts) (and submit them via GitHub issue for adoption as an officially supported transport!)
- Use the same logger instance throughout your app (singleton logger)
- Use multiple logger instances throughout your app with de-duplicated communication channels/transports (singleton transports)
- ANSI colors and easy to read formatting for CLI terminals
- CSS colors and easy to read formatting for browser dev consoles

## Usage

Install:

```sh
npm install @simplyhexagonal/logger

yarn add @simplyhexagonal/logger

pnpm i @simplyhexagonal/logger
```

There are three basic configuration elements you should established based on your app's needs and
the environment you will deploy to:

- log level
- communication channels
- error management strategy

### Log level

There are 6 log levels:

- debug
- info
- warn
- error
- fatal
- all

And a bypass level that outputs the message with no date, level, nor colors:

- raw

In your code you log messages to specific log levels:

```ts
logger.debug('hello');

try {
throw new Error('you shall not pass');
} catch {
logger.error('time to turn around');
}

// 2021-10-02T23:47:27.187Z DEBUG 🐞️:
//
// hello
//

// 2021-10-02T23:47:27.191Z ERROR 🚨️:
//
// time to turn around
//
```

Each log level is given a number value:

```ts
{
debug: 0,
info: 10,
warn: 20,
error: 30,
fatal: 40,
all: 100,
raw: 110,
}
```

When `Logger` is instantiated, it will only setup communication channels for the configured log
level and above (i.e. if you selected `warn` then the only `logLevel >= 20` would be initialized)

You can set the log level when instancing the logger:

```ts
import Logger, { LogLevels } from '@simplyhexagonal/logger';

new Logger({
logLevel: LogLevels.DEBUG,
//...
});
```

It is **highly recommended** to set the log level based on a condition that determines the environment
your app is running on:

```ts
const logLevel = (process.env.NODE_ENV === 'production') ? LogLevels.ERROR : LogLevels.DEBUG;

new Logger({
logLevel,
//...
});
```

Setting the log level using environment variables is only recommended as a way to override the
log level configured during instantiation:

```sh
# .env
LOG_LEVEL=debug
```

_(i.e. this is useful if you deem it necessary to turn on debug logging in production environments)_

### Communication channels

Let's say that you have a Discord server with a channel you want to receive only debug messages from
your app, and another channel dedicated to receiving only errors.

The debug channel has the webhook path: `/D3BU9/W3BH00K`

The error channel has the webhook path: `/3RR0R/W3BH00K`

Using `@simplyhexagonal/logger`, you can add the [official Discord transport](https://www.npmjs.com/package/@simplyhexagonal/logger-transport-discord) as a dependency
and import it:

```ts
import DiscordTransport from '@simplyhexagonal/logger-transport-discord';
```

Then, you can configure the transports for each of Logger's log levels:

```ts
const optionsByLevel = {
debug: [
{
transport: LoggerTransportName.DISCORD,
options: {
// debug channel webhook url
destination: 'https://discord.com/api/webhooks/D3BU9/W3BH00K',
},
},
],
info: [],
warn: [],
error: [
{
transport: LoggerTransportName.DISCORD,
options: {
// error channel webhook url
destination: 'https://discord.com/api/webhooks/3RR0R/W3BH00K',
},
},
],
fatal: [],
all: [],
};
```

Then you would let Logger know which transport to use for `LoggerTransportName.DISCORD`:

```ts
const transports = {
[LoggerTransportName.DISCORD]: DiscordTransport,
};
```

The final result would look something like this:

```ts
import {
LogLevels,
LoggerTransportName,
} from '@simplyhexagonal/logger';
import DiscordTransport from '@simplyhexagonal/logger-transport-discord';

const optionsByLevel = {
debug: [
{
transport: LoggerTransportName.DISCORD,
options: {
// debug channel webhook url
destination: 'https://discord.com/api/webhooks/D3BU9/W3BH00K',
},
},
],
info: [],
warn: [],
error: [
{
transport: LoggerTransportName.DISCORD,
options: {
// error channel webhook url
destination: 'https://discord.com/api/webhooks/3RR0R/W3BH00K',
},
},
],
fatal: [],
all: [],
raw: [],
};

const transports = {
[LoggerTransportName.DISCORD]: DiscordTransport,
};

const options = {
logLevel: LogLevels.DEBUG,
optionsByLevel,
transports,
};

const logger = new Logger(options);
```

### Error management strategy

In the previous example there's always a possibility for the Discord webhook to return an error.

When this happens Logger will default to throwing an error which can be handled using `.catch()`:

```ts
logger.debug('hello discord').catch((e) => {
logger.channel(LoggerTransportName.CONSOLE).error(e);
});
```

We understand that this adds unnecessary complexity, as such, you are encouraged to turn on error
catching when instantiating `Logger`. When you do this, Logger will automagically catch transport
errors and log them to console (with `error` log level):

```ts
const options = {
optionsByLevel: optionsWithBadTransport,
catchTransportErrors: true,
};

const logger = new Logger(options);

logger.debug('this will fail due to a bad transport');

// 2021-10-03T04:31:02.191Z ERROR 🚨️:
//
// {
// "transportResult": {
// "destination": "...",
// "channelName": "...",
// "error": {
// "name": "Error",
// "message": "LOGGER ERROR: ...",
// "stack": "Error: LOGGER ERROR: ...",
// // ...
// },
// },
// //...
// }
//
```

Furthermore, you could implement your own fallback transport:

```ts
import { LoggerTransport } from '@simplyhexagonal/logger/transports/base';

class MyTransport extends LoggerTransport {
constructor(options: LoggerTransportOptions['options']) {
const r = Math.random().toString(36).substring(7);
super({...options, r});
}

async error([timestamp, ...message]: unknown[]) {
console.log(timestamp, 'MY LOG:', ...message);

return {
destination: this.destination,
channelName: this.channelName,
};
}
}

const options = {
optionsByLevel: optionsWithBadTransport,
catchTransportErrors: true,
fallbackTransport: MyTransport,
};

const logger = new Logger(options);

logger.debug('this will fail due to a bad transport');

// 2021-10-03T04:31:02.201Z MY LOG: UndefinedTransportError: ...
```

And just as with `LOG_LEVEL`, we have implemented an environment variable for overriding purposes:

```sh
# .env
LOGGER_CATCH_TRANSPORT_ERRORS=true
```

**IMPORTANT NOTE:** we recommend always setting `catchTransportErrors` to `true` in production!

### More options

```ts
import {
LogLevels,
LoggerTransportName,
} from '@simplyhexagonal/logger';
import DiscordTransport from '@simplyhexagonal/logger-transport-discord';

const options = {
logLevel: LogLevels.DEBUG, // default
optionsByLevel: {
debug: [
// ***
// This console config is the default if a log level options array is left empty
// (like `info` in this example)
{
transport: LoggerTransportName.CONSOLE,
options: {
destination: LoggerTransportName.CONSOLE,
channelName: LoggerTransportName.CONSOLE,
},
},
// if you do this you would have only one instance of this transport since all
// transports are singleton (as in pre-filtered and de-duplicated)
// ***
{
transport: LoggerTransportName.DISCORD,
options: {
destination: 'https://discord.com/api/webhooks/D3BU9/W3BH00K',
channelName: 'discord-debug',
},
},
],
info: [], // in this case `loggger.info()` will default to logging to the console
warn: [],
error: [
{
transport: LoggerTransportName.DISCORD,
options: {
destination: 'https://discord.com/api/webhooks/3RR0R/W3BH00K',
},
},
],
fatal: [],
all: [],
raw: [],
},
transports: {
[`${LoggerTransportName.DISCORD}`]: DiscordTransport,
},
singleton: true, // default
catchTransportErrors: false, // default
fallbackTransport: MyTransport,
};

const logger = new Logger(options);
```

## The `all` and `raw` log levels

An important thing to note is that transports defined for the `all` and `raw` log levels will
**always** be instantiated.

In the same way, calls to `logger.all()` or `logger.raw()` will **always** log.

For this reason we suggest only ever using `logger.all()` when an app starts and when an app is
manually stopped, and only use `logger.raw()` when outputting messages you absolutely **need** to
be unformatted.

## Channels

It can be extremely useful to setup multiple channels for specific purposes on a log level:

```ts
const logger = new Logger({
logLevel: LogLevels.DEBUG,
optionsByLevel: {
warn: [],
info: [],
debug: [],
error: [
{
transport: LoggerTransportName.SLACK,
options: {
destination: 'https://hooks.slack.com/services/T123/B456/M0N90',
channelName: 'mongo',
},
},
{
transport: LoggerTransportName.SLACK,
options: {
destination: 'https://hooks.slack.com/services/T123/B456/M55QL',
channelName: 'mssql',
},
},
],
fatal: [],
all: [],
},
});
```

and then send messages to a specific channel depending on the event that's triggering the log:

```ts
const server = async () => {
await mongoose.connect('mongodb://mymongo.cluster:27017/myapp').catch((e) => {
logger.channel('mongo').error(e);
});

await sql.connect('Server=mymssql.cluster,1433;Database=myapp;').catch((e) => {
logger.channel('mssql').error(e);
});
}

server();
```

## Transports

We have the following officially supported transports:

- [WebSocket]() (Coming soon)
- [Discord](https://www.npmjs.com/package/@simplyhexagonal/logger-transport-discord)
- [Slack]() (Coming soon)
- [Email]() (Coming soon)
- [SMS]() (Coming soon)

## Time functions to measure performance

We have implemented functions similar to `console.time()` and `console.timeEnd()` to measure performance:

```ts
const logger = new Logger({});

logger.time('my-timed-operation');

// do things

const result: number = await logger.timeEnd('my-timed-operation');

// transport output:
//
// my-timed-operation: 123.456 ms
//

console.log(result);

// 123.456
```

Three important differences from the console functions to note:

- `logger.timeEnd` is async as it logs to the transports through the `.raw()` method
- `logger.timeEnd` will return a number representing the elapsed time in milliseconds, which is useful for being able to store the value for usage in measuring performance through code
- we used `performance.timeOrigin + performance.now()` instead of `Date.now()` to get the current time in high resolution milliseconds, due to this fact the time measurements are more accurate than some internal time-based functions (e.g. from experience we've seen code like `setTimeout(() => {}, 100)` might be measured to take `99.83 ms` which can break a test if you expect something like `await logger.timeEnd('...') > 100`)

## Contributing

Yes, thank you! This plugin is community-driven, most of its features are from different authors.
Please update the docs and tests and add your name to the `package.json` file.

## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):



Jean Lescure

🚧 💻 📓 ⚠️ 💡 📖

Alejandro Merino

🚧 💻 📓 ⚠️

## License

Copyright (c) 2021-Present [Logger Contributors](https://github.com/simplyhexagonal/logger/#contributors-).

Licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).