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
- Host: GitHub
- URL: https://github.com/simplyhexagonal/logger
- Owner: simplyhexagonal
- Created: 2021-09-07T01:35:41.000Z (almost 5 years ago)
- Default Branch: main
- Last Pushed: 2022-03-09T18:41:51.000Z (over 4 years ago)
- Last Synced: 2025-05-06T20:36:09.510Z (about 1 year ago)
- Topics: debug, hacktoberfest, logger, npm, typescript
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/@simplyhexagonal/logger
- Size: 540 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# Simply Hexagonal Logger

[](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).